Test/list (#19)
* test: add more case for list * feat: allow order by associations in sorting * fix: add more utils test in resourcer and fix except usage * test: fix cases * fix: page default value and max page size * fix: page params and cases * fix: list params * fix: constants in list action * fix: count when include, attributes when except and cases * test: add case for hasMany.
This commit is contained in:
parent
578454d07f
commit
cd0b357887
@ -4,25 +4,35 @@ import { initDatabase, agent } from './index';
|
|||||||
|
|
||||||
describe('list', () => {
|
describe('list', () => {
|
||||||
let db;
|
let db;
|
||||||
|
let now: string;
|
||||||
|
let timestamps: { created_at: string; updated_at: string; };
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
db = await initDatabase();
|
db = await initDatabase();
|
||||||
|
now = (new Date()).toISOString();
|
||||||
|
timestamps = { created_at: now, updated_at: now };
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => db.close());
|
afterAll(() => db.close());
|
||||||
|
|
||||||
describe('common', () => {
|
describe('common', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
const User = db.getModel('users');
|
||||||
|
await User.bulkCreate([
|
||||||
|
{ name: 'a', ...timestamps },
|
||||||
|
{ name: 'b', ...timestamps },
|
||||||
|
{ name: 'c', ...timestamps }
|
||||||
|
]);
|
||||||
|
const users = await User.findAll();
|
||||||
|
|
||||||
const Post = db.getModel('posts');
|
const Post = db.getModel('posts');
|
||||||
const items = [];
|
await Post.bulkCreate(Array(25).fill(null).map((_, index) => ({
|
||||||
for (let index = 0; index < 2; index++) {
|
|
||||||
items.push({
|
|
||||||
title: `title${index}`,
|
title: `title${index}`,
|
||||||
status: index % 2 ? 'published' : 'draft',
|
status: index % 2 ? 'published' : 'draft',
|
||||||
published_at: index % 2 ? new Date() : null
|
published_at: index % 2 ? new Date(2020, 10, 30 - index, 0, 0, 0) : null,
|
||||||
});
|
user_id: users[index % users.length].id,
|
||||||
}
|
...timestamps
|
||||||
await Post.bulkCreate(items);
|
})));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('filter', () => {
|
describe('filter', () => {
|
||||||
@ -61,6 +71,17 @@ describe('list', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('null', () => {
|
describe('null', () => {
|
||||||
|
it('filter[published_at]', async () => {
|
||||||
|
const Post = db.getModel('posts');
|
||||||
|
const expected = await Post.findAll({
|
||||||
|
where: {
|
||||||
|
published_at: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const response = await agent.get('/posts?filter[published_at]');
|
||||||
|
expect(response.body.count).toBe(expected.length);
|
||||||
|
});
|
||||||
|
|
||||||
it('filter[published_at.is]', async () => {
|
it('filter[published_at.is]', async () => {
|
||||||
const Post = db.getModel('posts');
|
const Post = db.getModel('posts');
|
||||||
const expected = await Post.findAll({
|
const expected = await Post.findAll({
|
||||||
@ -104,70 +125,232 @@ describe('list', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('page', () => {
|
describe('page', () => {
|
||||||
it('page by default size(20) should be ok', async () => {
|
it('default page and size(20) should be ok', async () => {
|
||||||
const response = await agent.get('/posts?fields=title&page=1');
|
const response = await agent.get('/posts?fields=title');
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toEqual({
|
||||||
count: 2,
|
count: 25,
|
||||||
page: 1,
|
page: 1,
|
||||||
per_page: 20,
|
per_page: 20,
|
||||||
rows: [ { title: 'title0' }, { title: 'title1' } ],
|
rows: Array(20).fill(null).map((_, index) => ({ title: `title${index}` })),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('page 1 by size(1) should be ok', async () => {
|
it('page 1 by size(1) should be ok', async () => {
|
||||||
const response = await agent.get('/posts?fields=title&page=2&perPage=1');
|
const response = await agent.get('/posts?fields=title&page=1&perPage=1');
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toEqual({
|
||||||
count: 2,
|
count: 25,
|
||||||
page: 2,
|
page: 1,
|
||||||
per_page: 1,
|
per_page: 1,
|
||||||
rows: [ { title: 'title1' } ],
|
rows: [ { title: 'title0' } ],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('page 2 by size(1) should be ok', async () => {
|
it('page 2 by size(1) should be ok', async () => {
|
||||||
const response = await agent.get('/posts?fields=title&page=2&per_page=1');
|
const response = await agent.get('/posts?fields=title&page=2&per_page=1');
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toEqual({
|
||||||
count: 2,
|
count: 25,
|
||||||
page: 2,
|
page: 2,
|
||||||
per_page: 1,
|
per_page: 1,
|
||||||
rows: [ { title: 'title1' } ],
|
rows: [ { title: 'title1' } ],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('page 1 by size(101) should be change to 100', async () => {
|
||||||
|
const response = await agent.get('/posts?fields=title&page=1&per_page=101');
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 25,
|
||||||
|
page: 1,
|
||||||
|
per_page: 100,
|
||||||
|
rows: Array(25).fill(null).map((_, index) => ({ title: `title${index}` })),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('list6', async () => {
|
it('page 2 by size(101) should be change to 100 and result is empty', async () => {
|
||||||
|
const response = await agent.get('/posts?fields=title&page=2&per_page=101');
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 25,
|
||||||
|
page: 2,
|
||||||
|
per_page: 100,
|
||||||
|
rows: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('default page by size(-1) should be change to 100 and result will be 25 items', async () => {
|
||||||
|
const response = await agent.get('/posts?fields=title&per_page=-1');
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 25,
|
||||||
|
page: 1,
|
||||||
|
per_page: 100,
|
||||||
|
rows: Array(25).fill(null).map((_, index) => ({ title: `title${index}` })),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('page 2 by size(-1) should be change to 100 and result is empty', async () => {
|
||||||
|
const response = await agent.get('/posts?fields=title&page=2&per_page=-1');
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 25,
|
||||||
|
page: 2,
|
||||||
|
per_page: 100,
|
||||||
|
rows: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fields', () => {
|
||||||
|
it('custom field', async () => {
|
||||||
const response = await agent.get('/posts?fields=title&filter[customTitle]=title0');
|
const response = await agent.get('/posts?fields=title&filter[customTitle]=title0');
|
||||||
expect(response.body).toEqual({ count: 1, rows: [ { title: 'title0' } ] });
|
expect(response.body).toEqual({
|
||||||
|
count: 1,
|
||||||
|
page: 1,
|
||||||
|
per_page: 20,
|
||||||
|
rows: [ { title: 'title0' } ]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('self field and belongs to field', async () => {
|
||||||
|
const response = await agent.get('/posts?fields=title,user.name&filter[title]=title0');
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 1,
|
||||||
|
page: 1,
|
||||||
|
per_page: 20,
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
title: 'title0',
|
||||||
|
user: {
|
||||||
|
name: 'a'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO(question): 当 fields 只填写了关联字段时,当前表的其他字段是否需要输出?
|
||||||
|
it.skip('only belongs to', async () => {
|
||||||
|
const response = await agent.get('/posts?fields=user&filter[title]=title0');
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 1,
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
title: 'title0',
|
||||||
|
user: { name: 'a' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('except fields', async () => {
|
||||||
|
const response = await agent.get('/posts?fields[except]=status&filter[title]=title0');
|
||||||
|
expect(response.body.rows[0].status).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only and except fields', async () => {
|
||||||
|
const response = await agent.get('/posts?fields=title&fields[except]=status&filter[title]=title0');
|
||||||
|
expect(response.body.rows[0].status).toBeUndefined();
|
||||||
|
expect(response.body.rows).toEqual([{ title: 'title0' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only with belongs to fields', async () => {
|
||||||
|
const response = await agent.get('/posts?fields[only]=title&fields[only]=user.name&filter[title]=title0');
|
||||||
|
expect(response.body.rows[0].user.name).toEqual('a');
|
||||||
|
expect(response.body.rows).toEqual([{ title: 'title0', user: { name: 'a' } }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('appends fields', async () => {
|
||||||
|
const response = await agent.get('/posts?fields[only]=title&fields[appends]=user.name&filter[title]=title0');
|
||||||
|
expect(response.body.rows[0].user.name).toEqual('a');
|
||||||
|
expect(response.body.rows).toEqual([{ title: 'title0', user: { id: 1, name: 'a', ...timestamps } }]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('hasMany', () => {
|
describe('hasMany', () => {
|
||||||
it('list1', async () => {
|
beforeEach(async () => {
|
||||||
|
const User = db.getModel('users');
|
||||||
|
await User.bulkCreate([
|
||||||
|
{ name: 'a' },
|
||||||
|
{ name: 'b' },
|
||||||
|
{ name: 'c' }
|
||||||
|
]);
|
||||||
|
const users = await User.findAll();
|
||||||
const Post = db.getModel('posts');
|
const Post = db.getModel('posts');
|
||||||
const post = await Post.create();
|
const post = await Post.create({ user_id: users[0].id });
|
||||||
await post.updateAssociations({
|
await post.updateAssociations({
|
||||||
comments: [
|
comments: Array(6).fill(null).map((_, index) => ({
|
||||||
{content: 'content1', status: 'published'},
|
content: `content${index}`,
|
||||||
{content: 'content2', status: 'published'},
|
status: index % 2 ? 'published' : 'draft',
|
||||||
{content: 'content3', status: 'draft'},
|
user_id: users[index % users.length].id
|
||||||
{content: 'content4', status: 'published'},
|
}))
|
||||||
{content: 'content5', status: 'draft'},
|
|
||||||
{content: 'content6', status: 'published'},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('get comments of a post', async () => {
|
||||||
|
const Post = db.getModel('posts');
|
||||||
|
const post = await Post.findByPk(1);
|
||||||
const response = await agent
|
const response = await agent
|
||||||
.get(`/posts/${post.id}/comments?page=2&perPage=2&sort=content&fields=content&filter[published]=1`);
|
.get(`/posts/${post.id}/comments?page=2&perPage=2&sort=content&fields=content&filter[published]=1`);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toEqual({
|
||||||
rows: [ { content: 'content4' }, { content: 'content6' } ],
|
rows: [ { content: 'content5' } ],
|
||||||
count: 4,
|
count: 3,
|
||||||
page: 2,
|
page: 2,
|
||||||
per_page: 2
|
per_page: 2
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('get comments within a post, order by comments.content', async () => {
|
||||||
|
const response = await agent
|
||||||
|
.get('/posts?fields=title,comments.content&filter[comments.status]=draft&page=1&perPage=2&sort=-comments.content');
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
rows: [{
|
||||||
|
title: null,
|
||||||
|
comments: [{ content: 'content4' }, { content: 'content2' }, { content: 'content0'}]
|
||||||
|
}],
|
||||||
|
count: 1,
|
||||||
|
page: 1,
|
||||||
|
per_page: 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('get comments of a post, and user of each comment', async () => {
|
||||||
|
const Post = db.getModel('posts');
|
||||||
|
const post = await Post.findByPk(1);
|
||||||
|
const response = await agent
|
||||||
|
.get(`/posts/${post.id}/comments?fields=content,user.name&filter[status]=draft&sort=-content&page=1&perPage=2`);
|
||||||
|
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 3,
|
||||||
|
page: 1,
|
||||||
|
per_page: 2,
|
||||||
|
rows: [
|
||||||
|
{ content: 'content4', user: { name: 'b' } },
|
||||||
|
{ content: 'content2', user: { name: 'c' } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO(bug)
|
||||||
|
it.skip('get posts of user with comments', async () => {
|
||||||
|
const response = await agent
|
||||||
|
.get(`/users/1/posts?fields=comments.content,user.name&filter[comments.status]=draft&sort=-content&page=1&perPage=2`);
|
||||||
|
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
count: 1,
|
||||||
|
page: 1,
|
||||||
|
per_page: 2,
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
comments: [
|
||||||
|
{ content: 'content4' },
|
||||||
|
{ content: 'content2' }
|
||||||
|
],
|
||||||
|
user: { name: 'a' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('belongsToMany', () => {
|
describe('belongsToMany', () => {
|
||||||
it('list1', async () => {
|
beforeEach(async () => {
|
||||||
const Post = db.getModel('posts');
|
const Post = db.getModel('posts');
|
||||||
const post = await Post.create();
|
const post = await Post.create();
|
||||||
await post.updateAssociations({
|
await post.updateAssociations({
|
||||||
@ -181,15 +364,22 @@ describe('list', () => {
|
|||||||
{name: 'tag7', status: 'published'},
|
{name: 'tag7', status: 'published'},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('list1', async () => {
|
||||||
|
const Post = db.getModel('posts');
|
||||||
|
const post = await Post.findByPk(1);
|
||||||
const response = await agent
|
const response = await agent
|
||||||
.get(`/posts/${post.id}/tags?page=2&perPage=2&sort=name&fields=name&filter[published]=1`);
|
.get(`/posts/${post.id}/tags?page=2&perPage=2&sort=-name&fields=name&filter[status]=published`);
|
||||||
expect(response.body).toEqual({
|
expect(response.body).toEqual({
|
||||||
rows: [ { name: 'tag5' }, { name: 'tag7' } ],
|
rows: [ { name: 'tag3' }, { name: 'tag1' } ],
|
||||||
count: 4,
|
count: 4,
|
||||||
page: 2,
|
page: 2,
|
||||||
per_page: 2
|
per_page: 2
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { Context, Next } from '.';
|
import { Context, Next } from '.';
|
||||||
import { Relation, Model, Field, HasOne, HasMany, BelongsTo, BelongsToMany } from '@nocobase/database';
|
import { Relation, Model, Field, HasOne, HasMany, BelongsTo, BelongsToMany } from '@nocobase/database';
|
||||||
|
import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '@nocobase/resourcer';
|
||||||
import { Utils, Op, Sequelize } from 'sequelize';
|
import { Utils, Op, Sequelize } from 'sequelize';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询数据列表
|
* 查询数据列表
|
||||||
@ -17,8 +19,8 @@ import { isEmpty } from 'lodash';
|
|||||||
*/
|
*/
|
||||||
export async function list(ctx: Context, next: Next) {
|
export async function list(ctx: Context, next: Next) {
|
||||||
const {
|
const {
|
||||||
page,
|
page = DEFAULT_PAGE,
|
||||||
perPage,
|
perPage = DEFAULT_PER_PAGE,
|
||||||
sort = [],
|
sort = [],
|
||||||
fields = [],
|
fields = [],
|
||||||
filter = {},
|
filter = {},
|
||||||
@ -50,17 +52,19 @@ export async function list(ctx: Context, next: Next) {
|
|||||||
...options,
|
...options,
|
||||||
context: ctx,
|
context: ctx,
|
||||||
});
|
});
|
||||||
delete options.attributes;
|
const associatedOptions = _.omit(options, [
|
||||||
delete options.limit;
|
'attributes',
|
||||||
delete options.offset;
|
'limit',
|
||||||
delete options.order;
|
'offset',
|
||||||
if (options.include) {
|
'order'
|
||||||
options.include = options.include.map(includeOptions => {
|
]);
|
||||||
|
if (associatedOptions.include) {
|
||||||
|
associatedOptions.include = associatedOptions.include.map(includeOptions => {
|
||||||
includeOptions.attributes = [];
|
includeOptions.attributes = [];
|
||||||
return includeOptions;
|
return includeOptions;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const count = await associated[countAccessor]({ ...options, context: ctx });
|
const count = await associated[countAccessor]({ ...associatedOptions, context: ctx });
|
||||||
data = {
|
data = {
|
||||||
rows,
|
rows,
|
||||||
count,
|
count,
|
||||||
@ -72,9 +76,10 @@ export async function list(ctx: Context, next: Next) {
|
|||||||
context: ctx,
|
context: ctx,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (page || perPage) {
|
if (options.limit || typeof options.offset !== 'undefined') {
|
||||||
data['page'] = 1*(page||1);
|
// Math.round 避免精度问题
|
||||||
data[Utils.underscoredIf('perPage', Model.options.underscored)] = 1*(perPage||20);
|
data['page'] = Math.round((options.offset || 0) / options.limit + 1);
|
||||||
|
data[Utils.underscoredIf('perPage', Model.options.underscored)] = options.limit;
|
||||||
}
|
}
|
||||||
ctx.body = data;
|
ctx.body = data;
|
||||||
await next();
|
await next();
|
||||||
|
@ -6,26 +6,37 @@ import Model, { ModelCtor } from '../../model';
|
|||||||
|
|
||||||
let db: Database;
|
let db: Database;
|
||||||
let Bar: ModelCtor<Model>;
|
let Bar: ModelCtor<Model>;
|
||||||
|
let Baz: ModelCtor<Model>;
|
||||||
|
let Bay: ModelCtor<Model>;
|
||||||
let Foo: ModelCtor<Model>;
|
let Foo: ModelCtor<Model>;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
db = getDatabase();
|
db = getDatabase();
|
||||||
db.table({
|
db.table({
|
||||||
name: 'bazs',
|
name: 'bazs',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'belongsToMany',
|
||||||
|
name: 'bays'
|
||||||
|
},
|
||||||
|
]
|
||||||
});
|
});
|
||||||
db.table({
|
db.table({
|
||||||
name: 'bays',
|
name: 'bays',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'belongsToMany',
|
||||||
|
name: 'bazs'
|
||||||
|
},
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
db.table({
|
db.table({
|
||||||
name: 'bars',
|
name: 'bars',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
name: 'baz',
|
name: 'foo',
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'belongsTo',
|
|
||||||
name: 'bay',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -53,6 +64,8 @@ beforeAll(() => {
|
|||||||
name: 'coos',
|
name: 'coos',
|
||||||
});
|
});
|
||||||
Bar = db.getModel('bars');
|
Bar = db.getModel('bars');
|
||||||
|
Baz = db.getModel('bazs');
|
||||||
|
Bay = db.getModel('bays');
|
||||||
Foo = db.getModel('foos');
|
Foo = db.getModel('foos');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -62,6 +75,10 @@ afterAll(() => db.close());
|
|||||||
|
|
||||||
describe('parseApiJson', () => {
|
describe('parseApiJson', () => {
|
||||||
describe('self table', () => {
|
describe('self table', () => {
|
||||||
|
it('empty', () => {
|
||||||
|
expect(Foo.parseApiJson({})).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
it('filter', () => {
|
it('filter', () => {
|
||||||
const data = Foo.parseApiJson({
|
const data = Foo.parseApiJson({
|
||||||
filter: {
|
filter: {
|
||||||
@ -78,6 +95,26 @@ describe('parseApiJson', () => {
|
|||||||
expect(data).toEqual({ attributes: ['col1'] });
|
expect(data).toEqual({ attributes: ['col1'] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('fields.except', () => {
|
||||||
|
expect(Foo.parseApiJson({
|
||||||
|
fields: {
|
||||||
|
except: ['col']
|
||||||
|
},
|
||||||
|
})).toEqual({ attributes: {
|
||||||
|
exclude: ['col']
|
||||||
|
}});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fields.appends', () => {
|
||||||
|
expect(Foo.parseApiJson({
|
||||||
|
fields: {
|
||||||
|
appends: ['col']
|
||||||
|
},
|
||||||
|
})).toEqual({ attributes: {
|
||||||
|
include: ['col']
|
||||||
|
}});
|
||||||
|
});
|
||||||
|
|
||||||
it('filter and fields', () => {
|
it('filter and fields', () => {
|
||||||
const data = Foo.parseApiJson({
|
const data = Foo.parseApiJson({
|
||||||
fields: ['col1'],
|
fields: ['col1'],
|
||||||
@ -100,10 +137,29 @@ describe('parseApiJson', () => {
|
|||||||
|
|
||||||
it('pagination: only page', () => {
|
it('pagination: only page', () => {
|
||||||
expect(Foo.parseApiJson({
|
expect(Foo.parseApiJson({
|
||||||
page: 1
|
page: 2
|
||||||
|
})).toEqual({
|
||||||
|
offset: 100,
|
||||||
|
limit: 100,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pagination: only perPage and max limit', () => {
|
||||||
|
expect(Foo.parseApiJson({
|
||||||
|
perPage: 1000
|
||||||
})).toEqual({
|
})).toEqual({
|
||||||
offset: 0,
|
offset: 0,
|
||||||
limit: 20,
|
limit: 500,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pagination: perPage=-1 stand for max limit', () => {
|
||||||
|
expect(Foo.parseApiJson({
|
||||||
|
page: 2,
|
||||||
|
perPage: -1
|
||||||
|
})).toEqual({
|
||||||
|
offset: 500,
|
||||||
|
limit: 500,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -117,7 +173,6 @@ describe('parseApiJson', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO(bug): should not contain additional attributes
|
|
||||||
it('sort: multiple self fields', () => {
|
it('sort: multiple self fields', () => {
|
||||||
expect(Foo.parseApiJson({
|
expect(Foo.parseApiJson({
|
||||||
sort: 'a,-b'
|
sort: 'a,-b'
|
||||||
@ -129,13 +184,22 @@ describe('parseApiJson', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO(feature): order by association should be ok
|
it('sort: association field', () => {
|
||||||
it.skip('sort: association field', () => {
|
|
||||||
expect(Bar.parseApiJson({
|
expect(Bar.parseApiJson({
|
||||||
sort: '-foo.a'
|
sort: '-foo.a'
|
||||||
})).toEqual({
|
})).toEqual({
|
||||||
order: [
|
order: [
|
||||||
[{ model: Foo, as: 'foo' }, 'a', 'DESC'],
|
[Foo, 'a', 'DESC'],
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sort: many to many association field', () => {
|
||||||
|
expect(Baz.parseApiJson({
|
||||||
|
sort: '-bays.a'
|
||||||
|
})).toEqual({
|
||||||
|
order: [
|
||||||
|
[Bay, 'a', 'DESC'],
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -177,6 +241,7 @@ describe('parseApiJson', () => {
|
|||||||
where: { col1: 'val1' },
|
where: { col1: 'val1' },
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
distinct: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -200,6 +265,7 @@ describe('parseApiJson', () => {
|
|||||||
where: { col1: 'val1' },
|
where: { col1: 'val1' },
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
distinct: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -59,7 +59,7 @@ describe('toInclude', () => {
|
|||||||
|
|
||||||
const toIncludeExpect = (options: any, logging = false) => {
|
const toIncludeExpect = (options: any, logging = false) => {
|
||||||
const include = toInclude(options, {
|
const include = toInclude(options, {
|
||||||
Model: Foo,
|
model: Foo,
|
||||||
associations: Foo.associations,
|
associations: Foo.associations,
|
||||||
});
|
});
|
||||||
if (logging) {
|
if (logging) {
|
||||||
|
@ -127,6 +127,10 @@ export interface WithCountAttributeOptions {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_OFFSET = 0;
|
||||||
|
export const DEFAULT_LIMIT = 100;
|
||||||
|
export const MAX_LIMIT = 500;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model 相关
|
* Model 相关
|
||||||
*
|
*
|
||||||
@ -234,14 +238,14 @@ export abstract class Model extends SequelizeModel {
|
|||||||
static parseApiJson(options: ApiJsonOptions) {
|
static parseApiJson(options: ApiJsonOptions) {
|
||||||
const { fields, filter, sort, context, page, perPage } = options;
|
const { fields, filter, sort, context, page, perPage } = options;
|
||||||
const data = toInclude({fields, filter, sort}, {
|
const data = toInclude({fields, filter, sort}, {
|
||||||
Model: this,
|
model: this,
|
||||||
associations: this.associations,
|
associations: this.associations,
|
||||||
dialect: this.sequelize.getDialect(),
|
dialect: this.sequelize.getDialect(),
|
||||||
ctx: context,
|
ctx: context,
|
||||||
});
|
});
|
||||||
if (page || perPage) {
|
if (page || perPage) {
|
||||||
data.limit = perPage || 20;
|
data.limit = perPage === -1 ? MAX_LIMIT : Math.min(perPage || DEFAULT_LIMIT, MAX_LIMIT);
|
||||||
data.offset = data.limit * (page > 0 ? page - 1 : 0);
|
data.offset = data.limit * (page > 0 ? page - 1 : DEFAULT_OFFSET);
|
||||||
}
|
}
|
||||||
if (data.attributes && data.attributes.length === 0) {
|
if (data.attributes && data.attributes.length === 0) {
|
||||||
delete data.attributes;
|
delete data.attributes;
|
||||||
|
@ -12,7 +12,7 @@ for (const key in Op) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ToWhereContext {
|
interface ToWhereContext {
|
||||||
Model?: ModelCtor<Model> | Model | typeof Model;
|
model?: ModelCtor<Model> | Model | typeof Model;
|
||||||
associations?: any;
|
associations?: any;
|
||||||
dialect?: string;
|
dialect?: string;
|
||||||
ctx?: any;
|
ctx?: any;
|
||||||
@ -25,7 +25,7 @@ export function toWhere(options: any, context: ToWhereContext = {}) {
|
|||||||
if (Array.isArray(options)) {
|
if (Array.isArray(options)) {
|
||||||
return options.map((item) => toWhere(item, context));
|
return options.map((item) => toWhere(item, context));
|
||||||
}
|
}
|
||||||
const { Model, associations = {}, ctx, dialect } = context;
|
const { model, associations = {}, ctx, dialect } = context;
|
||||||
const items = {};
|
const items = {};
|
||||||
// 先处理「点号」的问题
|
// 先处理「点号」的问题
|
||||||
for (const key in options) {
|
for (const key in options) {
|
||||||
@ -37,13 +37,13 @@ export function toWhere(options: any, context: ToWhereContext = {}) {
|
|||||||
values['$__include'] = values['$__include'] || {}
|
values['$__include'] = values['$__include'] || {}
|
||||||
values['$__include'][key] = toWhere(items[key], {
|
values['$__include'][key] = toWhere(items[key], {
|
||||||
...context,
|
...context,
|
||||||
Model: associations[key].target,
|
model: associations[key].target,
|
||||||
associations: associations[key].target.associations,
|
associations: associations[key].target.associations,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (Model && Model.options.scopes && Model.options.scopes[key]) {
|
else if (model && model.options.scopes && model.options.scopes[key]) {
|
||||||
values['$__scopes'] = values['$__scopes'] || [];
|
values['$__scopes'] = values['$__scopes'] || [];
|
||||||
const scope = Model.options.scopes[key];
|
const scope = model.options.scopes[key];
|
||||||
if (typeof scope === 'function') {
|
if (typeof scope === 'function') {
|
||||||
values['$__scopes'].push({ method: [key, items[key], ctx] });
|
values['$__scopes'].push({ method: [key, items[key], ctx] });
|
||||||
} else {
|
} else {
|
||||||
@ -59,7 +59,7 @@ export function toWhere(options: any, context: ToWhereContext = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ToIncludeContext {
|
interface ToIncludeContext {
|
||||||
Model?: ModelCtor<Model> | Model | typeof Model;
|
model?: ModelCtor<Model> | Model | typeof Model;
|
||||||
sourceAlias?: string;
|
sourceAlias?: string;
|
||||||
associations?: any;
|
associations?: any;
|
||||||
dialect?: string;
|
dialect?: string;
|
||||||
@ -67,14 +67,58 @@ interface ToIncludeContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toInclude(options: any, context: ToIncludeContext = {}) {
|
export function toInclude(options: any, context: ToIncludeContext = {}) {
|
||||||
|
function makeFields(key) {
|
||||||
|
if (!Array.isArray(items[key])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
items[key].forEach(field => {
|
||||||
|
const arr: Array<string> = Array.isArray(field) ? Utils.cloneDeep(field) : field.split('.');
|
||||||
|
const col = arr.shift();
|
||||||
|
// 内嵌的情况
|
||||||
|
if (arr.length > 0) {
|
||||||
|
if (!children.has(col)) {
|
||||||
|
children.set(col, {
|
||||||
|
fields: {
|
||||||
|
only: [],
|
||||||
|
except: [],
|
||||||
|
appends: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
children.get(col).fields[key].push(arr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 关系字段
|
||||||
|
if (associations[col]) {
|
||||||
|
const includeItem: any = {
|
||||||
|
association: col,
|
||||||
|
};
|
||||||
|
if (includeWhere[col]) {
|
||||||
|
includeItem.where = includeWhere[col];
|
||||||
|
}
|
||||||
|
include.set(col, includeItem);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const matches: Array<any> = /(.+)_count$/.exec(col);
|
||||||
|
if (matches && associations[matches[1]]) {
|
||||||
|
attributes[key].push(model.withCountAttribute({
|
||||||
|
association: matches[1],
|
||||||
|
sourceAlias: sourceAlias
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
attributes[key].push(col);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { fields = [] } = options;
|
const { fields = [] } = options;
|
||||||
const { Model, sourceAlias, associations = {}, ctx, dialect } = context;
|
const { model, sourceAlias, associations = {}, ctx, dialect } = context;
|
||||||
|
|
||||||
let where = options.where || {};
|
let where = options.where || {};
|
||||||
|
|
||||||
if (options.filter) {
|
if (options.filter) {
|
||||||
where = toWhere(options.filter, {
|
where = toWhere(options.filter, {
|
||||||
Model,
|
model,
|
||||||
associations,
|
associations,
|
||||||
ctx,
|
ctx,
|
||||||
}) || {};
|
}) || {};
|
||||||
@ -96,6 +140,7 @@ export function toInclude(options: any, context: ToIncludeContext = {}) {
|
|||||||
const children = new Map();
|
const children = new Map();
|
||||||
|
|
||||||
const items = Array.isArray(fields) ? { only: fields } : fields;
|
const items = Array.isArray(fields) ? { only: fields } : fields;
|
||||||
|
items.appends = items.appends || [];
|
||||||
|
|
||||||
let sort = options.sort;
|
let sort = options.sort;
|
||||||
|
|
||||||
@ -106,104 +151,29 @@ export function toInclude(options: any, context: ToIncludeContext = {}) {
|
|||||||
const order = [];
|
const order = [];
|
||||||
|
|
||||||
if (Array.isArray(sort) && sort.length > 0) {
|
if (Array.isArray(sort) && sort.length > 0) {
|
||||||
items.appends = items.appends || [];
|
|
||||||
sort.forEach(key => {
|
sort.forEach(key => {
|
||||||
if (Array.isArray(key)) {
|
if (Array.isArray(key)) {
|
||||||
order.push(key);
|
order.push(key);
|
||||||
} else {
|
} else {
|
||||||
const direction = key[0] === '-' ? 'DESC' : 'ASC';
|
const direction = key[0] === '-' ? 'DESC' : 'ASC';
|
||||||
const field = key.replace(/^-/, '');
|
const keys = key.replace(/^-/, '').split('.');
|
||||||
// TODO(verify): 理论上只是排序并不一定要输出相关字段
|
const field = keys.pop();
|
||||||
// items.appends.push(field);
|
const by = [];
|
||||||
// TODO: 暂时只支持主表排序,后续需要按`.`分隔符拆分 field 为关联排序
|
let associationModel = model;
|
||||||
order.push([field, direction]);
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
const association = model.associations[keys[i]];
|
||||||
|
if (association && association.target) {
|
||||||
|
associationModel = association.target;
|
||||||
|
by.push(associationModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
order.push([...by, field, direction]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(items.only) && items.only.length > 0) {
|
makeFields('only');
|
||||||
items.only.forEach(field => {
|
makeFields('appends');
|
||||||
const arr: Array<string> = Array.isArray(field) ? Utils.cloneDeep(field) : field.split('.');
|
|
||||||
const col = arr.shift();
|
|
||||||
// 内嵌的情况
|
|
||||||
if (arr.length > 0) {
|
|
||||||
if (!children.has(col)) {
|
|
||||||
children.set(col, {
|
|
||||||
fields: {
|
|
||||||
only: [arr],
|
|
||||||
except: [],
|
|
||||||
appends: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
children.get(col).fields.only.push(arr);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 关系字段
|
|
||||||
if (associations[col]) {
|
|
||||||
const includeItem: any = {
|
|
||||||
association: col,
|
|
||||||
};
|
|
||||||
if (includeWhere[col]) {
|
|
||||||
includeItem.where = includeWhere[col];
|
|
||||||
}
|
|
||||||
include.set(col, includeItem);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const matches: Array<any> = /(.+)\_count$/.exec(col);
|
|
||||||
if (matches && associations[matches[1]]) {
|
|
||||||
attributes.only.push(Model.withCountAttribute({
|
|
||||||
association: matches[1],
|
|
||||||
sourceAlias: sourceAlias
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
attributes.only.push(col);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(items.appends) && items.appends.length > 0) {
|
|
||||||
items.appends.forEach(field => {
|
|
||||||
const arr: Array<string> = Array.isArray(field) ? Utils.cloneDeep(field) : field.split('.');
|
|
||||||
const col = arr.shift();
|
|
||||||
// 内嵌的情况
|
|
||||||
if (arr.length > 0) {
|
|
||||||
if (!children.has(col)) {
|
|
||||||
children.set(col, {
|
|
||||||
fields: {
|
|
||||||
only: [],
|
|
||||||
except: [],
|
|
||||||
appends: [arr],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
children.get(col).fields.appends.push(arr);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 关系字段
|
|
||||||
if (associations[col]) {
|
|
||||||
const includeItem: any = {
|
|
||||||
association: col,
|
|
||||||
};
|
|
||||||
if (includeWhere[col]) {
|
|
||||||
includeItem.where = includeWhere[col];
|
|
||||||
}
|
|
||||||
include.set(col, includeItem);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const matches: Array<any> = /(.+)\_count$/.exec(col);
|
|
||||||
if (matches && associations[matches[1]]) {
|
|
||||||
attributes.appends.push(Model.withCountAttribute({
|
|
||||||
association: matches[1],
|
|
||||||
sourceAlias: sourceAlias
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
attributes.appends.push(col);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(items.except) && items.except.length > 0) {
|
if (Array.isArray(items.except) && items.except.length > 0) {
|
||||||
items.except.forEach(field => {
|
items.except.forEach(field => {
|
||||||
@ -245,7 +215,7 @@ export function toInclude(options: any, context: ToIncludeContext = {}) {
|
|||||||
for (const [key, child] of children) {
|
for (const [key, child] of children) {
|
||||||
const result = toInclude(child, {
|
const result = toInclude(child, {
|
||||||
...context,
|
...context,
|
||||||
Model: associations[key].target,
|
model: associations[key].target,
|
||||||
sourceAlias: key,
|
sourceAlias: key,
|
||||||
associations: associations[key].target.associations,
|
associations: associations[key].target.associations,
|
||||||
});
|
});
|
||||||
@ -272,9 +242,11 @@ export function toInclude(options: any, context: ToIncludeContext = {}) {
|
|||||||
// 存在黑名单时
|
// 存在黑名单时
|
||||||
if (attributes.except.length > 0) {
|
if (attributes.except.length > 0) {
|
||||||
data.attributes = {
|
data.attributes = {
|
||||||
include: attributes.appends,
|
|
||||||
exclude: attributes.except,
|
exclude: attributes.except,
|
||||||
};
|
};
|
||||||
|
if (attributes.appends.length) {
|
||||||
|
data.attributes.include = attributes.appends;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 存在白名单时
|
// 存在白名单时
|
||||||
else if (attributes.only.length > 0) {
|
else if (attributes.only.length > 0) {
|
||||||
@ -292,6 +264,7 @@ export function toInclude(options: any, context: ToIncludeContext = {}) {
|
|||||||
data.attributes = [];
|
data.attributes = [];
|
||||||
}
|
}
|
||||||
data.include = Array.from(include.values());
|
data.include = Array.from(include.values());
|
||||||
|
data.distinct = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(where).length > 0) {
|
if (Object.keys(where).length > 0) {
|
||||||
|
@ -1,6 +1,94 @@
|
|||||||
import { parseRequest } from '..';
|
import { mergeFields, parseFields, parseQuery, parseRequest } from '..';
|
||||||
|
|
||||||
describe('utils', () => {
|
describe('utils', () => {
|
||||||
|
describe('parseQuery', () => {
|
||||||
|
it('filter support normal json type', () => {
|
||||||
|
const object = {
|
||||||
|
number: -1.1,
|
||||||
|
string: 'str=a',
|
||||||
|
boolean: true,
|
||||||
|
null: null,
|
||||||
|
array: [5],
|
||||||
|
object: {
|
||||||
|
member: {}
|
||||||
|
},
|
||||||
|
undefined: undefined
|
||||||
|
};
|
||||||
|
const json = JSON.stringify(object);
|
||||||
|
expect(parseQuery(`filter=${encodeURIComponent(json)}&sort=-col`)).toEqual({
|
||||||
|
filter: object,
|
||||||
|
sort: '-col'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseFields', () => {
|
||||||
|
it('plain string fields equal to only', () => {
|
||||||
|
expect(parseFields('name,age')).toEqual({
|
||||||
|
only: ['name', 'age']
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('plain array fields equal to only', () => {
|
||||||
|
expect(parseFields(['name', 'age'])).toEqual({
|
||||||
|
only: ['name', 'age']
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only string fields equal to only', () => {
|
||||||
|
expect(parseFields({ only: 'name,age' })).toEqual({
|
||||||
|
only: ['name', 'age']
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only array fields equal to only', () => {
|
||||||
|
expect(parseFields({ only: ['name', 'age'] })).toEqual({
|
||||||
|
only: ['name', 'age']
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('plain only and expect fields', () => {
|
||||||
|
// input as "fields=title&fields[only]=content&fields[except]=status&fields[except]=created_at"
|
||||||
|
const result = parseFields([ 'title', { only: 'content' }, { except: ['status', 'created_at'] } ]);
|
||||||
|
expect(result).toEqual({
|
||||||
|
only: ['title', 'content'],
|
||||||
|
except: ['status', 'created_at']
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mergeFields', () => {
|
||||||
|
describe('empty default', () => {
|
||||||
|
it('always contains "appends"', async () => {
|
||||||
|
expect(mergeFields({}, { only: ['col'] }))
|
||||||
|
.toEqual({ appends: [], only: ['col'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('appends', async () => {
|
||||||
|
expect(mergeFields({}, { only: ['col1'], appends: ['col2'] }))
|
||||||
|
.toEqual({ only: ['col1'], appends: ['col2'] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('options provided', () => {
|
||||||
|
it('defaults provided: only, except, appends', () => {
|
||||||
|
expect(mergeFields({
|
||||||
|
only: ['col1', 'col2'],
|
||||||
|
except: ['col3'],
|
||||||
|
appends: ['col4']
|
||||||
|
}, {
|
||||||
|
only: ['col1', 'col3', 'col4'],
|
||||||
|
except: ['col5'],
|
||||||
|
appends: ['col6']
|
||||||
|
}))
|
||||||
|
.toEqual({
|
||||||
|
only: ['col1'],
|
||||||
|
appends: ['col6', 'col4']
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('parseRequest', () => {
|
describe('parseRequest', () => {
|
||||||
it('index action', () => {
|
it('index action', () => {
|
||||||
const params = parseRequest({
|
const params = parseRequest({
|
||||||
|
@ -77,6 +77,10 @@ export interface ActionOptions {
|
|||||||
* 每页显示数量
|
* 每页显示数量
|
||||||
*/
|
*/
|
||||||
perPage?: number;
|
perPage?: number;
|
||||||
|
/**
|
||||||
|
* 最大每页显示数量
|
||||||
|
*/
|
||||||
|
maxPerPage?: number;
|
||||||
/**
|
/**
|
||||||
* 中间件
|
* 中间件
|
||||||
*/
|
*/
|
||||||
@ -167,6 +171,10 @@ export interface ActionParams {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_PAGE = 1;
|
||||||
|
export const DEFAULT_PER_PAGE = 20;
|
||||||
|
export const MAX_PER_PAGE = 100;
|
||||||
|
|
||||||
export class Action {
|
export class Action {
|
||||||
|
|
||||||
protected handler: any;
|
protected handler: any;
|
||||||
@ -229,10 +237,32 @@ export class Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async mergeParams(params: ActionParams) {
|
async mergeParams(params: ActionParams) {
|
||||||
const { filter, fields, values, ...restPrams } = params;
|
const {
|
||||||
let { filter: optionsFilter, fields: optionsFields } = this.options;
|
filter,
|
||||||
|
fields,
|
||||||
|
values,
|
||||||
|
page: paramPage,
|
||||||
|
perPage: paramPerPage,
|
||||||
|
per_page,
|
||||||
|
...restPrams
|
||||||
|
} = params;
|
||||||
|
const {
|
||||||
|
filter: optionsFilter,
|
||||||
|
fields: optionsFields,
|
||||||
|
page = DEFAULT_PAGE,
|
||||||
|
perPage = DEFAULT_PER_PAGE,
|
||||||
|
maxPerPage = MAX_PER_PAGE
|
||||||
|
} = this.options;
|
||||||
const options = _.omit(this.options, [
|
const options = _.omit(this.options, [
|
||||||
'defaultValues', 'filter', 'fields', 'handler', 'middlewares', 'middleware',
|
'defaultValues',
|
||||||
|
'filter',
|
||||||
|
'fields',
|
||||||
|
'maxPerPage',
|
||||||
|
'page',
|
||||||
|
'perPage',
|
||||||
|
'handler',
|
||||||
|
'middlewares',
|
||||||
|
'middleware',
|
||||||
]);
|
]);
|
||||||
const data: ActionParams = {
|
const data: ActionParams = {
|
||||||
...options,
|
...options,
|
||||||
@ -241,8 +271,13 @@ export class Action {
|
|||||||
if (!_.isEmpty(this.options.defaultValues) || !_.isEmpty(values)) {
|
if (!_.isEmpty(this.options.defaultValues) || !_.isEmpty(values)) {
|
||||||
data.values = _.merge(_.cloneDeep(this.options.defaultValues), values);
|
data.values = _.merge(_.cloneDeep(this.options.defaultValues), values);
|
||||||
}
|
}
|
||||||
if (data.per_page) {
|
// TODO: to be unified by style funciton
|
||||||
data.perPage = data.per_page;
|
if (per_page || paramPerPage) {
|
||||||
|
data.perPage = per_page || paramPerPage;
|
||||||
|
}
|
||||||
|
if (paramPage || data.perPage) {
|
||||||
|
data.page = paramPage || page;
|
||||||
|
data.perPage = data.perPage == -1 ? maxPerPage : Math.min(data.perPage || perPage, maxPerPage);
|
||||||
}
|
}
|
||||||
// if (typeof optionsFilter === 'function') {
|
// if (typeof optionsFilter === 'function') {
|
||||||
// this.parameters = _.cloneDeep(data);
|
// this.parameters = _.cloneDeep(data);
|
||||||
|
@ -3,7 +3,7 @@ import glob from 'glob';
|
|||||||
import compose from 'koa-compose';
|
import compose from 'koa-compose';
|
||||||
import Action, { ActionName } from './action';
|
import Action, { ActionName } from './action';
|
||||||
import Resource, { ResourceOptions } from './resource';
|
import Resource, { ResourceOptions } from './resource';
|
||||||
import { parseRequest, getNameByParams, ParsedParams, requireModule } from './utils';
|
import { parseRequest, getNameByParams, ParsedParams, requireModule, parseQuery } from './utils';
|
||||||
import { pathToRegexp } from 'path-to-regexp';
|
import { pathToRegexp } from 'path-to-regexp';
|
||||||
|
|
||||||
export interface ResourcerContext {
|
export interface ResourcerContext {
|
||||||
@ -256,15 +256,7 @@ export class Resourcer {
|
|||||||
// action 需要 clone 之后再赋给 ctx
|
// action 需要 clone 之后再赋给 ctx
|
||||||
ctx.action = this.getAction(nameRule(params), params.actionName).clone();
|
ctx.action = this.getAction(nameRule(params), params.actionName).clone();
|
||||||
ctx.action.setContext(ctx);
|
ctx.action.setContext(ctx);
|
||||||
// 自带 query 处理的不太给力,需要用 qs 转一下
|
const query = parseQuery(ctx.request.querystring);
|
||||||
const query = qs.parse(ctx.request.querystring, {
|
|
||||||
// 原始 query string 中如果一个键连等号“=”都没有可以被认为是 null 类型
|
|
||||||
strictNullHandling: true
|
|
||||||
});
|
|
||||||
// filter 支持 json string
|
|
||||||
if (typeof query.filter === 'string') {
|
|
||||||
query.filter = JSON.parse(query.filter);
|
|
||||||
}
|
|
||||||
// 兼容 ctx.params 的处理,之后的版本里会去掉
|
// 兼容 ctx.params 的处理,之后的版本里会去掉
|
||||||
ctx[paramsKey] = {
|
ctx[paramsKey] = {
|
||||||
table: params.resourceName,
|
table: params.resourceName,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { pathToRegexp } from 'path-to-regexp';
|
import { pathToRegexp } from 'path-to-regexp';
|
||||||
|
import qs from 'qs';
|
||||||
import { ResourceType } from './resource';
|
import { ResourceType } from './resource';
|
||||||
|
|
||||||
export interface ParseRequest {
|
export interface ParseRequest {
|
||||||
@ -189,6 +190,20 @@ export function requireModule(module: any) {
|
|||||||
return module.__esModule ? module.default : module;
|
return module.__esModule ? module.default : module;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseQuery(input: string): any {
|
||||||
|
// 自带 query 处理的不太给力,需要用 qs 转一下
|
||||||
|
const query = qs.parse(input, {
|
||||||
|
// 原始 query string 中如果一个键连等号“=”都没有可以被认为是 null 类型
|
||||||
|
strictNullHandling: true
|
||||||
|
});
|
||||||
|
// filter 支持 json string
|
||||||
|
if (typeof query.filter === 'string') {
|
||||||
|
query.filter = JSON.parse(query.filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
export function parseFields(fields: any) {
|
export function parseFields(fields: any) {
|
||||||
if (!fields) {
|
if (!fields) {
|
||||||
return {}
|
return {}
|
||||||
@ -197,10 +212,22 @@ export function parseFields(fields: any) {
|
|||||||
fields = fields.split(',').map(field => field.trim());
|
fields = fields.split(',').map(field => field.trim());
|
||||||
}
|
}
|
||||||
if (Array.isArray(fields)) {
|
if (Array.isArray(fields)) {
|
||||||
return {
|
const onlyFields = [];
|
||||||
only: fields,
|
const output: any = {};
|
||||||
appends: [],
|
fields.forEach(item => {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
onlyFields.push(item);
|
||||||
|
} else if (typeof item === 'object') {
|
||||||
|
if (item.only) {
|
||||||
|
onlyFields.push(...item.only.toString().split(','));
|
||||||
}
|
}
|
||||||
|
Object.assign(output, parseFields(item));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (onlyFields.length) {
|
||||||
|
output.only = onlyFields;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
}
|
}
|
||||||
if (fields.only && typeof fields.only === 'string') {
|
if (fields.only && typeof fields.only === 'string') {
|
||||||
fields.only = fields.only.split(',').map(field => field.trim());
|
fields.only = fields.only.split(',').map(field => field.trim());
|
||||||
|
Loading…
Reference in New Issue
Block a user