refactor: improve action unit test cases
This commit is contained in:
parent
409eb38d00
commit
99d33a0241
@ -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",
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
afterEach(async () => {
|
||||
return api.destroy();
|
||||
});
|
||||
|
||||
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',
|
||||
});
|
||||
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('create', async () => {
|
||||
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');
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
});
|
||||
registerActions(api);
|
||||
api.database.table({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'hasMany', name: 'comments' },
|
||||
],
|
||||
});
|
||||
api.database.table({
|
||||
name: 'comments',
|
||||
fields: [{ type: 'string', name: 'content' }],
|
||||
});
|
||||
await api.database.sync();
|
||||
});
|
||||
|
||||
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' }
|
||||
]);
|
||||
});
|
||||
afterEach(async () => {
|
||||
return api.destroy();
|
||||
});
|
||||
|
||||
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();
|
||||
it('destroy', async () => {
|
||||
const Post = api.database.getModel('posts');
|
||||
const post = await Post.create();
|
||||
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('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' },
|
||||
],
|
||||
});
|
||||
const [comment] = await post.getComments();
|
||||
await agent
|
||||
.delete(`/posts/${post.id}/comments/${comment.id}`);
|
||||
const count = await post.countComments();
|
||||
expect(count).toBe(0);
|
||||
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({
|
||||
comments: [comment],
|
||||
});
|
||||
|
||||
it('delete batch items in has-many list', async () => {
|
||||
const Post = db.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']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('belongsTo', () => {
|
||||
it('delete belongs-to item', async () => {
|
||||
const Post = db.getModel('posts');
|
||||
const post = await Post.create();
|
||||
await post.updateAssociations({
|
||||
user: { name: 'name121234' },
|
||||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
afterAll(() => db.close());
|
||||
|
||||
it('common1', async () => {
|
||||
const Post = db.getModel('posts');
|
||||
const post = await Post.create({
|
||||
title: 'title11112222'
|
||||
api = mockServer({
|
||||
dataWrapping: false,
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
@ -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 };
|
||||
});
|
||||
|
||||
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
|
||||
})));
|
||||
api = mockServer({
|
||||
dataWrapping: false,
|
||||
});
|
||||
|
||||
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' } }));
|
||||
});
|
||||
|
||||
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',
|
||||
},
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
registerActions(api);
|
||||
api.database.table({
|
||||
name: 'comments',
|
||||
fields: [
|
||||
{ type: 'string', name: 'content' },
|
||||
{ type: 'string', name: 'status', defaultValue: 'draft' },
|
||||
],
|
||||
});
|
||||
|
||||
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: [],
|
||||
});
|
||||
});
|
||||
api.database.table({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'hasMany', name: 'comments' },
|
||||
{ type: 'string', name: 'status', defaultValue: 'draft' },
|
||||
],
|
||||
});
|
||||
|
||||
describe('fields', () => {
|
||||
it('custom field', async () => {
|
||||
const response = await agent.get('/posts?fields=title&filter[customTitle]=title0');
|
||||
expect(response.body).toEqual({
|
||||
count: 1,
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
rows: [{ title: 'title0' }]
|
||||
});
|
||||
});
|
||||
|
||||
it('self field and belongs to field', async () => {
|
||||
const response = await agent.get('/posts?fields=title,user.name&filter[title]=title0');
|
||||
expect(response.body).toEqual({
|
||||
count: 1,
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
rows: [
|
||||
{
|
||||
title: 'title0',
|
||||
user: {
|
||||
name: 'a'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
// TODO(question): 当 fields 只填写了关联字段时,当前表的其他字段是否需要输出?
|
||||
it.skip('only belongs to', async () => {
|
||||
const response = await agent.get('/posts?fields=user&filter[title]=title0');
|
||||
expect(response.body).toEqual({
|
||||
count: 1,
|
||||
rows: [
|
||||
{
|
||||
title: 'title0',
|
||||
user: { name: 'a' }
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('except fields', async () => {
|
||||
const response = await agent.get('/posts?fields[except]=status&filter[title]=title0');
|
||||
expect(response.body.rows[0].status).toBeUndefined();
|
||||
});
|
||||
|
||||
it('only and except fields', async () => {
|
||||
const response = await agent.get('/posts?fields=title&fields[except]=status&filter[title]=title0');
|
||||
expect(response.body.rows[0].status).toBeUndefined();
|
||||
expect(response.body.rows).toEqual([{ title: 'title0' }]);
|
||||
});
|
||||
|
||||
it('only with belongs to fields', async () => {
|
||||
const response = await agent.get('/posts?fields[only]=title&fields[only]=user.name&filter[title]=title0');
|
||||
expect(response.body.rows[0].user.name).toEqual('a');
|
||||
expect(response.body.rows).toEqual([{ title: 'title0', user: { name: 'a' } }]);
|
||||
});
|
||||
|
||||
it('appends fields', async () => {
|
||||
const response = await agent.get('/posts?fields[only]=title&fields[appends]=user.name&filter[title]=title0');
|
||||
expect(response.body.rows[0].user.name).toEqual('a');
|
||||
expect(response.body.rows).toEqual([{
|
||||
title: 'title0', user: { id: 1, 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 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: Array(6).fill(null).map((_, index) => ({
|
||||
content: `content${index}`,
|
||||
status: index % 2 ? 'published' : 'draft',
|
||||
user_id: users[index % users.length].id
|
||||
}))
|
||||
comments: [{ content: 'c1', status: 'publish' }, { content: 'c2' }, { content: 'c3' }],
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
afterEach(async () => {
|
||||
return api.destroy();
|
||||
});
|
||||
|
||||
describe('fields', () => {
|
||||
it('fields', async () => {
|
||||
const response = await api.resource('posts').list({
|
||||
fields: ['title'],
|
||||
filter: {
|
||||
title: 't1',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
rows: [{ title: 't1' }],
|
||||
page: 1,
|
||||
per_page: 2,
|
||||
per_page: 20,
|
||||
});
|
||||
});
|
||||
|
||||
it('fields#appends', async () => {
|
||||
const response = await api.resource('posts').list({
|
||||
fields: {
|
||||
appends: ['comments'],
|
||||
},
|
||||
filter: {
|
||||
title: 't1',
|
||||
},
|
||||
});
|
||||
expect(response.body).toMatchObject({
|
||||
count: 1,
|
||||
rows: [
|
||||
{
|
||||
title: 't1',
|
||||
comments: [
|
||||
{ content: 'content4' },
|
||||
{ content: 'content2' }
|
||||
{
|
||||
content: 'c1',
|
||||
},
|
||||
{
|
||||
content: 'c2',
|
||||
},
|
||||
{
|
||||
content: 'c3',
|
||||
},
|
||||
],
|
||||
user: { name: 'a' }
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
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]
|
||||
describe('filter', () => {
|
||||
it('and', async () => {
|
||||
const response = await api.resource('posts').list({
|
||||
filter: {
|
||||
and: [
|
||||
{ title: 't1' },
|
||||
{ status: 'draft' },
|
||||
],
|
||||
},
|
||||
});
|
||||
await post2.updateAssociations({
|
||||
tags: [2, 5, 8]
|
||||
});
|
||||
const User = db.getModel('users');
|
||||
const user = await User.create();
|
||||
await user.updateAssociations({
|
||||
posts: [post1]
|
||||
expect(response.body).toMatchObject({
|
||||
count: 1,
|
||||
rows: [
|
||||
{
|
||||
title: 't1',
|
||||
},
|
||||
],
|
||||
page: 1,
|
||||
per_page: 20
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
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
|
||||
});
|
||||
});
|
||||
|
||||
// TODO(bug): SQL 报错
|
||||
it.skip('list2', async () => {
|
||||
const response = await agent
|
||||
.get(`/users/1/posts?fields=tags`);
|
||||
console.log(response.body);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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 }
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
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,
|
||||
}
|
||||
api = mockServer({
|
||||
dataWrapping: false,
|
||||
});
|
||||
const postUser = await post.getUser();
|
||||
expect(user.id).toBe(postUser.id);
|
||||
registerActions(api);
|
||||
});
|
||||
|
||||
// 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);
|
||||
afterEach(async () => {
|
||||
return api.destroy();
|
||||
});
|
||||
|
||||
it('set', async () => {
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -1,302 +1,513 @@
|
||||
import { initDatabase, agent } from './index';
|
||||
import { mockServer, MockServer } from '@nocobase/test';
|
||||
import { registerActions } from '..';
|
||||
|
||||
describe('get', () => {
|
||||
let db;
|
||||
describe('sort', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
db = await initDatabase();
|
||||
const User = db.getModel('users');
|
||||
const users = await User.bulkCreate(Array.from('abcdefg').map(name => ({ name })));
|
||||
describe('same scope', () => {
|
||||
let api: MockServer;
|
||||
|
||||
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());
|
||||
});
|
||||
|
||||
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']]
|
||||
beforeEach(async () => {
|
||||
api = mockServer({
|
||||
dataWrapping: false,
|
||||
});
|
||||
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 }
|
||||
]);
|
||||
});
|
||||
|
||||
it('initialization by updateAssociations', async () => {
|
||||
const Comment = db.getModel('comments');
|
||||
const comments = await Comment.findAll({
|
||||
order: [['id', 'ASC']]
|
||||
registerActions(api);
|
||||
api.database.table({
|
||||
name: 'tests',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'sort', name: 'sort' },
|
||||
{ type: 'sort', name: 'sort2' },
|
||||
],
|
||||
});
|
||||
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 }
|
||||
]);
|
||||
await api.database.sync();
|
||||
const Test = api.database.getModel('tests');
|
||||
for (let index = 1; index < 5; index++) {
|
||||
await Test.create({ title: `t${index}` });
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
afterEach(async () => {
|
||||
return api.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
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']]
|
||||
it('targetId', async () => {
|
||||
await api.resource('tests').sort({
|
||||
sourceId: 1,
|
||||
targetId: 3,
|
||||
});
|
||||
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']]
|
||||
const response = await api.resource('tests').list({
|
||||
sort: ['sort'],
|
||||
});
|
||||
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'
|
||||
},
|
||||
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'
|
||||
},
|
||||
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'
|
||||
},
|
||||
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 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
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 },
|
||||
});
|
||||
|
||||
const Post = db.getModel('posts');
|
||||
const posts = await Post.findAll({
|
||||
where: {
|
||||
user_id: 3
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't2',
|
||||
sort: 1,
|
||||
},
|
||||
attributes: ['id', 'sort_in_user'],
|
||||
order: [['id', 'ASC']]
|
||||
});
|
||||
{
|
||||
title: 't3',
|
||||
sort: 2,
|
||||
},
|
||||
{
|
||||
title: 't1',
|
||||
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('targetId', async () => {
|
||||
await api.resource('tests').sort({
|
||||
sourceId: 3,
|
||||
targetId: 1,
|
||||
});
|
||||
const response = await api.resource('tests').list({
|
||||
sort: ['sort'],
|
||||
});
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't3',
|
||||
sort: 1,
|
||||
},
|
||||
{
|
||||
title: 't1',
|
||||
sort: 2,
|
||||
},
|
||||
{
|
||||
title: 't2',
|
||||
sort: 3,
|
||||
},
|
||||
{
|
||||
title: 't4',
|
||||
sort: 4,
|
||||
}
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
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',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
21
packages/actions/src/__tests__/toggle.test.ts
Normal file
21
packages/actions/src/__tests__/toggle.test.ts
Normal 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 () => {
|
||||
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
|
||||
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');
|
||||
api = mockServer({
|
||||
dataWrapping: false,
|
||||
});
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
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 [tag2] = await post.getTags();
|
||||
expect(tag2.posts_tags.test).toBe('test112233');
|
||||
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 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 comment2 = await Comment.findByPk(comment.id);
|
||||
expect(comment2.toJSON()).toMatchObject({
|
||||
content: 'c2',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { initDatabase, agent } from './index';
|
||||
import { filterByFields } from '../utils';
|
||||
|
||||
describe('utils', () => {
|
||||
|
48
packages/actions/src/actions/add.ts
Normal file
48
packages/actions/src/actions/add.ts
Normal 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;
|
@ -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
|
||||
}
|
@ -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 层面一般不开放 HasOne、BelongsTo、BelongsToMany 的新增数据操作
|
||||
* 如果需要这类操作建议使用 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
|
||||
};
|
55
packages/actions/src/actions/create.ts
Normal file
55
packages/actions/src/actions/create.ts
Normal 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 层面一般不开放 HasOne、BelongsTo、BelongsToMany 的新增数据操作
|
||||
* 如果需要这类操作建议使用 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;
|
95
packages/actions/src/actions/destroy.ts
Normal file
95
packages/actions/src/actions/destroy.ts
Normal 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;
|
77
packages/actions/src/actions/get.ts
Normal file
77
packages/actions/src/actions/get.ts
Normal 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;
|
@ -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';
|
||||
|
229
packages/actions/src/actions/list.ts
Normal file
229
packages/actions/src/actions/list.ts
Normal 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;
|
57
packages/actions/src/actions/remove.ts
Normal file
57
packages/actions/src/actions/remove.ts
Normal 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;
|
52
packages/actions/src/actions/set.ts
Normal file
52
packages/actions/src/actions/set.ts
Normal 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;
|
170
packages/actions/src/actions/sort.ts
Normal file
170
packages/actions/src/actions/sort.ts
Normal 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;
|
72
packages/actions/src/actions/toggle.ts
Normal file
72
packages/actions/src/actions/toggle.ts
Normal 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;
|
104
packages/actions/src/actions/update.ts
Normal file
104
packages/actions/src/actions/update.ts
Normal 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;
|
@ -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;
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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;
|
@ -1,2 +1 @@
|
||||
export * from './associated';
|
||||
export * from './data-wrapping';
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user