refactor: improve action unit test cases

This commit is contained in:
chenos 2021-09-09 22:38:39 +08:00
parent 409eb38d00
commit 99d33a0241
43 changed files with 1894 additions and 2901 deletions

View File

@ -11,8 +11,7 @@
"@nocobase/resourcer": "^0.4.0-alpha.7"
},
"devDependencies": {
"koa": "^2.13.0",
"sequelize": "^6.3.4"
"@nocobase/test": "^0.4.0-alpha.7"
},
"repository": {
"type": "git",

View File

@ -1,16 +0,0 @@
import { ActionOptions } from '@nocobase/resourcer';
import { create } from '../../actions/common';
export default {
values: {
meta: {
location: 'Kunming'
}
},
fields: {
except: ['sort', 'user.profile', 'comments.status']
},
handler: create
} as unknown as ActionOptions;

View File

@ -1,10 +0,0 @@
import { ActionOptions } from '@nocobase/resourcer';
import { create } from '../../actions/common';
export default {
fields: {
only: ['title']
},
handler: create
} as unknown as ActionOptions;

View File

@ -1,17 +0,0 @@
import { ActionOptions } from '@nocobase/resourcer';
import { list } from '../../actions/common';
const now = new Date();
const before7Days = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7);
export default {
filter: {
status: 'published',
published_at: {
gte: before7Days.toISOString(),
lt: now.toISOString()
}
},
handler: list
} as unknown as ActionOptions;

View File

@ -1,16 +0,0 @@
import { ActionOptions } from '@nocobase/resourcer';
import { update } from '../../actions/common';
export default {
values: {
meta: {
location: 'Kunming'
}
},
fields: {
except: ['title']
},
handler: update
} as unknown as ActionOptions;

View File

@ -1,10 +0,0 @@
import { ActionOptions } from '@nocobase/resourcer';
import { update } from '../../actions/common';
export default {
fields: {
only: ['title']
},
handler: update
} as unknown as ActionOptions;

View File

@ -1,23 +1,21 @@
import { initDatabase, agent } from './index';
import { mockServer, MockServer } from '@nocobase/test';
import { registerActions } from '..';
describe('add', () => {
let db;
let api: MockServer;
beforeEach(async () => {
db = await initDatabase();
api = mockServer({
dataWrapping: false,
});
registerActions(api);
});
afterAll(() => db.close());
afterEach(async () => {
return api.destroy();
});
it('add', async () => {
it('belongsToMany1', async () => {
const [Post, Tag] = db.getModels(['posts', 'tags']);
let post = await Post.create();
let tag1 = await Tag.create({ name: 'tag1' });
let tag2 = await Tag.create({ name: 'tag2' });
await agent.post(`/posts/${post.id}/tags:add/${tag1.id}`);
await agent.post(`/posts/${post.id}/tags:add/${tag2.id}`);
let [tag01, tag02] = await post.getTags();
expect(tag01.id).toBe(tag1.id);
expect(tag02.id).toBe(tag2.id);
});
});

View File

@ -1,109 +1,65 @@
import { initDatabase, agent } from './index';
import { mockServer, MockServer } from '@nocobase/test';
import { registerActions } from '..';
describe('create', () => {
let db;
let api: MockServer;
beforeEach(async () => {
db = await initDatabase();
api = mockServer({
dataWrapping: false,
});
registerActions(api);
});
afterAll(() => db.close());
describe('single', () => {
it('create with hasMany items', async () => {
const response = await agent
.post('/posts')
.send({
title: 'title1',
comments: [
{ content: 'content1' },
{ content: 'content2' },
]
});
expect(response.body.title).toBe('title1');
const createdPost = await agent.get(`/posts/${response.body.id}?fields=comments`);
expect(createdPost.body.comments.length).toBe(2);
afterEach(async () => {
return api.destroy();
});
it('create with defaultValues by custom action', async () => {
const response = await agent
.post('/posts:create1')
.send({
title: 'title1',
});
expect(response.body.meta).toEqual({ location: 'Kunming' });
});
it('create with options.fields.except by custom action', async () => {
const response = await agent
.post('/posts:create1')
.send({
title: 'title1',
sort: 100,
user: { name: 'aaa', profile: { email: 'email' } },
comments: [
{ content: 'comment1', status: 'published' },
{ content: 'comment2', status: 'draft' },
]
});
expect(response.body.sort).toBe(1);
expect(response.body.user_id).toBe(1);
const postWithUser = await agent
.get(`/posts/${response.body.id}?fields=user`);
expect(postWithUser.body.user.id).toBe(1);
const user = await agent
.get(`/users/${postWithUser.body.user.id}?fields=profile`);
expect(user.body.profile).toBe(null);
const postWithComments = await agent
.get(`/posts/${response.body.id}?fields=comments`);
const comments = postWithComments.body.comments.map(({ content, status }) => ({ content, status }));
expect(comments).toEqual([
{ content: 'comment1', status: null },
{ content: 'comment2', status: null },
]);
});
it('create with options.fields.only by custom action', async () => {
const response = await agent
.post('/posts:create2')
.send({
title: 'title1',
meta: { a: 1 }
});
expect(response.body.title).toBe('title1');
expect(response.body.meta).toBe(null);
const result = await agent
.get(`/posts/${response.body.id}`);
expect(result.body.title).toBe('title1');
expect(result.body.meta).toBe(null);
});
});
describe('hasMany', () => {
it('create', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
const response = await agent
.post(`/posts/${post.id}/comments`)
.send({
content: 'content1',
api.database.table({
name: 'tests',
fields: [
{ type: 'string', name: 'name' },
],
});
await api.database.sync();
const response = await api.resource('tests').create({
values: { name: 'n1' },
});
expect(response.body.name).toBe('n1');
});
expect(response.body.post_id).toBe(post.id);
expect(response.body.content).toBe('content1');
const comments = await agent
.get('/comments?fields=id,content');
expect(comments.body.count).toBe(1);
expect(comments.body.rows).toEqual([{
id: 1,
content: 'content1'
}]);
it('associations', async () => {
api.database.table({
name: 'comments',
fields: [
{ type: 'string', name: 'content' },
],
});
api.database.table({
name: 'posts',
fields: [
{ type: 'string', name: 'title' },
{ type: 'hasMany', name: 'comments' },
],
});
await api.database.sync();
const [Post, Comment] = api.database.getModels(['posts', 'comments']);
const response = await api.resource('posts').create({
values: {
title: 't1',
comments: [
{ content: 'c1' },
{ content: 'c2' },
]
},
});
expect(await Post.count()).toBe(1);
expect(await Comment.count()).toBe(2);
await api.resource('posts.comments').create({
associatedKey: response.body.id,
values: { content: 'c1' },
});
expect(await Comment.count()).toBe(3);
});
});

View File

@ -1,148 +1,62 @@
import { initDatabase, agent } from './index';
import { mockServer, MockServer } from '@nocobase/test';
import { registerActions } from '..';
describe('destroy', () => {
let db;
let api: MockServer;
beforeEach(async () => {
db = await initDatabase();
api = mockServer({
dataWrapping: false,
});
afterAll(() => db.close());
describe('single', () => {
it('common1', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
const response = await agent
.delete(`/posts/${post.id}`);
// console.log(response.body);
expect(response.body.count).toBe(1);
});
it('batch delete by filter', async () => {
const Post = db.getModel('posts');
const posts = await Post.bulkCreate([
{ title: 'title1', status: 'published' },
{ title: 'title2', status: 'draft' },
{ title: 'title3', status: 'published' },
{ title: 'title4', status: 'draft' },
]);
await agent
.delete('/posts?filter[status]=draft');
const published = await Post.findAll();
expect(published.length).toBe(2);
expect(published.map(({ title, status }) => ({ title, status }))).toEqual([
{ title: 'title1', status: 'published' },
{ title: 'title3', status: 'published' }
]);
});
});
describe('hasOne', () => {
it('delete has-one item', async () => {
const User = db.getModel('users');
const user = await User.create();
await user.updateAssociations({
profile: {
email: 'email1122',
}
});
const response = await agent
.delete(`/users/${user.id}/profile`);
const profile = await user.getProfile();
expect(profile).toBeNull();
});
});
describe('hasMany', () => {
it('delete single item in has-many list', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
comments: [
{ content: 'content111222' },
registerActions(api);
api.database.table({
name: 'posts',
fields: [
{ type: 'string', name: 'title' },
{ type: 'hasMany', name: 'comments' },
],
});
const [comment] = await post.getComments();
await agent
.delete(`/posts/${post.id}/comments/${comment.id}`);
const count = await post.countComments();
expect(count).toBe(0);
api.database.table({
name: 'comments',
fields: [{ type: 'string', name: 'content' }],
});
await api.database.sync();
});
it('delete batch items in has-many list', async () => {
const Post = db.getModel('posts');
afterEach(async () => {
return api.destroy();
});
it('destroy', async () => {
const Post = api.database.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
comments: [
{ content: 'content1', status: 'published' },
{ content: 'content2', status: 'draft' },
{ content: 'content3', status: 'published' },
{ content: 'content4', status: 'draft' },
],
});
await agent
.delete(`/posts/${post.id}/comments?filter[status]=draft`);
const comments = await post.getComments();
expect(comments.length).toBe(2);
expect(comments.map(({ content }) => content)).toEqual(['content1', 'content3']);
expect(
await Post.count({
where: { id: post.id },
}),
).toBe(1);
await api.resource('posts').destroy({
resourceKey: post.id,
});
expect(
await Post.count({
where: { id: post.id },
}),
).toBe(0);
});
describe('belongsTo', () => {
it('delete belongs-to item', async () => {
const Post = db.getModel('posts');
it('destroy associations', async () => {
const [Post, Comment] = api.database.getModels(['posts', 'comments']);
const post = await Post.create();
const comment = await Comment.create();
await post.updateAssociations({
user: { name: 'name121234' },
comments: [comment],
});
await agent.delete(`/posts/${post.id}/user:destroy`);
const user = await post.getUser();
expect(user).toBeNull();
});
});
describe('belongsToMany', () => {
it('delete single target item', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
tags: [
{ name: 'tag112233' },
],
});
const [tag] = await post.getTags();
await agent
.delete(`/posts/${post.id}/tags:destroy/${tag.id}`);
const tags = await post.getTags();
expect(tags.length).toBe(0);
const PostsTags = db.getModel('posts_tags');
const postsTags = await PostsTags.findAll();
expect(postsTags.length).toBe(0);
});
it('delete batch target item by filter', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
tags: [
{ name: 'tag1', status: 'enabled' },
{ name: 'tag2', status: 'disabled' },
{ name: 'tag3', status: 'enabled' },
{ name: 'tag4', status: 'disabled' },
],
});
await agent
.delete(`/posts/${post.id}/tags:destroy?filter[status]=disabled`);
const tags = await post.getTags();
expect(tags.length).toBe(2);
const PostsTags = db.getModel('posts_tags');
const postsTags = await PostsTags.findAll();
expect(postsTags.length).toBe(2);
await api.resource('posts.comments').destroy({
resourceKey: comment.id,
associatedKey: post.id,
});
const comment2 = await Comment.findByPk(comment.id);
expect(comment2).toBeNull();
});
});

View File

@ -1,91 +1,57 @@
import { initDatabase, agent } from './index';
import { mockServer, MockServer } from '@nocobase/test';
import { registerActions } from '..';
describe('get', () => {
let db;
let api: MockServer;
beforeEach(async () => {
db = await initDatabase();
api = mockServer({
dataWrapping: false,
});
afterAll(() => db.close());
it('common1', async () => {
const Post = db.getModel('posts');
const post = await Post.create({
title: 'title11112222'
});
const response = await agent
.get(`/posts/${post.id}`);
expect(response.body.title).toBe('title11112222');
});
it('hasOne1', async () => {
const User = db.getModel('users');
const user = await User.create();
const response = await agent
.get(`/users/${user.id}/profile?fields=email`);
expect(response.body).toEqual({});
});
it('hasOne2', async () => {
const User = db.getModel('users');
const user = await User.create();
await user.updateAssociations({
profile: {
email: 'email1',
},
});
const response = await agent
.get(`/users/${user.id}/profile?fields=email`);
expect(response.body).toEqual({ email: 'email1' });
});
it('hasMany1', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
comments: [
{ content: 'content111222' },
registerActions(api);
api.database.table({
name: 'posts',
fields: [
{ type: 'string', name: 'title' },
{ type: 'hasMany', name: 'comments' },
],
});
const [comment] = await post.getComments();
const response = await agent
.get(`/posts/${post.id}/comments/${comment.id}`);
expect(response.body.post_id).toBe(post.id);
expect(response.body.content).toBe('content111222');
});
it('belongsTo1', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
const response = await agent
.get(`/posts/${post.id}/user?fields=name`);
expect(response.body).toEqual({});
});
it('belongsTo2', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
user: { name: 'name121234' },
});
const response = await agent
.get(`/posts/${post.id}/user?fields=name`);
expect(response.body).toEqual({ name: 'name121234' });
});
it('belongsToMany', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
tags: [
{ name: 'tag112233' },
api.database.table({
name: 'comments',
fields: [
{ type: 'string', name: 'content' },
],
});
const [tag] = await post.getTags();
const response = await agent
.get(`/posts/${post.id}/tags/${tag.id}?fields=name,posts.id`);
expect(response.body.posts[0].id).toBe(post.id);
expect(response.body.name).toBe('tag112233');
await api.database.sync();
});
afterEach(async () => {
return api.destroy();
});
it('get', async () => {
const Post = api.database.getModel('posts');
const post = await Post.create({ title: 't1' });
const response = await api.resource('posts').get({
resourceKey: post.id,
fields: ['id', 'title']
});
expect(post.toJSON()).toMatchObject(response.body);
});
it('get associations', async () => {
const [Post, Comment] = api.database.getModels(['posts', 'comments']);
const post = await Post.create();
const comment = await Comment.create({ content: 'c2' });
await post.updateAssociations({
comments: [comment]
});
const response = await api.resource('posts.comments').get({
resourceKey: comment.id,
associatedKey: post.id,
fields: ['id', 'post_id', 'content']
});
const comment2 = await Comment.findByPk(comment.id);
expect(comment2.toJSON()).toMatchObject(response.body);
});
});

View File

@ -1,146 +0,0 @@
import { resolve } from 'path';
import glob from 'glob';
import Koa from 'koa';
import { Dialect } from 'sequelize';
import bodyParser from 'koa-bodyparser';
import supertest from 'supertest';
import Database, { requireModule } from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import associated from '../middlewares/associated';
import actions from '..';
import list1 from './actions/list1';
import create1 from './actions/create1';
import create2 from './actions/create2';
import update1 from './actions/update1';
import update2 from './actions/update2';
function getTestKey() {
const { id } = require.main;
const key = id
.replace(`${process.env.PWD}/packages`, '')
.replace(/src\/__tests__/g, '')
.replace('.test.ts', '')
.replace(/[^\w]/g, '_')
.replace(/_+/g, '_')
.replace(/^_|_$/g, '');
return key
}
const connection = {
config: {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: Number.parseInt(process.env.DB_PORT, 10),
dialect: process.env.DB_DIALECT as Dialect,
define: {
hooks: {
beforeCreate(model, options) {
},
},
},
logging: process.env.DB_LOG_SQL === 'on' ? console.log : false,
},
database: null,
create() {
this.database = new Database(this.config);
},
get() {
return this.database;
}
};
const tableFiles = glob.sync(`${resolve(__dirname, './tables')}/*.ts`);
// resourcer 在内存中是单例,需要谨慎使用
export const resourcer = new Resourcer();
resourcer.use(associated);
resourcer.registerActionHandlers({ ...actions.associate, ...actions.common });
resourcer.define({
name: 'posts',
actions: {
...actions.common,
list1,
create1,
create2,
update1,
update2
},
});
resourcer.define({
name: 'comments',
actions: actions.common,
});
resourcer.define({
name: 'users',
actions: actions.common,
});
resourcer.define({
name: 'profiles',
actions: actions.common,
});
resourcer.define({
type: 'hasOne',
name: 'users.profile',
actions: actions.associate,
});
resourcer.define({
type: 'hasMany',
name: 'posts.comments',
actions: actions.associate,
});
resourcer.define({
type: 'hasMany',
name: 'users.posts',
actions: actions.associate,
});
resourcer.define({
type: 'belongsTo',
name: 'posts.user',
actions: actions.associate,
});
resourcer.define({
type: 'belongsToMany',
name: 'posts.tags',
actions: actions.associate,
});
const app = new Koa();
app.use(async (ctx, next) => {
ctx.db = connection.get();
await next();
});
app.use(bodyParser());
app.use(resourcer.middleware());
// 使用 agent 可以减少部分模板代码
export const agent = supertest.agent(app.callback());
export async function initDatabase() {
if (!connection.get()) {
connection.create();
}
const database = connection.get();
// 由于 jest 每个测试文件是独立 worker 进程的机制,各个进程使用的是不同的数据库连接实例,但又对应到同一个数据库。
// 所以不同测试文件会存在表结构冲突,这里使用了基于文件唯一的 key 作为后缀区分不同进程使用的数据库表,以满足并行测试。
const key = getTestKey();
tableFiles.forEach(file => {
const options = requireModule(file);
database.table(typeof options === 'function' ? options(database) : {
...options,
tableName: `${key}_${options.tableName}`
});
});
await database.sync({
force: true,
});
return database;
}

View File

@ -1,626 +1,135 @@
import { literal, Op } from 'sequelize';
import { initDatabase, agent } from './index';
import { mockServer, MockServer } from '@nocobase/test';
import { registerActions } from '..';
describe('list', () => {
let db;
let now: Date;
let nowString: string;
let timestamps: { created_at: Date; updated_at: Date; };
let timestampsStrings;
let api: MockServer;
beforeEach(async () => {
db = await initDatabase();
now = new Date();
nowString = now.toISOString()
timestamps = { created_at: now, updated_at: now };
timestampsStrings = { created_at: nowString, updated_at: nowString };
api = mockServer({
dataWrapping: false,
});
afterAll(() => db.close());
describe('common', () => {
beforeEach(async () => {
const User = db.getModel('users');
await User.bulkCreate([
{ name: 'a', ...timestamps, nicknames: ['aa', 'aaa'] },
{ name: 'b', ...timestamps, nicknames: [] },
{ name: 'c', ...timestamps }
]);
const users = await User.findAll();
users[0].updateSingleAssociation('profile', { city: '1101', interest: [1] });
users[1].updateSingleAssociation('profile', { city: '3710', interest: [1, 2] });
users[2].updateSingleAssociation('profile', { city: '5301', interest: [] });
const Post = db.getModel('posts');
await Post.bulkCreate(Array(25).fill(null).map((_, index) => ({
title: `title${index}`,
status: index % 2 ? 'published' : 'draft',
published_at: index % 2 ? new Date(now.getFullYear(), now.getMonth(), now.getDate() - index, 0, 0, 0) : null,
user_id: users[index % users.length].id,
...timestamps
})));
registerActions(api);
api.database.table({
name: 'comments',
fields: [
{ type: 'string', name: 'content' },
{ type: 'string', name: 'status', defaultValue: 'draft' },
],
});
describe('filter', () => {
describe('equal', () => {
it('should be filtered by `status` equal to `published`', async () => {
const Post = db.getModel('posts');
const response = await agent.get('/posts?filter[status]=published');
expect(response.body.count).toBe(await Post.count({ where: { status: 'published' } }));
api.database.table({
name: 'posts',
fields: [
{ type: 'string', name: 'title' },
{ type: 'hasMany', name: 'comments' },
{ type: 'string', name: 'status', defaultValue: 'draft' },
],
});
it('should be filtered by `title` equal to `title1`', async () => {
const Post = db.getModel('posts');
const response = await agent.get('/posts?filter[title]=title1');
expect(response.body.count).toBe(await Post.count({
where: {
title: 'title1',
},
}));
await api.database.sync();
const [Post, Comment] = api.database.getModels(['posts', 'comments']);
for (let index = 1; index < 4; index++) {
const post = await Post.create({ title: `t${index}` });
await post.updateAssociations({
comments: [{ content: 'c1', status: 'publish' }, { content: 'c2' }, { content: 'c3' }],
});
});
describe('not equal', () => {
it('filter[status][ne]=published', async () => {
const Post = db.getModel('posts');
const drafts = (await Post.findAll({
where: {
status: {
[Op.ne]: 'published'
}
}
})).map(item => item.get('title'));
const response = await agent.get('/posts?filter[status][ne]=published');
expect(response.body.count).toBe(drafts.length);
expect(response.body.rows[0].title).toBe(drafts[0]);
});
});
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 () => {
const Post = db.getModel('posts');
const expected = await Post.findAll({
where: {
published_at: {
[Op.is]: null
}
}
});
const response = await agent.get('/posts?filter[published_at.is]');
expect(response.body.count).toBe(expected.length);
});
it('filter[published_at.not]', async () => {
const Post = db.getModel('posts');
const expected = await Post.findAll({
where: {
published_at: {
[Op.not]: null
}
}
});
const response = await agent.get('/posts?filter[published_at.not]');
expect(response.body.count).toBe(expected.length);
});
// TODO(bug): should use `user.is`
it('filter[user_id.is]', async () => {
const Post = db.getModel('posts');
const expected = await Post.findAll({
where: {
user_id: {
[Op.is]: null
}
}
});
const response = await agent.get('/posts?filter[user_id.is]');
expect(response.body.count).toBe(expected.length);
});
});
describe('merge params with action options', () => {
it('plain key-value filter', async () => {
const response = await agent.get('/posts:list1?filter[status]=draft');
expect(response.body.count).toBe(0);
});
it('date filter', async () => {
// const before1Days = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
const before3Days = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 3);
const response = await agent.get(`/posts:list1?filter[published_at.gt]=${before3Days.toISOString()}`);
expect(response.body.count).toBe(1);
expect(response.body.rows[0].id).toBe(2);
});
});
describe('custom ops', () => {
it('$null', async () => {
const Post = db.getModel('posts');
const expected = await Post.findAll({
where: {
published_at: null
}
});
const response = await agent.get('/posts?filter[published_at.$null]=');
expect(response.body.count).toBe(expected.length);
});
describe('$anyOf', () => {
describe('single', () => {
// TODO(question): 是否应该用 in/notIn 来处理单项?
// 或者单项存值也使用 JSON 类型也可以。
it.skip('$anyOf', async () => {
// const Profile = db.getModel('profiles');
// const profiles = await Profile.findAll();
const response = await agent.get('/profiles?filter[city.$anyOf]=Beijing,Weihai');
console.log(response.body);
// expect(response.body.count).toBe(2);
});
});
describe('multiple', () => {
it('$anyOf for 1 element in definition', async () => {
const User = db.getModel('users');
const expected = await User.findOne({
where: {
nicknames: { [Op.contains]: 'aa' }
}
});
const response = await agent.get('/users?filter[nicknames.$anyOf][]=aa');
expect(response.body.count).toBe(1);
expect(response.body.rows[0].name).toBe(expected.name);
});
it('$anyOf for all elements in definition', async () => {
const User = db.getModel('users');
const expected = await User.findOne({
where: {
nicknames: {
[Op.or]: [
{ [Op.contains]: 'aaa' },
{ [Op.contains]: 'aa' }
]
}
}
});
const response = await agent.get('/users?filter[nicknames.$anyOf]=aaa,aa');
expect(response.body.count).toBe(1);
expect(response.body.rows[0].name).toBe(expected.name);
});
it('$anyOf for some element not in definition', async () => {
const User = db.getModel('users');
const expected = await User.findOne({
where: {
nicknames: { [Op.or]: [{ [Op.contains]: ['aaa'] }, { [Op.contains]: ['a'] }] }
}
});
const response = await agent.get('/users?filter[nicknames.$anyOf]=aaa,a');
expect(response.body.count).toBe(1);
expect(response.body.rows[0].name).toBe(expected.name);
});
it('$anyOf for no element', async () => {
const User = db.getModel('users');
const expected = await User.findAll();
const response = await agent.get('/users?filter={"nicknames.$anyOf":[]}');
expect(response.body.count).toBe(expected.length);
});
});
});
describe('$allOf', () => {
it('$allOf for no element', async () => {
const response = await agent.get('/users?filter={"nicknames.$allOf":[]}');
expect(response.body.count).toBe(3);
});
it('$allOf for different element', async () => {
const response = await agent.get('/users?filter[nicknames.$allOf]=a,aa');
expect(response.body.count).toBe(0);
});
it('$allOf for less element', async () => {
const response = await agent.get('/users?filter[nicknames.$allOf][]=aa&fields=name,nicknames');
expect(response.body.count).toBe(1);
expect(response.body.rows).toEqual([
{ name: 'a', nicknames: ['aa', 'aaa'] }
]);
});
it('$allOf for same element', async () => {
const response = await agent.get('/users?filter[nicknames.$allOf]=aa,aaa&fields=name,nicknames');
expect(response.body.count).toBe(1);
expect(response.body.rows).toEqual([
{ name: 'a', nicknames: ['aa', 'aaa'] }
]);
});
it('$allOf for more element', async () => {
const response = await agent.get('/users?filter[nicknames.$allOf]=a,aa,aaa');
expect(response.body.count).toBe(0);
});
});
// TODO(bug): 需要 toWhere 重构和操作符函数修改
describe.skip('$noneOf', () => {
it('$noneOf for no element', async () => {
const response = await agent.get('/users?filter={"nicknames.$noneOf":[]}');
expect(response.body.count).toBe(3);
});
it('$noneOf for different element', async () => {
const User = db.getModel('users');
const users = await User.findAll({
where: {
[Op.not]: {
// 不使用 or 包装两个同一个 col 的条件会被转化成 and与官方文档不符
// WHERE NOT ("users"."nicknames" @> '"aa"' AND "users"."nicknames" @> '"a"')
[Op.or]: [
{ nicknames: { [Op.contains]: 'aa' } },
{ nicknames: { [Op.contains]: 'a' } },
]
}
}
});
console.log(users);
// const response = await agent.get('/users?filter[nicknames.$noneOf]=a,aa');
// expect(response.body.count).toBe(2);
});
it('$noneOf for less element', async () => {
const response = await agent.get('/users?filter[nicknames.$noneOf][]=aa&fields=name,nicknames');
expect(response.body.count).toBe(2);
});
it('$noneOf for same element', async () => {
const response = await agent.get('/users?filter[nicknames.$noneOf]=aa,aaa&fields=name,nicknames');
expect(response.body.count).toBe(2);
});
it('$noneOf for more element', async () => {
const response = await agent.get('/users?filter[nicknames.$noneOf]=a,aa,aaa');
expect(response.body.count).toBe(2);
});
});
describe('$match', () => {
// TODO(bug)
it.skip('$match for no element', async () => {
const response = await agent.get('/users?filter={"nicknames.$match":[]}');
expect(response.body.count).toBe(2);
});
it('$match for different element', async () => {
const response = await agent.get('/users?filter[nicknames.$match]=a,aa');
expect(response.body.count).toBe(0);
});
it('$match for less element', async () => {
const response = await agent.get('/users?filter[nicknames.$match][]=aa&fields=name,nicknames');
expect(response.body.count).toBe(0);
});
it('$match for same element', async () => {
const response = await agent.get('/users?filter[nicknames.$match]=aa,aaa&fields=name,nicknames');
expect(response.body.count).toBe(1);
});
it('$match for more element', async () => {
const response = await agent.get('/users?filter[nicknames.$match]=a,aa,aaa');
expect(response.body.count).toBe(0);
});
});
});
});
describe('page', () => {
it('default page and size(20) should be ok', async () => {
const response = await agent.get('/posts?fields=title');
expect(response.body).toEqual({
count: 25,
page: 1,
per_page: 20,
rows: Array(20).fill(null).map((_, index) => ({ title: `title${index}` })),
});
});
it('page 1 by size(1) should be ok', async () => {
const response = await agent.get('/posts?fields=title&page=1&perPage=1');
expect(response.body).toEqual({
count: 25,
page: 1,
per_page: 1,
rows: [{ title: 'title0' }],
});
});
it('page 2 by size(1) should be ok', async () => {
const response = await agent.get('/posts?fields=title&page=2&per_page=1');
expect(response.body).toEqual({
count: 25,
page: 2,
per_page: 1,
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('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: [],
});
});
afterEach(async () => {
return api.destroy();
});
describe('fields', () => {
it('custom field', async () => {
const response = await agent.get('/posts?fields=title&filter[customTitle]=title0');
it('fields', async () => {
const response = await api.resource('posts').list({
fields: ['title'],
filter: {
title: 't1',
},
});
expect(response.body).toEqual({
count: 1,
rows: [{ title: 't1' }],
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'
}
}
]
it('fields#appends', async () => {
const response = await api.resource('posts').list({
fields: {
appends: ['comments'],
},
filter: {
title: 't1',
},
});
});
// TODO(question): 当 fields 只填写了关联字段时,当前表的其他字段是否需要输出?
it.skip('only belongs to', async () => {
const response = await agent.get('/posts?fields=user&filter[title]=title0');
expect(response.body).toEqual({
expect(response.body).toMatchObject({
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, nicknames: ['aa', 'aaa'], name: 'a', ...timestampsStrings }
}]);
});
});
});
describe('hasMany', () => {
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 = await Post.create({ user_id: users[0].id });
await post.updateAssociations({
comments: Array(6).fill(null).map((_, index) => ({
content: `content${index}`,
status: index % 2 ? 'published' : 'draft',
user_id: users[index % users.length].id
}))
});
});
it('get comments of a post', async () => {
const Post = db.getModel('posts');
const post = await Post.findByPk(1);
const response = await agent
.get(`/posts/${post.id}/comments?page=2&perPage=2&sort=content&fields=content&filter[published]=1`);
expect(response.body).toEqual({
rows: [{ content: 'content5' }],
count: 3,
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=-comments.content&page=1&perPage=2`);
expect(response.body).toEqual({
count: 1,
page: 1,
per_page: 2,
rows: [
{
title: 't1',
comments: [
{ content: 'content4' },
{ content: 'content2' }
{
content: 'c1',
},
{
content: 'c2',
},
{
content: 'c3',
},
],
user: { name: 'a' }
},
],
page: 1,
per_page: 20,
});
});
});
describe('filter', () => {
it('and', async () => {
const response = await api.resource('posts').list({
filter: {
and: [
{ title: 't1' },
{ status: 'draft' },
],
},
});
expect(response.body).toMatchObject({
count: 1,
rows: [
{
title: 't1',
},
],
page: 1,
per_page: 20
});
});
it('or', async () => {
const response = await api.resource('posts').list({
filter: {
or: [
{ title: 't1' },
{ title: 't2' },
],
},
});
expect(response.body).toMatchObject({
count: 2,
rows: [
{
title: 't1',
},
{
title: 't2',
}
]
],
page: 1,
per_page: 20
});
});
it('count field in hasMany', async () => {
try {
const response = await agent
.get(`/users/1?fields=name,posts_count`);
console.log(response.body);
} catch (err) {
console.error(err);
}
});
it('count field in hasMany', async () => {
try {
const response = await agent
.get(`/users/1/posts?fields=title,comments_count`);
console.log(response.body);
} catch (err) {
console.error(err);
}
});
});
describe('belongsToMany', () => {
beforeEach(async () => {
const Tag = db.getModel('tags');
const tags = await Tag.bulkCreate([
{ name: 'tag1', status: 'published' },
{ name: 'tag2', status: 'draft' },
{ name: 'tag3', status: 'published' },
{ name: 'tag4', status: 'draft' },
{ name: 'tag5', status: 'published' },
{ name: 'tag6', status: 'draft' },
{ name: 'tag7', status: 'published' },
{ name: 'tag8', status: 'published' },
{ name: 'tag9', status: 'draft' },
{ name: 'tag10', status: 'published' },
]);
const Post = db.getModel('posts');
const [post1, post2] = await Post.bulkCreate([{}, {}]);
await post1.updateAssociations({
tags: [1, 2, 3, 4, 5, 6, 7]
});
await post2.updateAssociations({
tags: [2, 5, 8]
});
const User = db.getModel('users');
const user = await User.create();
await user.updateAssociations({
posts: [post1]
});
});
it('list1', async () => {
const Post = db.getModel('posts');
const post = await Post.findByPk(1);
const response = await agent
.get(`/posts/${post.id}/tags?page=2&perPage=2&sort=-name&fields=name&filter[status]=published`);
expect(response.body).toEqual({
rows: [{ name: 'tag3' }, { name: 'tag1' }],
count: 4,
page: 2,
per_page: 2
});
});
// TODO(bug): SQL 报错
it.skip('list2', async () => {
const response = await agent
.get(`/users/1/posts?fields=tags`);
console.log(response.body);
});
});
});

View File

@ -1,65 +0,0 @@
import actions from '..';
import { Context } from '../actions';
import { dataWrapping } from '../middlewares';
import { initDatabase, agent, resourcer } from './index';
describe('list', () => {
let db;
beforeAll(async () => {
resourcer.define({
name: 'articles',
middlewares: [
dataWrapping,
],
actions: actions.common,
});
db = await initDatabase();
db.table({
name: 'articles',
tableName: 'actions__articles',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'string',
name: 'status',
defaultValue: 'publish',
}
],
scopes: {
customTitle: (title, ctx: Context) => {
return {
where: {
title: title,
},
}
},
}
});
await db.sync({
force: true,
});
});
afterAll(() => db.close());
it('create', async () => {
const response = await agent
.post('/articles')
.send({
title: 'title1',
});
expect(response.body.data.title).toBe('title1');
});
it('list', async () => {
const response = await agent.get('/articles?fields=title&page=1');
expect(response.body).toEqual({
data: [{ title: 'title1' }],
meta: { count: 1, page: 1, per_page: 20 }
});
});
});

View File

@ -1,76 +1,21 @@
import { initDatabase, agent } from './index';
import { mockServer, MockServer } from '@nocobase/test';
import { registerActions } from '..';
describe('remove', () => {
let db;
let api: MockServer;
beforeEach(async () => {
db = await initDatabase();
api = mockServer({
dataWrapping: false,
});
registerActions(api);
});
afterAll(() => db.close());
it('hasOne1', async () => {
const User = db.getModel('users');
const user = await User.create();
await user.updateAssociations({
profile: {
email: 'email1122',
}
});
const response = await agent
.post(`/users/${user.id}/profile:remove`);
const profile = await user.getProfile();
expect(profile).toBeNull();
afterEach(async () => {
return api.destroy();
});
it('hasMany1', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
comments: [
{ content: 'content111222' },
],
});
let [comment] = await post.getComments();
await agent
.post(`/posts/${post.id}/comments:remove/${comment.id}`);
const count = await post.countComments();
expect(count).toBe(0);
});
it('remove', async () => {
it('belongsTo1', async () => {
const Post = db.getModel('posts');
let post = await Post.create();
await post.updateAssociations({
user: { name: 'name121234' },
});
await agent.post(`/posts/${post.id}/user:remove`);
post = await Post.findOne({
where: {
id: post.id,
}
});
const user = await post.getUser();
expect(user).toBeNull();
});
it('belongsToMany', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
tags: [
{
name: 'tag112233',
posts_tags: {
test: 'test1',
}
},
],
});
const [tag] = await post.getTags();
await agent
.delete(`/posts/${post.id}/tags:remove/${tag.id}`);
const tags = await post.getTags();
expect(tags.length).toBe(0);
});
});

View File

@ -1,44 +1,21 @@
import { initDatabase, agent } from './index';
import { mockServer, MockServer } from '@nocobase/test';
import { registerActions } from '..';
describe('set', () => {
let db;
let api: MockServer;
beforeEach(async () => {
db = await initDatabase();
api = mockServer({
dataWrapping: false,
});
registerActions(api);
});
afterAll(() => db.close());
it('belongsTo1', async () => {
const Post = db.getModel('posts');
const User = db.getModel('users');
let post = await Post.create();
let user = await User.create();
await agent.post(`/posts/${post.id}/user:set/${user.id}`);
post = await Post.findOne({
where: {
id: post.id,
}
});
const postUser = await post.getUser();
expect(user.id).toBe(postUser.id);
afterEach(async () => {
return api.destroy();
});
// TODO: 关系暂不关注,先注释了
it.skip('belongsToMany1', async () => {
const [Post, Tag] = db.getModels(['posts', 'tags']);
let post = await Post.create();
let tag1 = await Tag.create({ name: 'tag1' });
let tag2 = await Tag.create({ name: 'tag2' });
await agent.post(`/posts/${post.id}/tags:set/${tag1.id}`);
// 单独跑 ok和上面的 it 一起跑就无法获取到
const tags = await post.getTags();
console.log(post, tags);
expect(tag1.id).toBe(tags[0].id);
expect(await post.countTags()).toBe(1);
await agent.post(`/posts/${post.id}/tags:set/${tag2.id}`);
const [tag02] = await post.getTags();
expect(tag2.id).toBe(tag02.id);
expect(await post.countTags()).toBe(1);
it('set', async () => {
});
});

View File

@ -1,302 +1,513 @@
import { initDatabase, agent } from './index';
import { mockServer, MockServer } from '@nocobase/test';
import { registerActions } from '..';
describe('get', () => {
let db;
describe('sort', () => {
describe('same scope', () => {
let api: MockServer;
beforeEach(async () => {
db = await initDatabase();
const User = db.getModel('users');
const users = await User.bulkCreate(Array.from('abcdefg').map(name => ({ name })));
const Post = db.getModel('posts');
const posts = await Post.bulkCreate(Array(10).fill(null).map((_, i) => ({
title: `title_${i}`,
status: i % 2 ? 'publish' : 'draft',
user_id: users[i % users.length].id
})));
await posts.reduce((promise, post) => promise.then(() => post.updateAssociations({
comments: Array(post.sort % 5).fill(null).map((_, index) => ({
content: `content_${index}`,
status: index % 2 ? 'published' : 'draft',
user_id: users[index % users.length].id
}))
})), Promise.resolve());
api = mockServer({
dataWrapping: false,
});
registerActions(api);
api.database.table({
name: 'tests',
fields: [
{ type: 'string', name: 'title' },
{ type: 'sort', name: 'sort' },
{ type: 'sort', name: 'sort2' },
],
});
await api.database.sync();
const Test = api.database.getModel('tests');
for (let index = 1; index < 5; index++) {
await Test.create({ title: `t${index}` });
}
});
afterAll(() => db.close());
describe('sort value initialization', () => {
it('initialization by bulkCreate', async () => {
const Post = db.getModel('posts');
const posts = await Post.findAll({
order: [['id', 'ASC']]
});
expect(posts.map(({ id, sort, sort_in_status, sort_in_user }) => ({ id, sort, sort_in_status, sort_in_user }))).toEqual([
{ id: 1, sort: 1, sort_in_status: 1, sort_in_user: 1 },
{ id: 2, sort: 2, sort_in_status: 1, sort_in_user: 1 },
{ id: 3, sort: 3, sort_in_status: 2, sort_in_user: 1 },
{ id: 4, sort: 4, sort_in_status: 2, sort_in_user: 1 },
{ id: 5, sort: 5, sort_in_status: 3, sort_in_user: 1 },
{ id: 6, sort: 6, sort_in_status: 3, sort_in_user: 1 },
{ id: 7, sort: 7, sort_in_status: 4, sort_in_user: 1 },
{ id: 8, sort: 8, sort_in_status: 4, sort_in_user: 2 },
{ id: 9, sort: 9, sort_in_status: 5, sort_in_user: 2 },
{ id: 10, sort: 10, sort_in_status: 5, sort_in_user: 2 }
]);
afterEach(async () => {
return api.destroy();
});
it('initialization by updateAssociations', async () => {
const Comment = db.getModel('comments');
const comments = await Comment.findAll({
order: [['id', 'ASC']]
it('targetId', async () => {
await api.resource('tests').sort({
sourceId: 1,
targetId: 3,
});
expect(comments.map(({ id, sort, sort_in_status, sort_in_post }) => ({ id, sort, sort_in_status, sort_in_post }))).toEqual([
{ id: 1, sort: 1, sort_in_status: 1, sort_in_post: 1 },
{ id: 2, sort: 2, sort_in_status: 2, sort_in_post: 1 },
{ id: 3, sort: 3, sort_in_status: 1, sort_in_post: 2 },
{ id: 4, sort: 4, sort_in_status: 3, sort_in_post: 1 },
{ id: 5, sort: 5, sort_in_status: 2, sort_in_post: 2 },
{ id: 6, sort: 6, sort_in_status: 4, sort_in_post: 3 },
{ id: 7, sort: 7, sort_in_status: 5, sort_in_post: 1 },
{ id: 8, sort: 8, sort_in_status: 3, sort_in_post: 2 },
{ id: 9, sort: 9, sort_in_status: 6, sort_in_post: 3 },
{ id: 10, sort: 10, sort_in_status: 4, sort_in_post: 4 },
{ id: 11, sort: 11, sort_in_status: 7, sort_in_post: 1 },
{ id: 12, sort: 12, sort_in_status: 8, sort_in_post: 1 },
{ id: 13, sort: 13, sort_in_status: 5, sort_in_post: 2 },
{ id: 14, sort: 14, sort_in_status: 9, sort_in_post: 1 },
{ id: 15, sort: 15, sort_in_status: 6, sort_in_post: 2 },
{ id: 16, sort: 16, sort_in_status: 10, sort_in_post: 3 },
{ id: 17, sort: 17, sort_in_status: 11, sort_in_post: 1 },
{ id: 18, sort: 18, sort_in_status: 7, sort_in_post: 2 },
{ id: 19, sort: 19, sort_in_status: 12, sort_in_post: 3 },
{ id: 20, sort: 20, sort_in_status: 8, sort_in_post: 4 }
]);
const response = await api.resource('tests').list({
sort: ['sort'],
});
it('sort value of append item', async () => {
const Post = db.getModel('posts');
const post = await Post.create({ user_id: 1 });
expect(post.sort).toBe(11);
expect(post.sort_in_status).toBe(6);
expect(post.sort_in_user).toBe(3);
});
});
describe('sort in whole table', () => {
it('move id=1 to position at id=2', async () => {
await agent
.post('/posts:sort/1')
.send({
field: 'sort',
target: { id: 2 },
});
const Post = db.getModel('posts');
const posts = await Post.findAll({
attributes: ['id', 'sort'],
order: [['id', 'ASC']]
});
expect(posts.map(item => item.get())).toEqual([
{ id: 1, sort: 2 },
{ id: 2, sort: 1 },
{ id: 3, sort: 3 },
{ id: 4, sort: 4 },
{ id: 5, sort: 5 },
{ id: 6, sort: 6 },
{ id: 7, sort: 7 },
{ id: 8, sort: 8 },
{ id: 9, sort: 9 },
{ id: 10, sort: 10 }
]);
});
it('move id=2 to position at id=1', async () => {
await agent
.post('/posts:sort/2')
.send({
field: 'sort',
target: { id: 1 },
});
const Post = db.getModel('posts');
const posts = await Post.findAll({
attributes: ['id', 'sort'],
order: [['id', 'ASC']]
});
expect(posts.map(item => item.get())).toEqual([
{ id: 1, sort: 2 },
{ id: 2, sort: 1 },
{ id: 3, sort: 3 },
{ id: 4, sort: 4 },
{ id: 5, sort: 5 },
{ id: 6, sort: 6 },
{ id: 7, sort: 7 },
{ id: 8, sort: 8 },
{ id: 9, sort: 9 },
{ id: 10, sort: 10 }
]);
});
it('move id=1 to position at id=10', async () => {
await agent
.post('/posts:sort/1')
.send({
field: 'sort',
target: { id: 10 },
});
const Post = db.getModel('posts');
const posts = await Post.findAll({
attributes: ['id', 'sort'],
order: [['id', 'ASC']]
});
expect(posts.map(item => item.get())).toEqual([
{ id: 1, sort: 10 },
{ id: 2, sort: 1 },
{ id: 3, sort: 2 },
{ id: 4, sort: 3 },
{ id: 5, sort: 4 },
{ id: 6, sort: 5 },
{ id: 7, sort: 6 },
{ id: 8, sort: 7 },
{ id: 9, sort: 8 },
{ id: 10, sort: 9 }
]);
});
});
describe('sort in filtered scope', () => {
it('move id=2 to position at id=8 (same scope value)', async () => {
await agent
.post('/posts:sort/2')
.send({
field: 'sort_in_status',
target: { id: 8 },
});
const Post = db.getModel('posts');
const posts = await Post.findAll({
where: {
status: 'publish'
expect(response.body).toMatchObject({
rows: [
{
title: 't2',
sort: 1,
},
attributes: ['id', 'sort_in_status'],
order: [['id', 'ASC']]
});
expect(posts.map(item => item.get())).toEqual([
{ id: 2, sort_in_status: 4 },
{ id: 4, sort_in_status: 1 },
{ id: 6, sort_in_status: 2 },
{ id: 8, sort_in_status: 3 },
{ id: 10, sort_in_status: 5 }
]);
});
it('move id=1 to position at id=8 (different scope value)', async () => {
await agent
.post('/posts:sort/1')
.send({
field: 'sort_in_status',
target: { id: 8 },
});
const Post = db.getModel('posts');
const posts = await Post.findAll({
where: {
status: 'publish'
{
title: 't3',
sort: 2,
},
attributes: ['id', 'sort_in_status'],
order: [['id', 'ASC']]
});
expect(posts.map(item => item.get())).toEqual([
{ id: 1, sort_in_status: 4 },
{ id: 2, sort_in_status: 1 },
{ id: 4, sort_in_status: 2 },
{ id: 6, sort_in_status: 3 },
{ id: 8, sort_in_status: 5 },
{ id: 10, sort_in_status: 6 }
]);
});
it('move id=1 to new empty list of scope', async () => {
await agent
.post('/posts:sort/1')
.send({
field: 'sort_in_status',
target: { status: 'archived' },
});
const Post = db.getModel('posts');
const posts = await Post.findAll({
attributes: ['id', 'sort_in_status'],
order: [['id', 'ASC']]
});
expect(posts.map(item => item.get())).toEqual([
{ id: 1, sort_in_status: 1 },
{ id: 2, sort_in_status: 1 },
{ id: 3, sort_in_status: 2 },
{ id: 4, sort_in_status: 2 },
{ id: 5, sort_in_status: 3 },
{ id: 6, sort_in_status: 3 },
{ id: 7, sort_in_status: 4 },
{ id: 8, sort_in_status: 4 },
{ id: 9, sort_in_status: 5 },
{ id: 10, sort_in_status: 5 }
]);
});
it('move id=1 to scope without target primary key', async () => {
await agent
.post('/posts:sort/1')
.send({
field: 'sort_in_status',
target: { status: 'publish' },
});
const Post = db.getModel('posts');
const posts = await Post.findAll({
where: {
status: 'publish'
{
title: 't1',
sort: 3,
},
attributes: ['id', 'sort_in_status'],
order: [['id', 'ASC']]
});
expect(posts.map(item => item.get())).toEqual([
{ id: 1, sort_in_status: 6 },
{ id: 2, sort_in_status: 1 },
{ id: 4, sort_in_status: 2 },
{ id: 6, sort_in_status: 3 },
{ id: 8, sort_in_status: 4 },
{ id: 10, sort_in_status: 5 }
]);
{
title: 't4',
sort: 4,
}
],
});
});
describe('associations', () => {
describe('hasMany', () => {
it('move id=1 to position at id=3 (different scope value)', async () => {
await agent
.post('/users/1/posts:sort/1')
.send({
field: 'sort_in_user',
target: { id: 3 },
it('targetId', async () => {
await api.resource('tests').sort({
sourceId: 3,
targetId: 1,
});
const Post = db.getModel('posts');
const posts = await Post.findAll({
where: {
user_id: 3
const response = await api.resource('tests').list({
sort: ['sort'],
});
expect(response.body).toMatchObject({
rows: [
{
title: 't3',
sort: 1,
},
attributes: ['id', 'sort_in_user'],
order: [['id', 'ASC']]
{
title: 't1',
sort: 2,
},
{
title: 't2',
sort: 3,
},
{
title: 't4',
sort: 4,
}
],
});
});
expect(posts.map(item => item.get())).toEqual([
{ id: 1, sort_in_user: 1 },
{ id: 3, sort_in_user: 2 },
{ id: 10, sort_in_user: 3 },
]);
it('sortField', async () => {
await api.resource('tests').sort({
sortField: 'sort2',
sourceId: 1,
targetId: 3,
});
const response = await api.resource('tests').list({
sort: ['sort2'],
});
expect(response.body).toMatchObject({
rows: [
{
title: 't2',
sort2: 1,
},
{
title: 't3',
sort2: 2,
},
{
title: 't1',
sort2: 3,
},
{
title: 't4',
sort2: 4,
}
],
});
});
it('sticky', async () => {
await api.resource('tests').sort({
sourceId: 3,
sticky: true,
});
const response = await api.resource('tests').list({
sort: ['sort'],
});
expect(response.body).toMatchObject({
rows: [
{
title: 't3',
sort: 0,
},
{
title: 't1',
sort: 1,
},
{
title: 't2',
sort: 2,
},
{
title: 't4',
sort: 4,
}
],
});
});
});
describe('different scope', () => {
let api: MockServer;
beforeEach(async () => {
api = mockServer({
dataWrapping: false,
});
registerActions(api);
api.database.table({
name: 'tests',
fields: [
{ type: 'string', name: 'title' },
{ type: 'integer', name: 'state' },
{ type: 'sort', name: 'sort', scope: ['state'] },
],
});
await api.database.sync();
const Test = api.database.getModel('tests');
for (let index = 1; index < 5; index++) {
await Test.create({ title: `t1${index}`, state: 1 });
}
for (let index = 1; index < 5; index++) {
await Test.create({ title: `t2${index}`, state: 2 });
}
});
afterEach(async () => {
return api.destroy();
});
it('targetId/1->6', async () => {
await api.resource('tests').sort({
sourceId: 1,
targetId: 6,
});
let response = await api.resource('tests').list({
sort: ['sort'],
filter: { state: 1 },
});
expect(response.body).toMatchObject({
rows: [
{
title: 't12',
sort: 2,
},
{
title: 't13',
sort: 3,
},
{
title: 't14',
sort: 4,
},
],
});
response = await api.resource('tests').list({
sort: ['sort'],
filter: { state: 2 },
});
expect(response.body).toMatchObject({
rows: [
{
title: 't21',
sort: 1,
},
{
title: 't11',
sort: 2,
},
{
title: 't22',
sort: 3,
},
{
title: 't23',
sort: 4,
},
{
title: 't24',
sort: 5,
},
],
});
});
it('targetId/1->6 - method=insertAfter', async () => {
await api.resource('tests').sort({
sourceId: 1,
targetId: 6,
method: 'insertAfter',
});
let response = await api.resource('tests').list({
sort: ['sort'],
filter: { state: 1 },
});
expect(response.body).toMatchObject({
rows: [
{
title: 't12',
sort: 2,
},
{
title: 't13',
sort: 3,
},
{
title: 't14',
sort: 4,
},
],
});
response = await api.resource('tests').list({
sort: ['sort'],
filter: { state: 2 },
});
expect(response.body).toMatchObject({
rows: [
{
title: 't21',
sort: 1,
},
{
title: 't22',
sort: 2,
},
{
title: 't11',
sort: 3,
},
{
title: 't23',
sort: 4,
},
{
title: 't24',
sort: 5,
},
],
});
});
it('targetId/6->2', async () => {
await api.resource('tests').sort({
sourceId: 6,
targetId: 2,
});
let response = await api.resource('tests').list({
sort: ['sort'],
filter: { state: 1 },
});
expect(response.body).toMatchObject({
rows: [
{
title: 't11',
sort: 1,
},
{
title: 't22',
sort: 2,
},
{
title: 't12',
sort: 3,
},
{
title: 't13',
sort: 4,
},
{
title: 't14',
sort: 5,
},
],
});
response = await api.resource('tests').list({
sort: ['sort'],
filter: { state: 2 },
});
expect(response.body).toMatchObject({
rows: [
{
title: 't21',
sort: 1,
},
{
title: 't23',
sort: 3,
},
{
title: 't24',
sort: 4,
},
],
});
});
it('targetId/6->2 - method=insertAfter', async () => {
await api.resource('tests').sort({
sourceId: 6,
targetId: 2,
method: 'insertAfter',
});
let response = await api.resource('tests').list({
sort: ['sort'],
filter: { state: 1 },
});
expect(response.body).toMatchObject({
rows: [
{
title: 't11',
sort: 1,
},
{
title: 't12',
sort: 2,
},
{
title: 't22',
sort: 3,
},
{
title: 't13',
sort: 4,
},
{
title: 't14',
sort: 5,
},
],
});
response = await api.resource('tests').list({
sort: ['sort'],
filter: { state: 2 },
});
expect(response.body).toMatchObject({
rows: [
{
title: 't21',
sort: 1,
},
{
title: 't23',
sort: 3,
},
{
title: 't24',
sort: 4,
},
],
});
});
it('targetScope', async () => {
await api.resource('tests').sort({
sourceId: 1,
targetScope: {
state: 2,
},
});
let response = await api.resource('tests').list({
sort: ['sort'],
filter: { state: 1 },
});
expect(response.body).toMatchObject({
rows: [
{
title: 't12',
sort: 2,
},
{
title: 't13',
sort: 3,
},
{
title: 't14',
sort: 4,
},
],
});
response = await api.resource('tests').list({
sort: ['sort'],
filter: { state: 2 },
});
expect(response.body).toMatchObject({
rows: [
{
title: 't21',
sort: 1,
},
{
title: 't22',
sort: 2,
},
{
title: 't23',
sort: 3,
},
{
title: 't24',
sort: 4,
},
{
title: 't11',
sort: 5,
},
],
});
});
it('targetScope - method=prepend', async () => {
await api.resource('tests').sort({
sourceId: 1,
targetScope: {
state: 2,
},
method: 'prepend',
});
let response = await api.resource('tests').list({
sort: ['sort'],
filter: { state: 1 },
});
expect(response.body).toMatchObject({
rows: [
{
title: 't12',
},
{
title: 't13',
},
{
title: 't14',
},
],
});
response = await api.resource('tests').list({
sort: ['sort'],
filter: { state: 2 },
});
expect(response.body).toMatchObject({
rows: [
{
title: 't11',
},
{
title: 't21',
},
{
title: 't22',
},
{
title: 't23',
},
{
title: 't24',
},
],
});
});
});

View File

@ -1,45 +0,0 @@
import { TableOptions } from "@nocobase/database";
export default {
name: 'comments',
tableName: 'actions__comments',
fields: [
{
type: 'string',
name: 'content',
},
{
type: 'string',
name: 'status',
},
{
type: 'belongsTo',
name: 'post',
},
{
type: 'belongsTo',
name: 'user',
},
{
type: 'sort',
name: 'sort'
},
{
type: 'sort',
name: 'sort_in_status',
scope: ['status']
},
{
type: 'sort',
name: 'sort_in_post',
scope: ['post']
}
],
scopes: {
published: {
where: {
status: 'published'
}
}
}
} as TableOptions;

View File

@ -1,60 +0,0 @@
import { TableOptions } from "@nocobase/database";
export default {
name: 'posts',
tableName: 'actions__posts',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'string',
name: 'status',
defaultValue: 'publish',
},
{
type: 'date',
name: 'published_at'
},
{
type: 'belongsTo',
name: 'user',
},
{
type: 'hasMany',
name: 'comments',
},
{
type: 'belongsToMany',
name: 'tags',
},
{
type: 'sort',
name: 'sort'
},
{
type: 'sort',
name: 'sort_in_status',
scope: ['status']
},
{
type: 'sort',
name: 'sort_in_user',
scope: ['user']
},
{
type: 'json',
name: 'meta'
}
],
scopes: {
customTitle: (title, ctx) => {
return {
where: {
title: title,
},
}
}
}
} as TableOptions;

View File

@ -1,12 +0,0 @@
import { TableOptions } from "@nocobase/database";
export default {
name: 'posts_tags',
tableName: 'actions__posts_tags',
fields: [
{
type: 'string',
name: 'test',
},
],
} as TableOptions;

View File

@ -1,36 +0,0 @@
import { TableOptions } from "@nocobase/database";
export default {
name: 'profiles',
tableName: 'actions__profiles',
fields: [
{
type: 'belongsTo',
name: 'user'
},
{
type: 'string',
name: 'email',
},
{
type: 'string',
name: 'city',
dataSource: [
{ value: '1101', title: 'Beijing' },
{ value: '3710', title: 'Weihai' },
{ value: '5301', title: 'Kunming' }
]
},
{
type: 'jsonb',
name: 'interest',
defaultValue: [],
multiple: true,
dataSource: [
{ value: 1, title: 'running' },
{ value: 2, title: 'climbing' },
{ value: 3, title: 'fishing' },
]
}
],
} as TableOptions;

View File

@ -1,27 +0,0 @@
import { TableOptions } from "@nocobase/database";
export default {
name: 'tags',
tableName: 'actions__tags',
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'string',
name: 'status',
},
{
type: 'belongsToMany',
name: 'posts',
},
],
scopes: {
published: {
where: {
status: 'published'
}
}
}
} as TableOptions;

View File

@ -1,25 +0,0 @@
import { TableOptions } from "@nocobase/database";
export default {
name: 'users',
tableName: 'actions__users',
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'jsonb',
name: 'nicknames',
defaultValue: []
},
{
type: 'hasOne',
name: 'profile',
},
{
type: 'hasMany',
name: 'posts'
}
],
} as TableOptions;

View File

@ -0,0 +1,21 @@
import { mockServer, MockServer } from '@nocobase/test';
import { registerActions } from '..';
describe('toggle', () => {
let api: MockServer;
beforeEach(async () => {
api = mockServer({
dataWrapping: false,
});
registerActions(api);
});
afterEach(async () => {
return api.destroy();
});
it('toggle', async () => {
});
});

View File

@ -1,192 +1,66 @@
import { initDatabase, agent } from './index';
import { mockServer, MockServer } from '@nocobase/test';
import { registerActions } from '..';
describe('update', () => {
let db;
let api: MockServer;
beforeEach(async () => {
db = await initDatabase();
api = mockServer({
dataWrapping: false,
});
afterAll(() => db.close());
describe('common', () => {
it('basic', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
const response = await agent
.put(`/posts/${post.id}`).send({
title: 'title11112222'
});
expect(response.body.title).toBe('title11112222');
});
it('update json field by replacing', async () => {
const Post = db.getModel('posts');
const post = await Post.create({ meta: { a: 1, b: 'c', c: { d: false } } });
const updated = await agent
.put(`/posts/${post.id}`).send({
meta: {}
});
expect(updated.body.meta).toEqual({});
});
it.skip('update json field by path based update', async () => {
const Post = db.getModel('posts');
const post = await Post.create({ meta: { a: 1, b: 'c', c: { d: false } } });
const updated = await agent
.put(`/posts/${post.id}?options[json]=merge`).send({
meta: {
b: 'b',
c: { d: true }
}
});
// console.log(updated.body);
});
// TODO(question): json 字段的覆盖/合并策略
it.skip('update with fields overwrite default values', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
const response = await agent
.put(`/posts:update1/${post.id}`).send({
meta: { a: 1 },
});
expect(response.body.meta).toEqual({ a: 1 });
const result = await agent
.get(`/posts/${post.id}`);
expect(result.body.meta).toEqual({ a: 1 });
});
// TODO(bug): action 的默认值处理时机不对
it.skip('update with different fields to default values', async () => {
const Post = db.getModel('posts');
const post = await Post.create({
meta: { location: 'Beijing' }
});
const response = await agent
.put(`/posts:update1/${post.id}`).send({
meta: { a: 1 },
});
expect(response.body.meta).toEqual({ location: 'Beijing', a: 1 });
});
it('update with options.fields.expect in action', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
const response = await agent
.put(`/posts:update1/${post.id}`).send({
title: 'title11112222',
});
expect(response.body.title).toBe(null);
expect(response.body.meta).toEqual({
location: 'Kunming'
});
const result = await agent
.get(`/posts/${post.id}`);
expect(result.body.title).toBe(null);
expect(result.body.meta).toEqual({
location: 'Kunming'
});
});
it('update with options.fields.only in action', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
const response = await agent
.put(`/posts:update2/${post.id}`).send({
title: 'title11112222',
meta: { a: 1 }
});
expect(response.body.title).toBe('title11112222');
expect(response.body.meta).toBe(null);
const result = await agent
.get(`/posts/${post.id}`);
expect(result.body.title).toBe('title11112222');
expect(result.body.meta).toBe(null);
});
});
it('hasOne', async () => {
const User = db.getModel('users');
const user = await User.create();
await user.updateAssociations({
profile: { email: 'email1122' }
});
const response = await agent
.put(`/users/${user.id}/profile`).send({
email: 'email1111',
});
expect(response.body.email).toEqual('email1111');
});
it('hasOne without exist target', async () => {
const User = db.getModel('users');
const user = await User.create();
const response = await agent
.put(`/users/${user.id}/profile`).send({
email: 'email1122',
});
expect(response.body).toEqual({});
});
it('hasMany1', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
comments: [
{ content: 'content111222' },
registerActions(api);
api.database.table({
name: 'posts',
fields: [
{ type: 'string', name: 'title' },
{ type: 'hasMany', name: 'comments' },
],
});
const [comment] = await post.getComments();
const response = await agent
.put(`/posts/${post.id}/comments/${comment.id}`).send({ content: 'content111222333' });
expect(response.body.post_id).toBe(post.id);
expect(response.body.content).toBe('content111222333');
});
it('belongsTo1', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
user: { name: 'name121234' },
});
const response = await agent
.post(`/posts/${post.id}/user:update`).send({ name: 'name1212345' });
expect(response.body.name).toEqual('name1212345');
});
it('belongsToMany', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
tags: [
{ name: 'tag112233' },
api.database.table({
name: 'comments',
fields: [
{ type: 'string', name: 'content' },
],
});
const [tag] = await post.getTags();
let response = await agent
.post(`/posts/${post.id}/tags:update/${tag.id}`).send({
name: 'tag11223344',
posts_tags: {
test: 'test1',
await api.database.sync();
});
afterEach(async () => {
return api.destroy();
});
it('update', async () => {
const Post = api.database.getModel('posts');
const post = await Post.create();
await api.resource('posts').update({
resourceKey: post.id,
values: {
title: 't1',
},
});
const [tag1] = await post.getTags();
expect(tag1.posts_tags.test).toBe('test1');
expect(response.body.name).toBe('tag11223344');
response = await agent
.post(`/posts/${post.id}/tags:update/${tag.id}`).send({
posts_tags: {
test: 'test112233',
const post2 = await Post.findByPk(post.id);
expect(post2.toJSON()).toMatchObject({
title: 't1',
});
});
it('update associations', async () => {
const [Post, Comment] = api.database.getModels(['posts', 'comments']);
const post = await Post.create();
const comment = await Comment.create();
await post.updateAssociations({
comments: [comment]
});
await api.resource('posts.comments').update({
resourceKey: comment.id,
associatedKey: post.id,
values: {
content: 'c2',
},
});
const [tag2] = await post.getTags();
expect(tag2.posts_tags.test).toBe('test112233');
const comment2 = await Comment.findByPk(comment.id);
expect(comment2.toJSON()).toMatchObject({
content: 'c2',
});
});
});

View File

@ -1,4 +1,3 @@
import { initDatabase, agent } from './index';
import { filterByFields } from '../utils';
describe('utils', () => {

View File

@ -0,0 +1,48 @@
import { Context, Next } from '..';
import {
Model,
Relation,
} from '@nocobase/database';
/**
*
*
* BlongsToMany
*
* @param ctx
* @param next
*/
export async function add(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
} = ctx.action.params as {
associated: Model,
associatedName: string,
resourceField: Relation,
values: any,
};
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const { add: addAccessor } = resourceField.getAccessors();
const { resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const TargetModel = ctx.db.getModel(resourceField.getTarget());
// const options = TargetModel.parseApiJson({
// fields,
// });
const model = await TargetModel.findOne({
// ...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
// @ts-ignore
context: ctx,
});
ctx.body = await associated[addAccessor](model);
await next();
}
export default add;

View File

@ -1,211 +0,0 @@
import { Context, Next } from '.';
import { list, get, create, update, destroy } from './common';
import {
Model,
Relation,
HASONE,
BELONGSTO,
BELONGSTOMANY,
HASMANY,
} from '@nocobase/database';
/**
*
*
* BlongsTo
* BlongsToMany
*
* @param ctx
* @param next
*/
export async function set(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
} = ctx.action.params as {
associated: Model,
associatedName: string,
resourceField: Relation,
values: any,
};
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const { set: setAccessor } = resourceField.getAccessors();
const { resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const TargetModel = ctx.db.getModel(resourceField.getTarget());
// const options = TargetModel.parseApiJson({
// fields,
// });
const model = await TargetModel.findOne({
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
// @ts-ignore
context: ctx,
});
ctx.body = await associated[setAccessor](model);
await next();
}
/**
*
*
* BlongsToMany
*
* @param ctx
* @param next
*/
export async function add(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
} = ctx.action.params as {
associated: Model,
associatedName: string,
resourceField: Relation,
values: any,
};
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const { add: addAccessor } = resourceField.getAccessors();
const { resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const TargetModel = ctx.db.getModel(resourceField.getTarget());
// const options = TargetModel.parseApiJson({
// fields,
// });
const model = await TargetModel.findOne({
// ...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
// @ts-ignore
context: ctx,
});
ctx.body = await associated[addAccessor](model);
await next();
}
/**
*
*
* BlongsTo
* BlongsToMany
*
* @param ctx
* @param next
*/
export async function remove(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
} = ctx.action.params as {
associated: Model,
associatedName: string,
resourceField: Relation,
values: any,
};
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const { get: getAccessor, remove: removeAccessor, set: setAccessor } = resourceField.getAccessors();
const { resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const TargetModel = ctx.db.getModel(resourceField.getTarget());
const options = TargetModel.parseApiJson({
fields,
});
if (resourceField instanceof HASONE || resourceField instanceof BELONGSTO) {
ctx.body = await associated[setAccessor](null);
} else if (resourceField instanceof HASMANY || resourceField instanceof BELONGSTOMANY) {
const [model]: Model[] = await associated[getAccessor]({
...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
context: ctx,
});
await associated[removeAccessor](model);
ctx.body = { id: model.id };
}
await next();
}
export async function toggle(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
} = ctx.action.params as {
associated: Model,
associatedName: string,
resourceField: Relation,
values: any,
};
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const { get: getAccessor, remove: removeAccessor, set: setAccessor, add: addAccessor } = resourceField.getAccessors();
const { resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const TargetModel = ctx.db.getModel(resourceField.getTarget());
const options = TargetModel.parseApiJson({
fields,
});
if (resourceField instanceof HASONE || resourceField instanceof BELONGSTO) {
const m1 = await associated[getAccessor]();
if (m1 && m1[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute] == resourceKey) {
ctx.body = await associated[setAccessor](null);
} else {
const m2 = await TargetModel.findOne({
// ...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
// @ts-ignore
context: ctx,
});
ctx.body = await associated[setAccessor](m2);
}
} else if (resourceField instanceof HASMANY || resourceField instanceof BELONGSTOMANY) {
const [model]: Model[] = await associated[getAccessor]({
...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
context: ctx,
});
if (model) {
ctx.body = await associated[removeAccessor](model);
} else {
const m2 = await TargetModel.findOne({
// ...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
// @ts-ignore
context: ctx,
});
ctx.body = await associated[addAccessor](m2);
}
}
await next();
}
export default {
list, // hasMany、belongsToMany
get, // 所有关系都有
create, // hasMany
update, // hasOne, hasMany, blongsToMany 中间表的数据更新
destroy, // 所有情况
set, // belongsTo、blongsToMany
add, // blongsToMany
remove, // belongsTo、blongsToMany
toggle, // blongsToMany
}

View File

@ -1,662 +0,0 @@
import { Utils, Op, Sequelize } from 'sequelize';
import _ from 'lodash';
import { Context, Next } from '.';
import {
Model,
HASONE,
HASMANY,
BELONGSTO,
BELONGSTOMANY,
whereCompare
} from '@nocobase/database';
import { PageParameter } from '@nocobase/resourcer';
import { filterByFields } from '../utils';
async function hasManyGet(instances, options: any = {}) {
const where = {};
let Model = this.target;
Model = Model.database.getModel(Model.name);
let instance;
let values;
if (!Array.isArray(instances)) {
instance = instances;
instances = undefined;
}
options = { ...options };
if (this.scope) {
Object.assign(where, this.scope);
}
if (instances) {
values = instances.map(_instance => _instance.get(this.sourceKey, { raw: true }));
if (options.limit && instances.length > 1) {
options.groupedLimit = {
limit: options.limit,
on: this, // association
values
};
delete options.limit;
} else {
where[this.foreignKey] = {
[Op.in]: values
};
delete options.groupedLimit;
}
} else {
where[this.foreignKey] = instance.get(this.sourceKey, { raw: true });
}
options.where = options.where ?
{ [Op.and]: [where, options.where] } :
where;
if (Object.prototype.hasOwnProperty.call(options, 'scope')) {
if (!options.scope) {
Model = Model.unscoped();
} else {
Model = Model.scope(options.scope);
}
}
if (Object.prototype.hasOwnProperty.call(options, 'schema')) {
Model = Model.schema(options.schema, options.schemaDelimiter);
}
const results = await Model.findAndCountAll(options);
if (instance) return results;
const result = {};
for (const _instance of instances) {
result[_instance.get(this.sourceKey, { raw: true })] = [];
}
for (const _instance of results) {
result[_instance.get(this.foreignKey, { raw: true })].push(_instance);
}
return result;
}
async function belongsToManyGet(instance, options) {
options = Utils.cloneDeep(options) || {};
const through = this.through;
let scopeWhere;
let throughWhere;
if (this.scope) {
scopeWhere = { ...this.scope };
}
options.where = {
[Op.and]: [
scopeWhere,
options.where
]
};
if (Object(through.model) === through.model) {
throughWhere = {};
throughWhere[this.foreignKey] = instance.get(this.sourceKey);
if (through.scope) {
Object.assign(throughWhere, through.scope);
}
//If a user pass a where on the options through options, make an "and" with the current throughWhere
if (options.through && options.through.where) {
throughWhere = {
[Op.and]: [throughWhere, options.through.where]
};
}
options.include = options.include || [];
options.include.push({
association: this.oneFromTarget,
attributes: options.joinTableAttributes,
required: true,
paranoid: _.get(options.through, 'paranoid', true),
where: throughWhere
});
}
let model = this.target;
if (Object.prototype.hasOwnProperty.call(options, 'scope')) {
if (!options.scope) {
model = model.unscoped();
} else {
model = model.scope(options.scope);
}
}
if (Object.prototype.hasOwnProperty.call(options, 'schema')) {
model = model.schema(options.schema, options.schemaDelimiter);
}
return model.findAndCountAll(options);
}
/**
*
*
* - Signle
* - HasMany
* - BelongsToMany
*
* HasOne belongsTo list
*
* @param ctx
* @param next
*/
export async function list(ctx: Context, next: Next) {
const {
page = PageParameter.DEFAULT_PAGE,
perPage = PageParameter.DEFAULT_PER_PAGE,
sort = [],
fields = [],
filter = {},
associated,
associatedName,
resourceName,
resourceField,
} = ctx.action.params;
let data = {};
let options: any = {};
let Model;
if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName);
Model = ctx.db.getModel(resourceField.options.target);
options = Model.parseApiJson({
sort,
page,
perPage,
filter,
fields,
context: ctx,
});
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
// const getAccessor = resourceField.getAccessors().get;
// const countAccessor = resourceField.getAccessors().count;
options.scope = options.scopes || [];
const association = AssociatedModel.associations[resourceField.options.name];
if (resourceField instanceof BELONGSTOMANY) {
data = await belongsToManyGet.call(association, associated, {
joinTableAttributes: [],
...options,
context: ctx,
});
}
if (resourceField instanceof HASMANY) {
data = await hasManyGet.call(association, associated, {
joinTableAttributes: [],
...options,
context: ctx,
});
}
} else {
Model = ctx.db.getModel(resourceName);
options = Model.parseApiJson({
sort,
page,
perPage,
filter,
fields,
context: ctx,
});
data = await Model.scope(options.scopes || []).findAndCountAll({
...options,
// @ts-ignore hooks 里添加 context
context: ctx,
});
}
if (options.limit || typeof options.offset !== 'undefined') {
// Math.round 避免精度问题
data['page'] = Math.round((options.offset || 0) / options.limit + 1);
data[Utils.underscoredIf('perPage', Model.options.underscored)] = options.limit;
}
ctx.body = data;
await next();
}
/**
*
*
* Signle
* HasMany
*
* resource action HasOneBelongsToBelongsToMany
* 使 model.updateAssociations
*
* TODO
*
* @param ctx
* @param next
*/
export async function create(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
resourceName,
values: data,
fields
} = ctx.action.params;
const values = filterByFields(data, fields);
const transaction = await ctx.db.sequelize.transaction();
const options = { transaction, context: ctx };
let model: Model;
try {
if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const { create } = resourceField.getAccessors();
model = await associated[create](values, options);
} else {
const ResourceModel = ctx.db.getModel(resourceName);
model = await ResourceModel.create(values, options);
}
await model.updateAssociations(values, options);
await transaction.commit();
ctx.body = model;
await next();
} catch (error) {
await transaction.rollback();
throw error;
}
}
/**
*
*
* @param ctx
* @param next
*/
export async function get(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
resourceName,
resourceKey,
resourceKeyAttribute,
fields = []
} = ctx.action.params;
if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const getAccessor = resourceField.getAccessors().get;
const TargetModel = ctx.db.getModel(resourceField.getTarget());
const options = TargetModel.parseApiJson({
fields,
});
if (resourceField instanceof HASONE || resourceField instanceof BELONGSTO) {
let model: Model = await associated[getAccessor]({ context: ctx });
if (model) {
model = await TargetModel.findOne({
...options,
context: ctx,
where: {
[TargetModel.primaryKeyAttribute]: model[TargetModel.primaryKeyAttribute],
},
});
}
ctx.body = model;
} else if (resourceField instanceof HASMANY || resourceField instanceof BELONGSTOMANY) {
const [model]: Model[] = await associated[getAccessor]({
...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
context: ctx,
});
ctx.body = model;
}
} else {
const Model = ctx.db.getModel(resourceName);
const options = Model.parseApiJson({
fields,
});
const data = await Model.findOne({
...options,
where: {
[resourceKeyAttribute || Model.primaryKeyAttribute]: resourceKey,
},
// @ts-ignore hooks 里添加 context
context: ctx,
});
ctx.body = data;
}
await next();
}
/**
*
*
* TODO
*
* @param ctx
* @param next
*/
export async function update(ctx: Context, next: Next) {
const {
associated,
associatedName,
resourceField,
resourceName,
resourceKey,
// TODO(question): 这个属性从哪设置的?
resourceKeyAttribute,
fields,
values: data
} = ctx.action.params;
const values = filterByFields(data, fields);
const transaction = await ctx.db.sequelize.transaction();
const options = { transaction, context: ctx };
if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
await transaction.rollback();
throw new Error(`${associatedName} associated model invalid`);
}
const { get: getAccessor } = resourceField.getAccessors();
if (resourceField instanceof HASONE || resourceField instanceof BELONGSTO) {
let model: Model = await associated[getAccessor](options);
if (model) {
// @ts-ignore
await model.update(values, options);
await model.updateAssociations(values, options);
ctx.body = model;
}
} else if (resourceField instanceof HASMANY || resourceField instanceof BELONGSTOMANY) {
const TargetModel = ctx.db.getModel(resourceField.getTarget());
const [model]: Model[] = await associated[getAccessor]({
...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
}
});
if (resourceField instanceof BELONGSTOMANY) {
const throughName = resourceField.getThroughName();
if (typeof values[throughName] === 'object') {
const ThroughModel = resourceField.getThroughModel();
const throughValues = values[throughName];
const { foreignKey, sourceKey, otherKey } = resourceField.options;
const through = await ThroughModel.findOne({
where: {
[foreignKey]: associated[sourceKey],
[otherKey]: resourceKey,
},
transaction
});
// TODO: 中间表的 Model 有问题,关联数据更新有 BUG
// await through.updateAssociations(throughValues, options);
await through.update(throughValues, options);
delete values[throughName];
}
}
if (!_.isEmpty(values)) {
// @ts-ignore
await model.update(values, options);
await model.updateAssociations(values, options);
}
ctx.body = model;
}
} else {
const Model = ctx.db.getModel(resourceName);
const model = await Model.findOne({
...options,
where: {
[resourceKeyAttribute || Model.primaryKeyAttribute]: resourceKey,
}
});
// @ts-ignore
await model.update(values, options);
// @ts-ignore
await model.updateAssociations(values, options);
ctx.body = model;
}
await transaction.commit();
await next();
}
/**
*
*
* Single
* HasOne
* HasMany
*
* TODO onUpdate/onDelete
*
* @param ctx
* @param next
*/
export async function destroy(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
resourceName,
resourceKey,
resourceKeyAttribute,
filter
} = ctx.action.params;
const transaction = await ctx.db.sequelize.transaction();
const commonOptions = { transaction, context: ctx };
let count;
if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
await transaction.rollback();
throw new Error(`${associatedName} associated model invalid`);
}
const { get: getAccessor, remove: removeAccessor, set: setAccessor } = resourceField.getAccessors();
const TargetModel = ctx.db.getModel(resourceField.getTarget());
const { where } = TargetModel.parseApiJson({ filter, context: ctx });
if (resourceField instanceof HASONE || resourceField instanceof BELONGSTO) {
const model: Model = await associated[getAccessor](commonOptions);
// TODO不能程序上解除关系直接通过 onDelete 触发,或者通过 afterDestroy 处理
// await associated[setAccessor](null, commonOptions);
// @ts-ignore
count = await model.destroy(commonOptions);
} else if (resourceField instanceof HASMANY || resourceField instanceof BELONGSTOMANY) {
const primaryKey = resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute;
const models: Model[] = await associated[getAccessor]({
where: resourceKey ? { [primaryKey]: resourceKey } : where,
...commonOptions
});
// TODO不能程序上解除关系直接通过 onDelete 触发,或者通过 afterDestroy 处理
// await associated[removeAccessor](models, commonOptions);
// @ts-ignore
count = await TargetModel.destroy({
where: { [primaryKey]: { [Op.in]: models.map(item => item[primaryKey]) } },
...commonOptions,
individualHooks: true,
});
}
} else {
const Model = ctx.db.getModel(resourceName);
const { where } = Model.parseApiJson({ filter, context: ctx });
const primaryKey = resourceKeyAttribute || Model.primaryKeyAttribute;
count = await Model.destroy({
where: resourceKey ? { [primaryKey]: resourceKey } : where,
// @ts-ignore hooks 里添加 context
...commonOptions,
individualHooks: true,
});
}
ctx.body = { count };
await transaction.commit();
await next();
}
/**
*
*
*
*
* TODO
*
* @param ctx
* @param next
*/
export async function sort(ctx: Context, next: Next) {
const {
resourceName,
resourceKey,
resourceField,
associatedName,
associatedKey,
associated,
values
} = ctx.action.params;
if (associated && resourceField) {
if (resourceField instanceof HASONE || resourceField instanceof BELONGSTO) {
throw new Error(`the association (${resourceName} belongs to ${associatedName}) cannot be sorted`);
}
// TODO(feature)
if (resourceField instanceof BELONGSTOMANY) {
throw new Error('sorting for belongs to many association has not been implemented');
}
}
const Model = ctx.db.getModel(resourceName);
const table = ctx.db.getTable(resourceName);
const { sticky, field, target, insertAfter } = values;
if (!values.field || typeof target === 'undefined') {
return next();
}
const sortField = table.getField(field);
if (!sortField) {
return next();
}
const { primaryKeyAttribute } = Model;
const { name: sortAttr, scope = [] } = sortField.options;
const transaction = await ctx.db.sequelize.transaction();
const where = {};
if (associated && resourceField instanceof HASMANY) {
where[resourceField.options.foreignKey] = associatedKey;
}
// 找到操作对象
const source = await Model.findOne({
where: {
...where,
[primaryKeyAttribute]: resourceKey
},
transaction
});
if (!source) {
await transaction.rollback();
throw new Error(`resource(${resourceKey}) does not exist`);
}
const sourceScopeWhere = source.getValuesByFieldNames(scope);
let targetScopeWhere: any;
let targetObject;
const { [primaryKeyAttribute]: targetId } = target;
if (targetId) {
targetObject = await Model.findByPk(targetId, { transaction });
if (!targetObject) {
await transaction.rollback();
throw new Error(`resource(${targetId}) does not exist`);
}
targetScopeWhere = targetObject.getValuesByFieldNames(scope);
} else {
targetScopeWhere = { ...sourceScopeWhere, ...target };
}
const sameScope = whereCompare(sourceScopeWhere, targetScopeWhere);
const updates = { ...targetScopeWhere };
if (targetObject) {
let increment: number;
const updateWhere = { ...targetScopeWhere };
if (sameScope) {
const direction = source[sortAttr] < targetObject[sortAttr] ? {
sourceOp: Op.gt,
targetOp: insertAfter ? Op.lt : Op.lte,
increment: -1
} : {
sourceOp: Op.lt,
targetOp: insertAfter ? Op.gt : Op.gte,
increment: 1
};
increment = direction.increment;
Object.assign(updateWhere, {
[sortAttr]: {
[direction.sourceOp]: source[sortAttr],
[direction.targetOp]: targetObject[sortAttr]
}
});
} else {
increment = 1;
Object.assign(updateWhere, {
[sortAttr]: {
[Op.gte]: targetObject[sortAttr]
}
});
}
console.log({ insertAfter, updateWhere })
await Model.increment(sortAttr, {
by: increment,
where: updateWhere,
transaction
});
Object.assign(updates, {
[sortAttr]: insertAfter ? targetObject[sortAttr] + 1 : targetObject[sortAttr]
});
} else {
Object.assign(updates, {
[sortAttr]: await sortField.getNextValue({
next: sticky ? 'min' : 'max',
where: targetScopeWhere,
transaction
})
});
}
await source.update(updates, { transaction });
await transaction.commit();
ctx.body = source;
await next();
}
export default {
list, // single、hasMany、belongsToMany
create, // signle、hasMany
get, // all
update, // single、
destroy,
sort
};

View File

@ -0,0 +1,55 @@
import _ from 'lodash';
import { Context, Next } from '..';
import { Model } from '@nocobase/database';
import { filterByFields } from '../utils';
/**
*
*
* Signle
* HasMany
*
* resource action HasOneBelongsToBelongsToMany
* 使 model.updateAssociations
*
* TODO
*
* @param ctx
* @param next
*/
export async function create(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
resourceName,
values: data,
fields,
} = ctx.action.params;
const values = filterByFields(data, fields);
const transaction = await ctx.db.sequelize.transaction();
const options = { transaction, context: ctx };
let model: Model;
try {
if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const { create } = resourceField.getAccessors();
model = await associated[create](values, options);
} else {
const ResourceModel = ctx.db.getModel(resourceName);
model = await ResourceModel.create(values, options);
}
await model.updateAssociations(values, options);
await transaction.commit();
ctx.body = model;
await next();
} catch (error) {
await transaction.rollback();
throw error;
}
}
export default create;

View File

@ -0,0 +1,95 @@
import _ from 'lodash';
import { Op } from 'sequelize';
import {
Model,
HASONE,
HASMANY,
BELONGSTO,
BELONGSTOMANY,
} from '@nocobase/database';
import { Context, Next } from '..';
/**
*
*
* Single
* HasOne
* HasMany
*
* TODO onUpdate/onDelete
*
* @param ctx
* @param next
*/
export async function destroy(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
resourceName,
resourceKey,
resourceKeyAttribute,
filter,
} = ctx.action.params;
const transaction = await ctx.db.sequelize.transaction();
const commonOptions = { transaction, context: ctx };
let count;
if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
await transaction.rollback();
throw new Error(`${associatedName} associated model invalid`);
}
const {
get: getAccessor,
remove: removeAccessor,
set: setAccessor,
} = resourceField.getAccessors();
const TargetModel = ctx.db.getModel(resourceField.getTarget());
const { where } = TargetModel.parseApiJson({ filter, context: ctx });
if (resourceField instanceof HASONE || resourceField instanceof BELONGSTO) {
const model: Model = await associated[getAccessor](commonOptions);
// TODO不能程序上解除关系直接通过 onDelete 触发,或者通过 afterDestroy 处理
// await associated[setAccessor](null, commonOptions);
// @ts-ignore
count = await model.destroy(commonOptions);
} else if (
resourceField instanceof HASMANY ||
resourceField instanceof BELONGSTOMANY
) {
const primaryKey =
resourceKeyAttribute ||
resourceField.options.targetKey ||
TargetModel.primaryKeyAttribute;
const models: Model[] = await associated[getAccessor]({
where: resourceKey ? { [primaryKey]: resourceKey } : where,
...commonOptions,
});
// TODO不能程序上解除关系直接通过 onDelete 触发,或者通过 afterDestroy 处理
// await associated[removeAccessor](models, commonOptions);
// @ts-ignore
count = await TargetModel.destroy({
where: {
[primaryKey]: { [Op.in]: models.map((item) => item[primaryKey]) },
},
...commonOptions,
individualHooks: true,
});
}
} else {
const Model = ctx.db.getModel(resourceName);
const { where } = Model.parseApiJson({ filter, context: ctx });
const primaryKey = resourceKeyAttribute || Model.primaryKeyAttribute;
count = await Model.destroy({
where: resourceKey ? { [primaryKey]: resourceKey } : where,
// @ts-ignore hooks 里添加 context
...commonOptions,
individualHooks: true,
});
}
ctx.body = { count };
await transaction.commit();
await next();
}
export default destroy;

View File

@ -0,0 +1,77 @@
import _ from 'lodash';
import { Context, Next } from '..';
import {
Model,
HASONE,
HASMANY,
BELONGSTO,
BELONGSTOMANY,
} from '@nocobase/database';
/**
*
*
* @param ctx
* @param next
*/
export async function get(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
resourceName,
resourceKey,
resourceKeyAttribute,
fields = []
} = ctx.action.params;
if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const getAccessor = resourceField.getAccessors().get;
const TargetModel = ctx.db.getModel(resourceField.getTarget());
const options = TargetModel.parseApiJson({
fields,
});
if (resourceField instanceof HASONE || resourceField instanceof BELONGSTO) {
let model: Model = await associated[getAccessor]({ context: ctx });
if (model) {
model = await TargetModel.findOne({
...options,
context: ctx,
where: {
[TargetModel.primaryKeyAttribute]: model[TargetModel.primaryKeyAttribute],
},
});
}
ctx.body = model;
} else if (resourceField instanceof HASMANY || resourceField instanceof BELONGSTOMANY) {
const [model]: Model[] = await associated[getAccessor]({
...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
context: ctx,
});
ctx.body = model;
}
} else {
const Model = ctx.db.getModel(resourceName);
const options = Model.parseApiJson({
fields,
});
const data = await Model.findOne({
...options,
where: {
[resourceKeyAttribute || Model.primaryKeyAttribute]: resourceKey,
},
// @ts-ignore hooks 里添加 context
context: ctx,
});
ctx.body = data;
}
await next();
}
export default get;

View File

@ -1,14 +1,10 @@
import Koa from 'koa';
import Database from '@nocobase/database';
import { Action } from '@nocobase/resourcer';
export type Next = () => Promise<any>;
export interface Context extends Koa.Context {
db: Database;
action: Action;
body: any;
};
export { default as common } from './common';
export { default as associate } from './associate';
export * from './add';
export * from './create';
export * from './destroy';
export * from './get';
export * from './list';
export * from './remove';
export * from './set';
export * from './sort';
export * from './toggle';
export * from './update';

View File

@ -0,0 +1,229 @@
import { Utils, Op, Sequelize } from 'sequelize';
import _ from 'lodash';
import { Context, Next } from '..';
import {
Model,
HASONE,
HASMANY,
BELONGSTO,
BELONGSTOMANY,
whereCompare
} from '@nocobase/database';
import { PageParameter } from '@nocobase/resourcer';
async function hasManyGet(instances, options: any = {}) {
const where = {};
let Model = this.target;
Model = Model.database.getModel(Model.name);
let instance;
let values;
if (!Array.isArray(instances)) {
instance = instances;
instances = undefined;
}
options = { ...options };
if (this.scope) {
Object.assign(where, this.scope);
}
if (instances) {
values = instances.map(_instance => _instance.get(this.sourceKey, { raw: true }));
if (options.limit && instances.length > 1) {
options.groupedLimit = {
limit: options.limit,
on: this, // association
values
};
delete options.limit;
} else {
where[this.foreignKey] = {
[Op.in]: values
};
delete options.groupedLimit;
}
} else {
where[this.foreignKey] = instance.get(this.sourceKey, { raw: true });
}
options.where = options.where ?
{ [Op.and]: [where, options.where] } :
where;
if (Object.prototype.hasOwnProperty.call(options, 'scope')) {
if (!options.scope) {
Model = Model.unscoped();
} else {
Model = Model.scope(options.scope);
}
}
if (Object.prototype.hasOwnProperty.call(options, 'schema')) {
Model = Model.schema(options.schema, options.schemaDelimiter);
}
const results = await Model.findAndCountAll(options);
if (instance) return results;
const result = {};
for (const _instance of instances) {
result[_instance.get(this.sourceKey, { raw: true })] = [];
}
for (const _instance of results) {
result[_instance.get(this.foreignKey, { raw: true })].push(_instance);
}
return result;
}
async function belongsToManyGet(instance, options) {
options = Utils.cloneDeep(options) || {};
const through = this.through;
let scopeWhere;
let throughWhere;
if (this.scope) {
scopeWhere = { ...this.scope };
}
options.where = {
[Op.and]: [
scopeWhere,
options.where
]
};
if (Object(through.model) === through.model) {
throughWhere = {};
throughWhere[this.foreignKey] = instance.get(this.sourceKey);
if (through.scope) {
Object.assign(throughWhere, through.scope);
}
//If a user pass a where on the options through options, make an "and" with the current throughWhere
if (options.through && options.through.where) {
throughWhere = {
[Op.and]: [throughWhere, options.through.where]
};
}
options.include = options.include || [];
options.include.push({
association: this.oneFromTarget,
attributes: options.joinTableAttributes,
required: true,
paranoid: _.get(options.through, 'paranoid', true),
where: throughWhere
});
}
let model = this.target;
if (Object.prototype.hasOwnProperty.call(options, 'scope')) {
if (!options.scope) {
model = model.unscoped();
} else {
model = model.scope(options.scope);
}
}
if (Object.prototype.hasOwnProperty.call(options, 'schema')) {
model = model.schema(options.schema, options.schemaDelimiter);
}
return model.findAndCountAll(options);
}
/**
*
*
* - Signle
* - HasMany
* - BelongsToMany
*
* HasOne belongsTo list
*
* @param ctx
* @param next
*/
export async function list(ctx: Context, next: Next) {
const {
page = PageParameter.DEFAULT_PAGE,
perPage = PageParameter.DEFAULT_PER_PAGE,
sort = [],
fields = [],
filter = {},
associated,
associatedName,
resourceName,
resourceField,
} = ctx.action.params;
let data = {};
let options: any = {};
let Model;
if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName);
Model = ctx.db.getModel(resourceField.options.target);
options = Model.parseApiJson({
sort,
page,
perPage,
filter,
fields,
context: ctx,
});
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
// const getAccessor = resourceField.getAccessors().get;
// const countAccessor = resourceField.getAccessors().count;
options.scope = options.scopes || [];
const association = AssociatedModel.associations[resourceField.options.name];
if (resourceField instanceof BELONGSTOMANY) {
data = await belongsToManyGet.call(association, associated, {
joinTableAttributes: [],
...options,
context: ctx,
});
}
if (resourceField instanceof HASMANY) {
data = await hasManyGet.call(association, associated, {
joinTableAttributes: [],
...options,
context: ctx,
});
}
} else {
Model = ctx.db.getModel(resourceName);
options = Model.parseApiJson({
sort,
page,
perPage,
filter,
fields,
context: ctx,
});
data = await Model.scope(options.scopes || []).findAndCountAll({
...options,
// @ts-ignore hooks 里添加 context
context: ctx,
});
}
if (options.limit || typeof options.offset !== 'undefined') {
// Math.round 避免精度问题
data['page'] = Math.round((options.offset || 0) / options.limit + 1);
data[Utils.underscoredIf('perPage', Model.options.underscored)] = options.limit;
}
ctx.body = data;
await next();
}
export default list;

View File

@ -0,0 +1,57 @@
import { Context, Next } from '..';
import {
Model,
Relation,
HASONE,
BELONGSTO,
BELONGSTOMANY,
HASMANY,
} from '@nocobase/database';
/**
*
*
* BlongsTo
* BlongsToMany
*
* @param ctx
* @param next
*/
export async function remove(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
} = ctx.action.params as {
associated: Model,
associatedName: string,
resourceField: Relation,
values: any,
};
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const { get: getAccessor, remove: removeAccessor, set: setAccessor } = resourceField.getAccessors();
const { resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const TargetModel = ctx.db.getModel(resourceField.getTarget());
const options = TargetModel.parseApiJson({
fields,
});
if (resourceField instanceof HASONE || resourceField instanceof BELONGSTO) {
ctx.body = await associated[setAccessor](null);
} else if (resourceField instanceof HASMANY || resourceField instanceof BELONGSTOMANY) {
const [model]: Model[] = await associated[getAccessor]({
...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
context: ctx,
});
await associated[removeAccessor](model);
ctx.body = { id: model.id };
}
await next();
}
export default remove;

View File

@ -0,0 +1,52 @@
import { Context, Next } from '..';
import {
Model,
Relation,
HASONE,
BELONGSTO,
BELONGSTOMANY,
HASMANY,
} from '@nocobase/database';
/**
*
*
* BlongsTo
* BlongsToMany
*
* @param ctx
* @param next
*/
export async function set(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
} = ctx.action.params as {
associated: Model,
associatedName: string,
resourceField: Relation,
values: any,
};
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const { set: setAccessor } = resourceField.getAccessors();
const { resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const TargetModel = ctx.db.getModel(resourceField.getTarget());
// const options = TargetModel.parseApiJson({
// fields,
// });
const model = await TargetModel.findOne({
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
// @ts-ignore
context: ctx,
});
ctx.body = await associated[setAccessor](model);
await next();
}
export default set;

View File

@ -0,0 +1,170 @@
import { Utils, Op, Sequelize } from 'sequelize';
import _ from 'lodash';
import { Context, Next } from '..';
import {
HASONE,
HASMANY,
BELONGSTO,
BELONGSTOMANY,
whereCompare
} from '@nocobase/database';
/**
*
*
* scope
* scope insertBefore
*/
export async function sort(ctx: Context, next: Next) {
const {
resourceName,
resourceKey,
resourceField,
associatedName,
associatedKey,
associated,
values = {},
...others
} = ctx.action.params;
if (associated && resourceField) {
if (resourceField instanceof HASONE || resourceField instanceof BELONGSTO) {
throw new Error(`the association (${resourceName} belongs to ${associatedName}) cannot be sorted`);
}
// TODO(feature)
if (resourceField instanceof BELONGSTOMANY) {
throw new Error('sorting for belongs to many association has not been implemented');
}
}
const Model = ctx.db.getModel(resourceName);
const table = ctx.db.getTable(resourceName);
const { primaryKeyAttribute } = Model;
const sourceId = others.sourceId || resourceKey;
const field = others.sortField || values?.sortField || values?.field || 'sort';
const targetId = others.targetId || values?.targetId || values?.target?.[primaryKeyAttribute];
const method = others.method || values?.method;
const insertAfter = method === 'insertAfter';
const sticky = others.sticky || values?.sticky || method === 'prepend';
const targetScope = others.targetScope || values?.targetScope;
if (!sourceId) {
throw new Error('source id invalid');
}
if (!(sticky || targetId || targetScope)) {
throw new Error('target id/scope invalid');
}
const sortField = table.getField(field);
if (!sortField) {
return next();
}
const { name: sortAttr, scope = [] } = sortField.options;
const transaction = await ctx.db.sequelize.transaction();
const where = {};
if (associated && resourceField instanceof HASMANY) {
where[resourceField.options.foreignKey] = associatedKey;
}
// 找到操作对象
const source = await Model.findOne({
where: {
...where,
[primaryKeyAttribute]: sourceId
},
transaction
});
if (!source) {
await transaction.rollback();
throw new Error(`resource ${sourceId} does not exist`);
}
const sourceScopeWhere = source.getValuesByFieldNames(scope);
let targetScopeWhere: any;
let targetObject;
if (targetId) {
targetObject = await Model.findByPk(targetId, { transaction });
if (!targetObject) {
await transaction.rollback();
throw new Error(`resource ${targetId} does not exist`);
}
targetScopeWhere = targetObject.getValuesByFieldNames(scope);
} else {
targetScopeWhere = { ...sourceScopeWhere, ...targetScope };
}
const sameScope = whereCompare(sourceScopeWhere, targetScopeWhere);
const updates = { ...targetScopeWhere };
if (targetObject) {
let increment: number;
const updateWhere = { ...targetScopeWhere };
if (sameScope) {
const direction = source[sortAttr] < targetObject[sortAttr] ? {
sourceOp: Op.gt,
targetOp: insertAfter ? Op.lt : Op.lte,
increment: -1
} : {
sourceOp: Op.lt,
targetOp: insertAfter ? Op.gt : Op.gte,
increment: 1
};
increment = direction.increment;
Object.assign(updateWhere, {
[sortAttr]: {
[direction.sourceOp]: source[sortAttr],
[direction.targetOp]: targetObject[sortAttr]
}
});
} else {
increment = 1;
Object.assign(updateWhere, {
[sortAttr]: {
[insertAfter ? Op.gt : Op.gte]: targetObject[sortAttr]
}
});
}
console.log({ insertAfter, updateWhere })
await Model.increment(sortAttr, {
by: increment,
where: updateWhere,
transaction
});
Object.assign(updates, {
[sortAttr]: insertAfter ? targetObject[sortAttr] + 1 : targetObject[sortAttr]
});
} else {
Object.assign(updates, {
[sortAttr]: await sortField.getNextValue({
next: sticky ? 'min' : 'max',
where: targetScopeWhere,
transaction
})
});
}
await source.update(updates, { transaction });
await transaction.commit();
ctx.body = source;
await next();
}
export default sort;

View File

@ -0,0 +1,72 @@
import { Context, Next } from '..';
import {
Model,
Relation,
HASONE,
BELONGSTO,
BELONGSTOMANY,
HASMANY,
} from '@nocobase/database';
export async function toggle(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
} = ctx.action.params as {
associated: Model,
associatedName: string,
resourceField: Relation,
values: any,
};
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const { get: getAccessor, remove: removeAccessor, set: setAccessor, add: addAccessor } = resourceField.getAccessors();
const { resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const TargetModel = ctx.db.getModel(resourceField.getTarget());
const options = TargetModel.parseApiJson({
fields,
});
if (resourceField instanceof HASONE || resourceField instanceof BELONGSTO) {
const m1 = await associated[getAccessor]();
if (m1 && m1[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute] == resourceKey) {
ctx.body = await associated[setAccessor](null);
} else {
const m2 = await TargetModel.findOne({
// ...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
// @ts-ignore
context: ctx,
});
ctx.body = await associated[setAccessor](m2);
}
} else if (resourceField instanceof HASMANY || resourceField instanceof BELONGSTOMANY) {
const [model]: Model[] = await associated[getAccessor]({
...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
context: ctx,
});
if (model) {
ctx.body = await associated[removeAccessor](model);
} else {
const m2 = await TargetModel.findOne({
// ...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
// @ts-ignore
context: ctx,
});
ctx.body = await associated[addAccessor](m2);
}
}
await next();
}
export default toggle;

View File

@ -0,0 +1,104 @@
import _ from 'lodash';
import { Context, Next } from '..';
import {
Model,
HASONE,
HASMANY,
BELONGSTO,
BELONGSTOMANY,
whereCompare
} from '@nocobase/database';
import { filterByFields } from '../utils';
/**
*
*
* TODO
*
* @param ctx
* @param next
*/
export async function update(ctx: Context, next: Next) {
const {
associated,
associatedName,
resourceField,
resourceName,
resourceKey,
// TODO(question): 这个属性从哪设置的?
resourceKeyAttribute,
fields,
values: data
} = ctx.action.params;
const values = filterByFields(data, fields);
const transaction = await ctx.db.sequelize.transaction();
const options = { transaction, context: ctx };
if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
await transaction.rollback();
throw new Error(`${associatedName} associated model invalid`);
}
const { get: getAccessor } = resourceField.getAccessors();
if (resourceField instanceof HASONE || resourceField instanceof BELONGSTO) {
let model: Model = await associated[getAccessor](options);
if (model) {
// @ts-ignore
await model.update(values, options);
await model.updateAssociations(values, options);
ctx.body = model;
}
} else if (resourceField instanceof HASMANY || resourceField instanceof BELONGSTOMANY) {
const TargetModel = ctx.db.getModel(resourceField.getTarget());
const [model]: Model[] = await associated[getAccessor]({
...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
}
});
if (resourceField instanceof BELONGSTOMANY) {
const throughName = resourceField.getThroughName();
if (typeof values[throughName] === 'object') {
const ThroughModel = resourceField.getThroughModel();
const throughValues = values[throughName];
const { foreignKey, sourceKey, otherKey } = resourceField.options;
const through = await ThroughModel.findOne({
where: {
[foreignKey]: associated[sourceKey],
[otherKey]: resourceKey,
},
transaction
});
// TODO: 中间表的 Model 有问题,关联数据更新有 BUG
// await through.updateAssociations(throughValues, options);
await through.update(throughValues, options);
delete values[throughName];
}
}
if (!_.isEmpty(values)) {
// @ts-ignore
await model.update(values, options);
await model.updateAssociations(values, options);
}
ctx.body = model;
}
} else {
const Model = ctx.db.getModel(resourceName);
const model = await Model.findOne({
...options,
where: {
[resourceKeyAttribute || Model.primaryKeyAttribute]: resourceKey,
}
});
// @ts-ignore
await model.update(values, options);
// @ts-ignore
await model.updateAssociations(values, options);
ctx.body = model;
}
await transaction.commit();
await next();
}
export default update;

View File

@ -1,6 +1,27 @@
import Koa from 'koa';
import Database from '@nocobase/database';
import Resourcer, { Action } from '@nocobase/resourcer';
import * as actions from './actions';
import * as middlewares from './middlewares';
export type Next = () => Promise<any>;
export interface Context extends Koa.Context {
db: Database;
action: Action;
body: any;
};
export * as utils from './utils';
export * as actions from './actions';
export * as middlewares from './middlewares';
export function registerActions(api: any) {
const resourcer = api.resourcer as Resourcer;
resourcer.use(middlewares.associated);
resourcer.registerActions({ ...actions });
}
export default actions;

View File

@ -1,4 +1,4 @@
import { Context, Next } from '../actions';
import { Context, Next } from '..';
import { Action } from '@nocobase/resourcer';
import { HASONE, HASMANY, BELONGSTO, BELONGSTOMANY } from '@nocobase/database';

View File

@ -1,31 +0,0 @@
import { Context, Next } from '../actions';
import { Action } from '@nocobase/resourcer';
export async function dataWrapping(ctx: Context, next: Next) {
await next();
if (!(ctx.action instanceof Action)) {
return;
}
if (ctx.withoutDataWrapping) {
return;
}
if (ctx.body instanceof Buffer) {
return;
}
if (!ctx.body) {
ctx.body = {};
}
const { rows, ...meta } = ctx.body;
if (rows) {
ctx.body = {
data: rows,
meta,
};
} else {
ctx.body = {
data: ctx.body,
};
}
}
export default dataWrapping;

View File

@ -1,2 +1 @@
export * from './associated';
export * from './data-wrapping';

View File

@ -1,21 +1,59 @@
import qs from 'qs';
import supertest from 'supertest';
import Application, { ApplicationOptions } from '@nocobase/server';
import { ActionParams } from '@nocobase/resourcer';
import { getConfig } from './mockDatabase';
interface ActionParams {
fields?: string[] | {
only?: string[];
except?: string[];
appends?: string[];
};
filter?: any;
sort?: string[];
page?: number;
perPage?: number;
values?: any;
resourceName?: string;
resourceKey?: string;
associatedName?: string;
associatedKey?: string;
[key: string]: any;
}
interface SortActionParams {
resourceName?: string;
resourceKey?: any;
associatedName?: string;
associatedKey?: any;
sourceId?: any;
targetId?: any;
sortField?: string;
method?: string;
target?: any;
sticky?: boolean;
[key: string]: any;
}
interface Resource {
get: (params?: ActionParams) => Promise<supertest.Response>;
list: (params?: ActionParams) => Promise<supertest.Response>;
create: (params?: ActionParams) => Promise<supertest.Response>;
update: (params?: ActionParams) => Promise<supertest.Response>;
destroy: (params?: ActionParams) => Promise<supertest.Response>;
sort: (params?: SortActionParams) => Promise<supertest.Response>;
[name: string]: (params?: ActionParams) => Promise<supertest.Response>;
}
export class MockServer extends Application {
protected agentInstance: supertest.SuperAgentTest;
agent() {
return supertest.agent(this.callback());
if (!this.agentInstance) {
this.agentInstance = supertest.agent(this.callback());
}
return this.agentInstance;
}
resource(name: string) {
@ -34,7 +72,7 @@ export class MockServer extends Application {
} = params;
let url = prefix;
if (keys.length > 1) {
url = `/${keys[0]}/${associatedKey}/${keys[1]}}`
url = `/${keys[0]}/${associatedKey}/${keys[1]}`
} else {
url = `/${name}`;
}