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