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" |     "@nocobase/resourcer": "^0.4.0-alpha.7" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "koa": "^2.13.0", |     "@nocobase/test": "^0.4.0-alpha.7" | ||||||
|     "sequelize": "^6.3.4" |  | ||||||
|   }, |   }, | ||||||
|   "repository": { |   "repository": { | ||||||
|     "type": "git", |     "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', () => { | describe('add', () => { | ||||||
|   let db; |   let api: MockServer; | ||||||
| 
 | 
 | ||||||
|   beforeEach(async () => { |   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', () => { | describe('create', () => { | ||||||
|   let db; |   let api: MockServer; | ||||||
| 
 | 
 | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     db = await initDatabase(); |     api = mockServer({ | ||||||
|  |       dataWrapping: false, | ||||||
|  |     }); | ||||||
|  |     registerActions(api); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   afterAll(() => db.close()); |   afterEach(async () => { | ||||||
| 
 |     return api.destroy(); | ||||||
|   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); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   describe('hasMany', () => { |  | ||||||
|   it('create', async () => { |   it('create', async () => { | ||||||
|       const Post = db.getModel('posts'); |     api.database.table({ | ||||||
|       const post = await Post.create(); |       name: 'tests', | ||||||
|       const response = await agent |       fields: [ | ||||||
|         .post(`/posts/${post.id}/comments`) |         { type: 'string', name: 'name' }, | ||||||
|         .send({ |       ], | ||||||
|           content: 'content1', |     }); | ||||||
|  |     await api.database.sync(); | ||||||
|  |     const response = await api.resource('tests').create({ | ||||||
|  |       values: { name: 'n1' }, | ||||||
|  |     }); | ||||||
|  |     expect(response.body.name).toBe('n1'); | ||||||
|   }); |   }); | ||||||
|       expect(response.body.post_id).toBe(post.id); |  | ||||||
|       expect(response.body.content).toBe('content1'); |  | ||||||
| 
 | 
 | ||||||
|       const comments = await agent |   it('associations', async () => { | ||||||
|         .get('/comments?fields=id,content'); |     api.database.table({ | ||||||
|       expect(comments.body.count).toBe(1); |       name: 'comments', | ||||||
|       expect(comments.body.rows).toEqual([{ |       fields: [ | ||||||
|         id: 1, |         { type: 'string', name: 'content' }, | ||||||
|         content: 'content1' |       ], | ||||||
|       }]); |     }); | ||||||
|     }); |     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', () => { | describe('destroy', () => { | ||||||
|   let db; |   let api: MockServer; | ||||||
| 
 | 
 | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     db = await initDatabase(); |     api = mockServer({ | ||||||
|  |       dataWrapping: false, | ||||||
|     }); |     }); | ||||||
| 
 |     registerActions(api); | ||||||
|   afterAll(() => db.close()); |     api.database.table({ | ||||||
| 
 |       name: 'posts', | ||||||
|   describe('single', () => { |       fields: [ | ||||||
|     it('common1', async () => { |         { type: 'string', name: 'title' }, | ||||||
|       const Post = db.getModel('posts'); |         { type: 'hasMany', name: 'comments' }, | ||||||
|       const post = await Post.create(); |  | ||||||
|       const response = await agent |  | ||||||
|         .delete(`/posts/${post.id}`); |  | ||||||
|       // console.log(response.body);
 |  | ||||||
|       expect(response.body.count).toBe(1); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('batch delete by filter', async () => { |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const posts = await Post.bulkCreate([ |  | ||||||
|         { title: 'title1', status: 'published' }, |  | ||||||
|         { title: 'title2', status: 'draft' }, |  | ||||||
|         { title: 'title3', status: 'published' }, |  | ||||||
|         { title: 'title4', status: 'draft' }, |  | ||||||
|       ]); |  | ||||||
| 
 |  | ||||||
|       await agent |  | ||||||
|         .delete('/posts?filter[status]=draft'); |  | ||||||
| 
 |  | ||||||
|       const published = await Post.findAll(); |  | ||||||
|       expect(published.length).toBe(2); |  | ||||||
|       expect(published.map(({ title, status }) => ({ title, status }))).toEqual([ |  | ||||||
|         { title: 'title1', status: 'published' }, |  | ||||||
|         { title: 'title3', status: 'published' } |  | ||||||
|       ]); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   describe('hasOne', () => { |  | ||||||
|     it('delete has-one item', async () => { |  | ||||||
|       const User = db.getModel('users'); |  | ||||||
|       const user = await User.create(); |  | ||||||
|       await user.updateAssociations({ |  | ||||||
|         profile: { |  | ||||||
|           email: 'email1122', |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|       const response = await agent |  | ||||||
|         .delete(`/users/${user.id}/profile`); |  | ||||||
|       const profile = await user.getProfile(); |  | ||||||
|       expect(profile).toBeNull(); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   describe('hasMany', () => { |  | ||||||
|     it('delete single item in has-many list', async () => { |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const post = await Post.create(); |  | ||||||
|       await post.updateAssociations({ |  | ||||||
|         comments: [ |  | ||||||
|           { content: 'content111222' }, |  | ||||||
|       ], |       ], | ||||||
|     }); |     }); | ||||||
|       const [comment] = await post.getComments(); |     api.database.table({ | ||||||
|       await agent |       name: 'comments', | ||||||
|         .delete(`/posts/${post.id}/comments/${comment.id}`); |       fields: [{ type: 'string', name: 'content' }], | ||||||
|       const count = await post.countComments(); |     }); | ||||||
|       expect(count).toBe(0); |     await api.database.sync(); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|     it('delete batch items in has-many list', async () => { |   afterEach(async () => { | ||||||
|       const Post = db.getModel('posts'); |     return api.destroy(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('destroy', async () => { | ||||||
|  |     const Post = api.database.getModel('posts'); | ||||||
|     const post = await Post.create(); |     const post = await Post.create(); | ||||||
|       await post.updateAssociations({ |     expect( | ||||||
|         comments: [ |       await Post.count({ | ||||||
|           { content: 'content1', status: 'published' }, |         where: { id: post.id }, | ||||||
|           { content: 'content2', status: 'draft' }, |       }), | ||||||
|           { content: 'content3', status: 'published' }, |     ).toBe(1); | ||||||
|           { content: 'content4', status: 'draft' }, |     await api.resource('posts').destroy({ | ||||||
|         ], |       resourceKey: post.id, | ||||||
|       }); |  | ||||||
|       await agent |  | ||||||
|         .delete(`/posts/${post.id}/comments?filter[status]=draft`); |  | ||||||
|       const comments = await post.getComments(); |  | ||||||
|       expect(comments.length).toBe(2); |  | ||||||
|       expect(comments.map(({ content }) => content)).toEqual(['content1', 'content3']); |  | ||||||
|     }); |     }); | ||||||
|  |     expect( | ||||||
|  |       await Post.count({ | ||||||
|  |         where: { id: post.id }, | ||||||
|  |       }), | ||||||
|  |     ).toBe(0); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('belongsTo', () => { |   it('destroy associations', async () => { | ||||||
|     it('delete belongs-to item', async () => { |     const [Post, Comment] = api.database.getModels(['posts', 'comments']); | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|     const post = await Post.create(); |     const post = await Post.create(); | ||||||
|  |     const comment = await Comment.create(); | ||||||
|     await post.updateAssociations({ |     await post.updateAssociations({ | ||||||
|         user: { name: 'name121234' }, |       comments: [comment], | ||||||
|       }); |  | ||||||
|       await agent.delete(`/posts/${post.id}/user:destroy`); |  | ||||||
|       const user = await post.getUser(); |  | ||||||
|       expect(user).toBeNull(); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   describe('belongsToMany', () => { |  | ||||||
|     it('delete single target item', async () => { |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const post = await Post.create(); |  | ||||||
|       await post.updateAssociations({ |  | ||||||
|         tags: [ |  | ||||||
|           { name: 'tag112233' }, |  | ||||||
|         ], |  | ||||||
|       }); |  | ||||||
|       const [tag] = await post.getTags(); |  | ||||||
|       await agent |  | ||||||
|         .delete(`/posts/${post.id}/tags:destroy/${tag.id}`); |  | ||||||
|       const tags = await post.getTags(); |  | ||||||
|       expect(tags.length).toBe(0); |  | ||||||
| 
 |  | ||||||
|       const PostsTags = db.getModel('posts_tags'); |  | ||||||
|       const postsTags = await PostsTags.findAll(); |  | ||||||
|       expect(postsTags.length).toBe(0); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('delete batch target item by filter', async () => { |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const post = await Post.create(); |  | ||||||
|       await post.updateAssociations({ |  | ||||||
|         tags: [ |  | ||||||
|           { name: 'tag1', status: 'enabled' }, |  | ||||||
|           { name: 'tag2', status: 'disabled' }, |  | ||||||
|           { name: 'tag3', status: 'enabled' }, |  | ||||||
|           { name: 'tag4', status: 'disabled' }, |  | ||||||
|         ], |  | ||||||
|       }); |  | ||||||
|       await agent |  | ||||||
|         .delete(`/posts/${post.id}/tags:destroy?filter[status]=disabled`); |  | ||||||
|       const tags = await post.getTags(); |  | ||||||
|       expect(tags.length).toBe(2); |  | ||||||
| 
 |  | ||||||
|       const PostsTags = db.getModel('posts_tags'); |  | ||||||
|       const postsTags = await PostsTags.findAll(); |  | ||||||
|       expect(postsTags.length).toBe(2); |  | ||||||
|     }); |     }); | ||||||
|  |     await api.resource('posts.comments').destroy({ | ||||||
|  |       resourceKey: comment.id, | ||||||
|  |       associatedKey: post.id, | ||||||
|  |     }); | ||||||
|  |     const comment2 = await Comment.findByPk(comment.id); | ||||||
|  |     expect(comment2).toBeNull(); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -1,91 +1,57 @@ | |||||||
| import { initDatabase, agent } from './index'; | import { mockServer, MockServer } from '@nocobase/test'; | ||||||
|  | import { registerActions } from '..'; | ||||||
| 
 | 
 | ||||||
| describe('get', () => { | describe('get', () => { | ||||||
|   let db; |   let api: MockServer; | ||||||
| 
 | 
 | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     db = await initDatabase(); |     api = mockServer({ | ||||||
|  |       dataWrapping: false, | ||||||
|     }); |     }); | ||||||
| 
 |     registerActions(api); | ||||||
|   afterAll(() => db.close()); |     api.database.table({ | ||||||
| 
 |       name: 'posts', | ||||||
|   it('common1', async () => { |       fields: [ | ||||||
|     const Post = db.getModel('posts'); |         { type: 'string', name: 'title' }, | ||||||
|     const post = await Post.create({ |         { type: 'hasMany', name: 'comments' }, | ||||||
|       title: 'title11112222' |  | ||||||
|     }); |  | ||||||
|     const response = await agent |  | ||||||
|       .get(`/posts/${post.id}`); |  | ||||||
|     expect(response.body.title).toBe('title11112222'); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   it('hasOne1', async () => { |  | ||||||
|     const User = db.getModel('users'); |  | ||||||
|     const user = await User.create(); |  | ||||||
|     const response = await agent |  | ||||||
|       .get(`/users/${user.id}/profile?fields=email`); |  | ||||||
|     expect(response.body).toEqual({}); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   it('hasOne2', async () => { |  | ||||||
|     const User = db.getModel('users'); |  | ||||||
|     const user = await User.create(); |  | ||||||
|     await user.updateAssociations({ |  | ||||||
|       profile: { |  | ||||||
|         email: 'email1', |  | ||||||
|       }, |  | ||||||
|     }); |  | ||||||
|     const response = await agent |  | ||||||
|       .get(`/users/${user.id}/profile?fields=email`); |  | ||||||
|     expect(response.body).toEqual({ email: 'email1' }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   it('hasMany1', async () => { |  | ||||||
|     const Post = db.getModel('posts'); |  | ||||||
|     const post = await Post.create(); |  | ||||||
|     await post.updateAssociations({ |  | ||||||
|       comments: [ |  | ||||||
|         { content: 'content111222' }, |  | ||||||
|       ], |       ], | ||||||
|     }); |     }); | ||||||
|     const [comment] = await post.getComments(); |     api.database.table({ | ||||||
|     const response = await agent |       name: 'comments', | ||||||
|       .get(`/posts/${post.id}/comments/${comment.id}`); |       fields: [ | ||||||
|     expect(response.body.post_id).toBe(post.id); |         { type: 'string', name: 'content' }, | ||||||
|     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' }, |  | ||||||
|       ], |       ], | ||||||
|     }); |     }); | ||||||
|     const [tag] = await post.getTags(); |     await api.database.sync(); | ||||||
|     const response = await agent |   }); | ||||||
|       .get(`/posts/${post.id}/tags/${tag.id}?fields=name,posts.id`); | 
 | ||||||
|     expect(response.body.posts[0].id).toBe(post.id); |   afterEach(async () => { | ||||||
|     expect(response.body.name).toBe('tag112233'); |     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 { mockServer, MockServer } from '@nocobase/test'; | ||||||
| 
 | import { registerActions } from '..'; | ||||||
| import { initDatabase, agent } from './index'; |  | ||||||
| 
 | 
 | ||||||
| describe('list', () => { | describe('list', () => { | ||||||
|   let db; |   let api: MockServer; | ||||||
|   let now: Date; |  | ||||||
|   let nowString: string; |  | ||||||
|   let timestamps: { created_at: Date; updated_at: Date; }; |  | ||||||
|   let timestampsStrings; |  | ||||||
| 
 | 
 | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     db = await initDatabase(); |     api = mockServer({ | ||||||
|     now = new Date(); |       dataWrapping: false, | ||||||
|     nowString = now.toISOString() |  | ||||||
|     timestamps = { created_at: now, updated_at: now }; |  | ||||||
|     timestampsStrings = { created_at: nowString, updated_at: nowString }; |  | ||||||
|     }); |     }); | ||||||
| 
 |     registerActions(api); | ||||||
|   afterAll(() => db.close()); |     api.database.table({ | ||||||
| 
 |       name: 'comments', | ||||||
|   describe('common', () => { |       fields: [ | ||||||
|     beforeEach(async () => { |         { type: 'string', name: 'content' }, | ||||||
|       const User = db.getModel('users'); |         { type: 'string', name: 'status', defaultValue: 'draft' }, | ||||||
|       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.database.table({ | ||||||
|     describe('filter', () => { |       name: 'posts', | ||||||
|       describe('equal', () => { |       fields: [ | ||||||
|         it('should be filtered by `status` equal to `published`', async () => { |         { type: 'string', name: 'title' }, | ||||||
|           const Post = db.getModel('posts'); |         { type: 'hasMany', name: 'comments' }, | ||||||
|           const response = await agent.get('/posts?filter[status]=published'); |         { type: 'string', name: 'status', defaultValue: 'draft' }, | ||||||
|           expect(response.body.count).toBe(await Post.count({ where: { status: 'published' } })); |       ], | ||||||
|     }); |     }); | ||||||
| 
 |     await api.database.sync(); | ||||||
|         it('should be filtered by `title` equal to `title1`', async () => { |     const [Post, Comment] = api.database.getModels(['posts', 'comments']); | ||||||
|           const Post = db.getModel('posts'); |     for (let index = 1; index < 4; index++) { | ||||||
|           const response = await agent.get('/posts?filter[title]=title1'); |       const post = await Post.create({ title: `t${index}` }); | ||||||
|           expect(response.body.count).toBe(await Post.count({ |       await post.updateAssociations({ | ||||||
|             where: { |         comments: [{ content: 'c1', status: 'publish' }, { content: 'c2' }, { content: 'c3' }], | ||||||
|               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 () => { |   afterEach(async () => { | ||||||
|           const Post = db.getModel('posts'); |     return api.destroy(); | ||||||
|           const expected = await Post.findAll({ |  | ||||||
|             where: { |  | ||||||
|               published_at: { |  | ||||||
|                 [Op.is]: null |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           }); |  | ||||||
|           const response = await agent.get('/posts?filter[published_at.is]'); |  | ||||||
|           expect(response.body.count).toBe(expected.length); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         it('filter[published_at.not]', async () => { |  | ||||||
|           const Post = db.getModel('posts'); |  | ||||||
|           const expected = await Post.findAll({ |  | ||||||
|             where: { |  | ||||||
|               published_at: { |  | ||||||
|                 [Op.not]: null |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           }); |  | ||||||
|           const response = await agent.get('/posts?filter[published_at.not]'); |  | ||||||
|           expect(response.body.count).toBe(expected.length); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         // TODO(bug): should use `user.is`
 |  | ||||||
|         it('filter[user_id.is]', async () => { |  | ||||||
|           const Post = db.getModel('posts'); |  | ||||||
|           const expected = await Post.findAll({ |  | ||||||
|             where: { |  | ||||||
|               user_id: { |  | ||||||
|                 [Op.is]: null |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           }); |  | ||||||
|           const response = await agent.get('/posts?filter[user_id.is]'); |  | ||||||
|           expect(response.body.count).toBe(expected.length); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       describe('merge params with action options', () => { |  | ||||||
|         it('plain key-value filter', async () => { |  | ||||||
|           const response = await agent.get('/posts:list1?filter[status]=draft'); |  | ||||||
|           expect(response.body.count).toBe(0); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         it('date filter', async () => { |  | ||||||
|           // const before1Days = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
 |  | ||||||
|           const before3Days = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 3); |  | ||||||
|           const response = await agent.get(`/posts:list1?filter[published_at.gt]=${before3Days.toISOString()}`); |  | ||||||
|           expect(response.body.count).toBe(1); |  | ||||||
|           expect(response.body.rows[0].id).toBe(2); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       describe('custom ops', () => { |  | ||||||
|         it('$null', async () => { |  | ||||||
|           const Post = db.getModel('posts'); |  | ||||||
|           const expected = await Post.findAll({ |  | ||||||
|             where: { |  | ||||||
|               published_at: null |  | ||||||
|             } |  | ||||||
|           }); |  | ||||||
|           const response = await agent.get('/posts?filter[published_at.$null]='); |  | ||||||
|           expect(response.body.count).toBe(expected.length); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         describe('$anyOf', () => { |  | ||||||
|           describe('single', () => { |  | ||||||
|             // TODO(question): 是否应该用 in/notIn 来处理单项?
 |  | ||||||
|             // 或者单项存值也使用 JSON 类型也可以。
 |  | ||||||
|             it.skip('$anyOf', async () => { |  | ||||||
|               // const Profile = db.getModel('profiles');
 |  | ||||||
|               // const profiles = await Profile.findAll();
 |  | ||||||
|               const response = await agent.get('/profiles?filter[city.$anyOf]=Beijing,Weihai'); |  | ||||||
|               console.log(response.body); |  | ||||||
|               // expect(response.body.count).toBe(2);
 |  | ||||||
|             }); |  | ||||||
|           }); |  | ||||||
| 
 |  | ||||||
|           describe('multiple', () => { |  | ||||||
|             it('$anyOf for 1 element in definition', async () => { |  | ||||||
|               const User = db.getModel('users'); |  | ||||||
|               const expected = await User.findOne({ |  | ||||||
|                 where: { |  | ||||||
|                   nicknames: { [Op.contains]: 'aa' } |  | ||||||
|                 } |  | ||||||
|               }); |  | ||||||
|               const response = await agent.get('/users?filter[nicknames.$anyOf][]=aa'); |  | ||||||
|               expect(response.body.count).toBe(1); |  | ||||||
|               expect(response.body.rows[0].name).toBe(expected.name); |  | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
|             it('$anyOf for all elements in definition', async () => { |  | ||||||
|               const User = db.getModel('users'); |  | ||||||
|               const expected = await User.findOne({ |  | ||||||
|                 where: { |  | ||||||
|                   nicknames: { |  | ||||||
|                     [Op.or]: [ |  | ||||||
|                       { [Op.contains]: 'aaa' }, |  | ||||||
|                       { [Op.contains]: 'aa' } |  | ||||||
|                     ] |  | ||||||
|                   } |  | ||||||
|                 } |  | ||||||
|               }); |  | ||||||
|               const response = await agent.get('/users?filter[nicknames.$anyOf]=aaa,aa'); |  | ||||||
|               expect(response.body.count).toBe(1); |  | ||||||
|               expect(response.body.rows[0].name).toBe(expected.name); |  | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
|             it('$anyOf for some element not in definition', async () => { |  | ||||||
|               const User = db.getModel('users'); |  | ||||||
|               const expected = await User.findOne({ |  | ||||||
|                 where: { |  | ||||||
|                   nicknames: { [Op.or]: [{ [Op.contains]: ['aaa'] }, { [Op.contains]: ['a'] }] } |  | ||||||
|                 } |  | ||||||
|               }); |  | ||||||
|               const response = await agent.get('/users?filter[nicknames.$anyOf]=aaa,a'); |  | ||||||
|               expect(response.body.count).toBe(1); |  | ||||||
|               expect(response.body.rows[0].name).toBe(expected.name); |  | ||||||
|             }); |  | ||||||
| 
 |  | ||||||
|             it('$anyOf for no element', async () => { |  | ||||||
|               const User = db.getModel('users'); |  | ||||||
|               const expected = await User.findAll(); |  | ||||||
|               const response = await agent.get('/users?filter={"nicknames.$anyOf":[]}'); |  | ||||||
|               expect(response.body.count).toBe(expected.length); |  | ||||||
|             }); |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         describe('$allOf', () => { |  | ||||||
|           it('$allOf for no element', async () => { |  | ||||||
|             const response = await agent.get('/users?filter={"nicknames.$allOf":[]}'); |  | ||||||
|             expect(response.body.count).toBe(3); |  | ||||||
|           }); |  | ||||||
| 
 |  | ||||||
|           it('$allOf for different element', async () => { |  | ||||||
|             const response = await agent.get('/users?filter[nicknames.$allOf]=a,aa'); |  | ||||||
|             expect(response.body.count).toBe(0); |  | ||||||
|           }); |  | ||||||
| 
 |  | ||||||
|           it('$allOf for less element', async () => { |  | ||||||
|             const response = await agent.get('/users?filter[nicknames.$allOf][]=aa&fields=name,nicknames'); |  | ||||||
|             expect(response.body.count).toBe(1); |  | ||||||
|             expect(response.body.rows).toEqual([ |  | ||||||
|               { name: 'a', nicknames: ['aa', 'aaa'] } |  | ||||||
|             ]); |  | ||||||
|           }); |  | ||||||
| 
 |  | ||||||
|           it('$allOf for same element', async () => { |  | ||||||
|             const response = await agent.get('/users?filter[nicknames.$allOf]=aa,aaa&fields=name,nicknames'); |  | ||||||
|             expect(response.body.count).toBe(1); |  | ||||||
|             expect(response.body.rows).toEqual([ |  | ||||||
|               { name: 'a', nicknames: ['aa', 'aaa'] } |  | ||||||
|             ]); |  | ||||||
|           }); |  | ||||||
| 
 |  | ||||||
|           it('$allOf for more element', async () => { |  | ||||||
|             const response = await agent.get('/users?filter[nicknames.$allOf]=a,aa,aaa'); |  | ||||||
|             expect(response.body.count).toBe(0); |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         // TODO(bug): 需要 toWhere 重构和操作符函数修改
 |  | ||||||
|         describe.skip('$noneOf', () => { |  | ||||||
|           it('$noneOf for no element', async () => { |  | ||||||
|             const response = await agent.get('/users?filter={"nicknames.$noneOf":[]}'); |  | ||||||
|             expect(response.body.count).toBe(3); |  | ||||||
|           }); |  | ||||||
| 
 |  | ||||||
|           it('$noneOf for different element', async () => { |  | ||||||
|             const User = db.getModel('users'); |  | ||||||
|             const users = await User.findAll({ |  | ||||||
|               where: { |  | ||||||
|                 [Op.not]: { |  | ||||||
|                   // 不使用 or 包装两个同一个 col 的条件会被转化成 and,与官方文档不符
 |  | ||||||
|                   // WHERE NOT ("users"."nicknames" @> '"aa"' AND "users"."nicknames" @> '"a"')
 |  | ||||||
|                   [Op.or]: [ |  | ||||||
|                     { nicknames: { [Op.contains]: 'aa' } }, |  | ||||||
|                     { nicknames: { [Op.contains]: 'a' } }, |  | ||||||
|                   ] |  | ||||||
|                 } |  | ||||||
|               } |  | ||||||
|             }); |  | ||||||
|             console.log(users); |  | ||||||
|             // const response = await agent.get('/users?filter[nicknames.$noneOf]=a,aa');
 |  | ||||||
|             // expect(response.body.count).toBe(2);
 |  | ||||||
|           }); |  | ||||||
| 
 |  | ||||||
|           it('$noneOf for less element', async () => { |  | ||||||
|             const response = await agent.get('/users?filter[nicknames.$noneOf][]=aa&fields=name,nicknames'); |  | ||||||
|             expect(response.body.count).toBe(2); |  | ||||||
|           }); |  | ||||||
| 
 |  | ||||||
|           it('$noneOf for same element', async () => { |  | ||||||
|             const response = await agent.get('/users?filter[nicknames.$noneOf]=aa,aaa&fields=name,nicknames'); |  | ||||||
|             expect(response.body.count).toBe(2); |  | ||||||
|           }); |  | ||||||
| 
 |  | ||||||
|           it('$noneOf for more element', async () => { |  | ||||||
|             const response = await agent.get('/users?filter[nicknames.$noneOf]=a,aa,aaa'); |  | ||||||
|             expect(response.body.count).toBe(2); |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         describe('$match', () => { |  | ||||||
|           // TODO(bug)
 |  | ||||||
|           it.skip('$match for no element', async () => { |  | ||||||
|             const response = await agent.get('/users?filter={"nicknames.$match":[]}'); |  | ||||||
|             expect(response.body.count).toBe(2); |  | ||||||
|           }); |  | ||||||
| 
 |  | ||||||
|           it('$match for different element', async () => { |  | ||||||
|             const response = await agent.get('/users?filter[nicknames.$match]=a,aa'); |  | ||||||
|             expect(response.body.count).toBe(0); |  | ||||||
|           }); |  | ||||||
| 
 |  | ||||||
|           it('$match for less element', async () => { |  | ||||||
|             const response = await agent.get('/users?filter[nicknames.$match][]=aa&fields=name,nicknames'); |  | ||||||
|             expect(response.body.count).toBe(0); |  | ||||||
|           }); |  | ||||||
| 
 |  | ||||||
|           it('$match for same element', async () => { |  | ||||||
|             const response = await agent.get('/users?filter[nicknames.$match]=aa,aaa&fields=name,nicknames'); |  | ||||||
|             expect(response.body.count).toBe(1); |  | ||||||
|           }); |  | ||||||
| 
 |  | ||||||
|           it('$match for more element', async () => { |  | ||||||
|             const response = await agent.get('/users?filter[nicknames.$match]=a,aa,aaa'); |  | ||||||
|             expect(response.body.count).toBe(0); |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     describe('page', () => { |  | ||||||
|       it('default page and size(20) should be ok', async () => { |  | ||||||
|         const response = await agent.get('/posts?fields=title'); |  | ||||||
|         expect(response.body).toEqual({ |  | ||||||
|           count: 25, |  | ||||||
|           page: 1, |  | ||||||
|           per_page: 20, |  | ||||||
|           rows: Array(20).fill(null).map((_, index) => ({ title: `title${index}` })), |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       it('page 1 by size(1) should be ok', async () => { |  | ||||||
|         const response = await agent.get('/posts?fields=title&page=1&perPage=1'); |  | ||||||
|         expect(response.body).toEqual({ |  | ||||||
|           count: 25, |  | ||||||
|           page: 1, |  | ||||||
|           per_page: 1, |  | ||||||
|           rows: [{ title: 'title0' }], |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       it('page 2 by size(1) should be ok', async () => { |  | ||||||
|         const response = await agent.get('/posts?fields=title&page=2&per_page=1'); |  | ||||||
|         expect(response.body).toEqual({ |  | ||||||
|           count: 25, |  | ||||||
|           page: 2, |  | ||||||
|           per_page: 1, |  | ||||||
|           rows: [{ title: 'title1' }], |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       it('page 1 by size(101) should be change to 100', async () => { |  | ||||||
|         const response = await agent.get('/posts?fields=title&page=1&per_page=101'); |  | ||||||
|         expect(response.body).toEqual({ |  | ||||||
|           count: 25, |  | ||||||
|           page: 1, |  | ||||||
|           per_page: 100, |  | ||||||
|           rows: Array(25).fill(null).map((_, index) => ({ title: `title${index}` })), |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       it('page 2 by size(101) should be change to 100 and result is empty', async () => { |  | ||||||
|         const response = await agent.get('/posts?fields=title&page=2&per_page=101'); |  | ||||||
|         expect(response.body).toEqual({ |  | ||||||
|           count: 25, |  | ||||||
|           page: 2, |  | ||||||
|           per_page: 100, |  | ||||||
|           rows: [], |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       it('default page by size(-1) should be change to 100 and result will be 25 items', async () => { |  | ||||||
|         const response = await agent.get('/posts?fields=title&per_page=-1'); |  | ||||||
|         expect(response.body).toEqual({ |  | ||||||
|           count: 25, |  | ||||||
|           page: 1, |  | ||||||
|           per_page: 100, |  | ||||||
|           rows: Array(25).fill(null).map((_, index) => ({ title: `title${index}` })), |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       it('page 2 by size(-1) should be change to 100 and result is empty', async () => { |  | ||||||
|         const response = await agent.get('/posts?fields=title&page=2&per_page=-1'); |  | ||||||
|         expect(response.body).toEqual({ |  | ||||||
|           count: 25, |  | ||||||
|           page: 2, |  | ||||||
|           per_page: 100, |  | ||||||
|           rows: [], |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   describe('fields', () => { |   describe('fields', () => { | ||||||
|       it('custom field', async () => { |     it('fields', async () => { | ||||||
|         const response = await agent.get('/posts?fields=title&filter[customTitle]=title0'); |       const response = await api.resource('posts').list({ | ||||||
|  |         fields: ['title'], | ||||||
|  |         filter: { | ||||||
|  |           title: 't1', | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|       expect(response.body).toEqual({ |       expect(response.body).toEqual({ | ||||||
|         count: 1, |         count: 1, | ||||||
|  |         rows: [{ title: 't1' }], | ||||||
|         page: 1, |         page: 1, | ||||||
|         per_page: 20, |         per_page: 20, | ||||||
|           rows: [{ title: 'title0' }] |  | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|       it('self field and belongs to field', async () => { |     it('fields#appends', async () => { | ||||||
|         const response = await agent.get('/posts?fields=title,user.name&filter[title]=title0'); |       const response = await api.resource('posts').list({ | ||||||
|         expect(response.body).toEqual({ |         fields: { | ||||||
|           count: 1, |           appends: ['comments'], | ||||||
|           page: 1, |         }, | ||||||
|           per_page: 20, |         filter: { | ||||||
|           rows: [ |           title: 't1', | ||||||
|             { |         }, | ||||||
|               title: 'title0', |  | ||||||
|               user: { |  | ||||||
|                 name: 'a' |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           ] |  | ||||||
|       }); |       }); | ||||||
|       }); |       expect(response.body).toMatchObject({ | ||||||
| 
 |  | ||||||
|       // 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, |         count: 1, | ||||||
|         rows: [ |         rows: [ | ||||||
|           { |           { | ||||||
|               title: 'title0', |             title: 't1', | ||||||
|               user: { name: 'a' } |  | ||||||
|             } |  | ||||||
|           ] |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       it('except fields', async () => { |  | ||||||
|         const response = await agent.get('/posts?fields[except]=status&filter[title]=title0'); |  | ||||||
|         expect(response.body.rows[0].status).toBeUndefined(); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       it('only and except fields', async () => { |  | ||||||
|         const response = await agent.get('/posts?fields=title&fields[except]=status&filter[title]=title0'); |  | ||||||
|         expect(response.body.rows[0].status).toBeUndefined(); |  | ||||||
|         expect(response.body.rows).toEqual([{ title: 'title0' }]); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       it('only with belongs to fields', async () => { |  | ||||||
|         const response = await agent.get('/posts?fields[only]=title&fields[only]=user.name&filter[title]=title0'); |  | ||||||
|         expect(response.body.rows[0].user.name).toEqual('a'); |  | ||||||
|         expect(response.body.rows).toEqual([{ title: 'title0', user: { name: 'a' } }]); |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       it('appends fields', async () => { |  | ||||||
|         const response = await agent.get('/posts?fields[only]=title&fields[appends]=user.name&filter[title]=title0'); |  | ||||||
|         expect(response.body.rows[0].user.name).toEqual('a'); |  | ||||||
|         expect(response.body.rows).toEqual([{ |  | ||||||
|           title: 'title0', user: { id: 1, nicknames: ['aa', 'aaa'], name: 'a', ...timestampsStrings } |  | ||||||
|         }]); |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   describe('hasMany', () => { |  | ||||||
|     beforeEach(async () => { |  | ||||||
|       const User = db.getModel('users'); |  | ||||||
|       await User.bulkCreate([ |  | ||||||
|         { name: 'a' }, |  | ||||||
|         { name: 'b' }, |  | ||||||
|         { name: 'c' } |  | ||||||
|       ]); |  | ||||||
|       const users = await User.findAll(); |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const post = await Post.create({ user_id: users[0].id }); |  | ||||||
|       await post.updateAssociations({ |  | ||||||
|         comments: Array(6).fill(null).map((_, index) => ({ |  | ||||||
|           content: `content${index}`, |  | ||||||
|           status: index % 2 ? 'published' : 'draft', |  | ||||||
|           user_id: users[index % users.length].id |  | ||||||
|         })) |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('get comments of a post', async () => { |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const post = await Post.findByPk(1); |  | ||||||
|       const response = await agent |  | ||||||
|         .get(`/posts/${post.id}/comments?page=2&perPage=2&sort=content&fields=content&filter[published]=1`); |  | ||||||
|       expect(response.body).toEqual({ |  | ||||||
|         rows: [{ content: 'content5' }], |  | ||||||
|         count: 3, |  | ||||||
|         page: 2, |  | ||||||
|         per_page: 2 |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('get comments within a post, order by comments.content', async () => { |  | ||||||
|       const response = await agent |  | ||||||
|         .get('/posts?fields=title,comments.content&filter[comments.status]=draft&page=1&perPage=2&sort=-comments.content'); |  | ||||||
|       expect(response.body).toEqual({ |  | ||||||
|         rows: [{ |  | ||||||
|           title: null, |  | ||||||
|           comments: [{ content: 'content4' }, { content: 'content2' }, { content: 'content0' }] |  | ||||||
|         }], |  | ||||||
|         count: 1, |  | ||||||
|         page: 1, |  | ||||||
|         per_page: 2 |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('get comments of a post, and user of each comment', async () => { |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const post = await Post.findByPk(1); |  | ||||||
|       const response = await agent |  | ||||||
|         .get(`/posts/${post.id}/comments?fields=content,user.name&filter[status]=draft&sort=-content&page=1&perPage=2`); |  | ||||||
| 
 |  | ||||||
|       expect(response.body).toEqual({ |  | ||||||
|         count: 3, |  | ||||||
|         page: 1, |  | ||||||
|         per_page: 2, |  | ||||||
|         rows: [ |  | ||||||
|           { content: 'content4', user: { name: 'b' } }, |  | ||||||
|           { content: 'content2', user: { name: 'c' } } |  | ||||||
|         ] |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // TODO(bug)
 |  | ||||||
|     it.skip('get posts of user with comments', async () => { |  | ||||||
|       const response = await agent |  | ||||||
|         .get(`/users/1/posts?fields=comments.content,user.name&filter[comments.status]=draft&sort=-comments.content&page=1&perPage=2`); |  | ||||||
| 
 |  | ||||||
|       expect(response.body).toEqual({ |  | ||||||
|         count: 1, |  | ||||||
|         page: 1, |  | ||||||
|         per_page: 2, |  | ||||||
|         rows: [ |  | ||||||
|           { |  | ||||||
|             comments: [ |             comments: [ | ||||||
|               { content: 'content4' }, |               { | ||||||
|               { content: 'content2' } |                 content: 'c1', | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 content: 'c2', | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 content: 'c3', | ||||||
|  |               }, | ||||||
|             ], |             ], | ||||||
|             user: { name: 'a' } |           }, | ||||||
|  |         ], | ||||||
|  |         page: 1, | ||||||
|  |         per_page: 20, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('filter', () => { | ||||||
|  |     it('and', async () => { | ||||||
|  |       const response = await api.resource('posts').list({ | ||||||
|  |         filter: { | ||||||
|  |           and: [ | ||||||
|  |             { title: 't1' }, | ||||||
|  |             { status: 'draft' }, | ||||||
|  |           ], | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |       expect(response.body).toMatchObject({ | ||||||
|  |         count: 1, | ||||||
|  |         rows: [ | ||||||
|  |           { | ||||||
|  |             title: 't1', | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |         page: 1, | ||||||
|  |         per_page: 20 | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |     it('or', async () => { | ||||||
|  |       const response = await api.resource('posts').list({ | ||||||
|  |         filter: { | ||||||
|  |           or: [ | ||||||
|  |             { title: 't1' }, | ||||||
|  |             { title: 't2' }, | ||||||
|  |           ], | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |       expect(response.body).toMatchObject({ | ||||||
|  |         count: 2, | ||||||
|  |         rows: [ | ||||||
|  |           { | ||||||
|  |             title: 't1', | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             title: 't2', | ||||||
|           } |           } | ||||||
|         ] |         ], | ||||||
|       }); |         page: 1, | ||||||
|     }); |         per_page: 20 | ||||||
| 
 |       }); | ||||||
|     it('count field in hasMany', async () => { |  | ||||||
|       try { |  | ||||||
|         const response = await agent |  | ||||||
|           .get(`/users/1?fields=name,posts_count`); |  | ||||||
|         console.log(response.body); |  | ||||||
|       } catch (err) { |  | ||||||
|         console.error(err); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('count field in hasMany', async () => { |  | ||||||
|       try { |  | ||||||
|         const response = await agent |  | ||||||
|           .get(`/users/1/posts?fields=title,comments_count`); |  | ||||||
|         console.log(response.body); |  | ||||||
|       } catch (err) { |  | ||||||
|         console.error(err); |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   describe('belongsToMany', () => { |  | ||||||
|     beforeEach(async () => { |  | ||||||
|       const Tag = db.getModel('tags'); |  | ||||||
|       const tags = await Tag.bulkCreate([ |  | ||||||
|         { name: 'tag1', status: 'published' }, |  | ||||||
|         { name: 'tag2', status: 'draft' }, |  | ||||||
|         { name: 'tag3', status: 'published' }, |  | ||||||
|         { name: 'tag4', status: 'draft' }, |  | ||||||
|         { name: 'tag5', status: 'published' }, |  | ||||||
|         { name: 'tag6', status: 'draft' }, |  | ||||||
|         { name: 'tag7', status: 'published' }, |  | ||||||
|         { name: 'tag8', status: 'published' }, |  | ||||||
|         { name: 'tag9', status: 'draft' }, |  | ||||||
|         { name: 'tag10', status: 'published' }, |  | ||||||
|       ]); |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const [post1, post2] = await Post.bulkCreate([{}, {}]); |  | ||||||
|       await post1.updateAssociations({ |  | ||||||
|         tags: [1, 2, 3, 4, 5, 6, 7] |  | ||||||
|       }); |  | ||||||
|       await post2.updateAssociations({ |  | ||||||
|         tags: [2, 5, 8] |  | ||||||
|       }); |  | ||||||
|       const User = db.getModel('users'); |  | ||||||
|       const user = await User.create(); |  | ||||||
|       await user.updateAssociations({ |  | ||||||
|         posts: [post1] |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('list1', async () => { |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const post = await Post.findByPk(1); |  | ||||||
|       const response = await agent |  | ||||||
|         .get(`/posts/${post.id}/tags?page=2&perPage=2&sort=-name&fields=name&filter[status]=published`); |  | ||||||
|       expect(response.body).toEqual({ |  | ||||||
|         rows: [{ name: 'tag3' }, { name: 'tag1' }], |  | ||||||
|         count: 4, |  | ||||||
|         page: 2, |  | ||||||
|         per_page: 2 |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // TODO(bug): SQL 报错
 |  | ||||||
|     it.skip('list2', async () => { |  | ||||||
|       const response = await agent |  | ||||||
|         .get(`/users/1/posts?fields=tags`); |  | ||||||
|       console.log(response.body); |  | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -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', () => { | describe('remove', () => { | ||||||
|   let db; |   let api: MockServer; | ||||||
| 
 | 
 | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     db = await initDatabase(); |     api = mockServer({ | ||||||
|  |       dataWrapping: false, | ||||||
|  |     }); | ||||||
|  |     registerActions(api); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   afterAll(() => db.close()); |   afterEach(async () => { | ||||||
| 
 |     return api.destroy(); | ||||||
|   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(); |  | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   it('hasMany1', async () => { |   it('remove', 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('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', () => { | describe('set', () => { | ||||||
|   let db; |   let api: MockServer; | ||||||
| 
 | 
 | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     db = await initDatabase(); |     api = mockServer({ | ||||||
|  |       dataWrapping: false, | ||||||
|  |     }); | ||||||
|  |     registerActions(api); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   afterAll(() => db.close()); |   afterEach(async () => { | ||||||
| 
 |     return api.destroy(); | ||||||
|   it('belongsTo1', async () => { |  | ||||||
|     const Post = db.getModel('posts'); |  | ||||||
|     const User = db.getModel('users'); |  | ||||||
|     let post = await Post.create(); |  | ||||||
|     let user = await User.create(); |  | ||||||
|     await agent.post(`/posts/${post.id}/user:set/${user.id}`); |  | ||||||
|     post = await Post.findOne({ |  | ||||||
|       where: { |  | ||||||
|         id: post.id, |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|     const postUser = await post.getUser(); |  | ||||||
|     expect(user.id).toBe(postUser.id); |  | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   // TODO: 关系暂不关注,先注释了
 |   it('set', async () => { | ||||||
|   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); |  | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -1,302 +1,513 @@ | |||||||
| import { initDatabase, agent } from './index'; | import { mockServer, MockServer } from '@nocobase/test'; | ||||||
|  | import { registerActions } from '..'; | ||||||
| 
 | 
 | ||||||
| describe('get', () => { | describe('sort', () => { | ||||||
|   let db; | 
 | ||||||
|  |   describe('same scope', () => { | ||||||
|  |     let api: MockServer; | ||||||
| 
 | 
 | ||||||
|     beforeEach(async () => { |     beforeEach(async () => { | ||||||
|     db = await initDatabase(); |       api = mockServer({ | ||||||
|     const User = db.getModel('users'); |         dataWrapping: false, | ||||||
|     const users = await User.bulkCreate(Array.from('abcdefg').map(name => ({ name }))); |       }); | ||||||
| 
 |       registerActions(api); | ||||||
|     const Post = db.getModel('posts'); |       api.database.table({ | ||||||
|     const posts = await Post.bulkCreate(Array(10).fill(null).map((_, i) => ({ |         name: 'tests', | ||||||
|       title: `title_${i}`, |         fields: [ | ||||||
|       status: i % 2 ? 'publish' : 'draft', |           { type: 'string', name: 'title' }, | ||||||
|       user_id: users[i % users.length].id |           { type: 'sort', name: 'sort' }, | ||||||
|     }))); |           { type: 'sort', name: 'sort2' }, | ||||||
| 
 |         ], | ||||||
|     await posts.reduce((promise, post) => promise.then(() => post.updateAssociations({ |       }); | ||||||
|       comments: Array(post.sort % 5).fill(null).map((_, index) => ({ |       await api.database.sync(); | ||||||
|         content: `content_${index}`, |       const Test = api.database.getModel('tests'); | ||||||
|         status: index % 2 ? 'published' : 'draft', |       for (let index = 1; index < 5; index++) { | ||||||
|         user_id: users[index % users.length].id |         await Test.create({ title: `t${index}` }); | ||||||
|       })) |       } | ||||||
|     })), Promise.resolve()); |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|   afterAll(() => db.close()); |     afterEach(async () => { | ||||||
| 
 |       return api.destroy(); | ||||||
|   describe('sort value initialization', () => { |  | ||||||
|     it('initialization by bulkCreate', async () => { |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const posts = await Post.findAll({ |  | ||||||
|         order: [['id', 'ASC']] |  | ||||||
|       }); |  | ||||||
|       expect(posts.map(({ id, sort, sort_in_status, sort_in_user }) => ({ id, sort, sort_in_status, sort_in_user }))).toEqual([ |  | ||||||
|         { id: 1, sort: 1, sort_in_status: 1, sort_in_user: 1 }, |  | ||||||
|         { id: 2, sort: 2, sort_in_status: 1, sort_in_user: 1 }, |  | ||||||
|         { id: 3, sort: 3, sort_in_status: 2, sort_in_user: 1 }, |  | ||||||
|         { id: 4, sort: 4, sort_in_status: 2, sort_in_user: 1 }, |  | ||||||
|         { id: 5, sort: 5, sort_in_status: 3, sort_in_user: 1 }, |  | ||||||
|         { id: 6, sort: 6, sort_in_status: 3, sort_in_user: 1 }, |  | ||||||
|         { id: 7, sort: 7, sort_in_status: 4, sort_in_user: 1 }, |  | ||||||
|         { id: 8, sort: 8, sort_in_status: 4, sort_in_user: 2 }, |  | ||||||
|         { id: 9, sort: 9, sort_in_status: 5, sort_in_user: 2 }, |  | ||||||
|         { id: 10, sort: 10, sort_in_status: 5, sort_in_user: 2 } |  | ||||||
|       ]); |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     it('initialization by updateAssociations', async () => { |     it('targetId', async () => { | ||||||
|       const Comment = db.getModel('comments'); |       await api.resource('tests').sort({ | ||||||
|       const comments = await Comment.findAll({ |         sourceId: 1, | ||||||
|         order: [['id', 'ASC']] |         targetId: 3, | ||||||
|       }); |       }); | ||||||
|       expect(comments.map(({ id, sort, sort_in_status, sort_in_post }) => ({ id, sort, sort_in_status, sort_in_post }))).toEqual([ |       const response = await api.resource('tests').list({ | ||||||
|         { id: 1, sort: 1, sort_in_status: 1, sort_in_post: 1 }, |         sort: ['sort'], | ||||||
|         { 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 } |  | ||||||
|       ]); |  | ||||||
|       }); |       }); | ||||||
| 
 |       expect(response.body).toMatchObject({ | ||||||
|     it('sort value of append item', async () => { |         rows: [ | ||||||
|       const Post = db.getModel('posts'); |           { | ||||||
|       const post = await Post.create({ user_id: 1 }); |             title: 't2', | ||||||
|       expect(post.sort).toBe(11); |             sort: 1, | ||||||
|       expect(post.sort_in_status).toBe(6); |  | ||||||
|       expect(post.sort_in_user).toBe(3); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   describe('sort in whole table', () => { |  | ||||||
|     it('move id=1 to position at id=2', async () => { |  | ||||||
|       await agent |  | ||||||
|         .post('/posts:sort/1') |  | ||||||
|         .send({ |  | ||||||
|           field: 'sort', |  | ||||||
|           target: { id: 2 }, |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const posts = await Post.findAll({ |  | ||||||
|         attributes: ['id', 'sort'], |  | ||||||
|         order: [['id', 'ASC']] |  | ||||||
|       }); |  | ||||||
|       expect(posts.map(item => item.get())).toEqual([ |  | ||||||
|         { id: 1, sort: 2 }, |  | ||||||
|         { id: 2, sort: 1 }, |  | ||||||
|         { id: 3, sort: 3 }, |  | ||||||
|         { id: 4, sort: 4 }, |  | ||||||
|         { id: 5, sort: 5 }, |  | ||||||
|         { id: 6, sort: 6 }, |  | ||||||
|         { id: 7, sort: 7 }, |  | ||||||
|         { id: 8, sort: 8 }, |  | ||||||
|         { id: 9, sort: 9 }, |  | ||||||
|         { id: 10, sort: 10 } |  | ||||||
|       ]); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('move id=2 to position at id=1', async () => { |  | ||||||
|       await agent |  | ||||||
|         .post('/posts:sort/2') |  | ||||||
|         .send({ |  | ||||||
|           field: 'sort', |  | ||||||
|           target: { id: 1 }, |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const posts = await Post.findAll({ |  | ||||||
|         attributes: ['id', 'sort'], |  | ||||||
|         order: [['id', 'ASC']] |  | ||||||
|       }); |  | ||||||
|       expect(posts.map(item => item.get())).toEqual([ |  | ||||||
|         { id: 1, sort: 2 }, |  | ||||||
|         { id: 2, sort: 1 }, |  | ||||||
|         { id: 3, sort: 3 }, |  | ||||||
|         { id: 4, sort: 4 }, |  | ||||||
|         { id: 5, sort: 5 }, |  | ||||||
|         { id: 6, sort: 6 }, |  | ||||||
|         { id: 7, sort: 7 }, |  | ||||||
|         { id: 8, sort: 8 }, |  | ||||||
|         { id: 9, sort: 9 }, |  | ||||||
|         { id: 10, sort: 10 } |  | ||||||
|       ]); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('move id=1 to position at id=10', async () => { |  | ||||||
|       await agent |  | ||||||
|         .post('/posts:sort/1') |  | ||||||
|         .send({ |  | ||||||
|           field: 'sort', |  | ||||||
|           target: { id: 10 }, |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const posts = await Post.findAll({ |  | ||||||
|         attributes: ['id', 'sort'], |  | ||||||
|         order: [['id', 'ASC']] |  | ||||||
|       }); |  | ||||||
|       expect(posts.map(item => item.get())).toEqual([ |  | ||||||
|         { id: 1, sort: 10 }, |  | ||||||
|         { id: 2, sort: 1 }, |  | ||||||
|         { id: 3, sort: 2 }, |  | ||||||
|         { id: 4, sort: 3 }, |  | ||||||
|         { id: 5, sort: 4 }, |  | ||||||
|         { id: 6, sort: 5 }, |  | ||||||
|         { id: 7, sort: 6 }, |  | ||||||
|         { id: 8, sort: 7 }, |  | ||||||
|         { id: 9, sort: 8 }, |  | ||||||
|         { id: 10, sort: 9 } |  | ||||||
|       ]); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   describe('sort in filtered scope', () => { |  | ||||||
|     it('move id=2 to position at id=8 (same scope value)', async () => { |  | ||||||
|       await agent |  | ||||||
|         .post('/posts:sort/2') |  | ||||||
|         .send({ |  | ||||||
|           field: 'sort_in_status', |  | ||||||
|           target: { id: 8 }, |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const posts = await Post.findAll({ |  | ||||||
|         where: { |  | ||||||
|           status: 'publish' |  | ||||||
|           }, |           }, | ||||||
|         attributes: ['id', 'sort_in_status'], |           { | ||||||
|         order: [['id', 'ASC']] |             title: 't3', | ||||||
|       }); |             sort: 2, | ||||||
|       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']] |             title: 't1', | ||||||
|       }); |             sort: 3, | ||||||
|       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']] |             title: 't4', | ||||||
|       }); |             sort: 4, | ||||||
|       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', () => { |     it('targetId', async () => { | ||||||
|     describe('hasMany', () => { |       await api.resource('tests').sort({ | ||||||
|       it('move id=1 to position at id=3 (different scope value)', async () => { |         sourceId: 3, | ||||||
|         await agent |         targetId: 1, | ||||||
|           .post('/users/1/posts:sort/1') |  | ||||||
|           .send({ |  | ||||||
|             field: 'sort_in_user', |  | ||||||
|             target: { id: 3 }, |  | ||||||
|       }); |       }); | ||||||
| 
 |       const response = await api.resource('tests').list({ | ||||||
|         const Post = db.getModel('posts'); |         sort: ['sort'], | ||||||
|         const posts = await Post.findAll({ |       }); | ||||||
|           where: { |       expect(response.body).toMatchObject({ | ||||||
|             user_id: 3 |         rows: [ | ||||||
|  |           { | ||||||
|  |             title: 't3', | ||||||
|  |             sort: 1, | ||||||
|           }, |           }, | ||||||
|           attributes: ['id', 'sort_in_user'], |           { | ||||||
|           order: [['id', 'ASC']] |             title: 't1', | ||||||
|  |             sort: 2, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             title: 't2', | ||||||
|  |             sort: 3, | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             title: 't4', | ||||||
|  |             sort: 4, | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|         expect(posts.map(item => item.get())).toEqual([ |     it('sortField', async () => { | ||||||
|           { id: 1, sort_in_user: 1 }, |       await api.resource('tests').sort({ | ||||||
|           { id: 3, sort_in_user: 2 }, |         sortField: 'sort2', | ||||||
|           { id: 10, sort_in_user: 3 }, |         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', () => { | describe('update', () => { | ||||||
|   let db; |   let api: MockServer; | ||||||
| 
 | 
 | ||||||
|   beforeEach(async () => { |   beforeEach(async () => { | ||||||
|     db = await initDatabase(); |     api = mockServer({ | ||||||
|  |       dataWrapping: false, | ||||||
|     }); |     }); | ||||||
| 
 |     registerActions(api); | ||||||
|   afterAll(() => db.close()); |     api.database.table({ | ||||||
| 
 |       name: 'posts', | ||||||
|   describe('common', () => { |       fields: [ | ||||||
|     it('basic', async () => { |         { type: 'string', name: 'title' }, | ||||||
|       const Post = db.getModel('posts'); |         { type: 'hasMany', name: 'comments' }, | ||||||
|       const post = await Post.create(); |  | ||||||
|       const response = await agent |  | ||||||
|         .put(`/posts/${post.id}`).send({ |  | ||||||
|           title: 'title11112222' |  | ||||||
|         }); |  | ||||||
|       expect(response.body.title).toBe('title11112222'); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('update json field by replacing', async () => { |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const post = await Post.create({ meta: { a: 1, b: 'c', c: { d: false } } }); |  | ||||||
|       const updated = await agent |  | ||||||
|         .put(`/posts/${post.id}`).send({ |  | ||||||
|           meta: {} |  | ||||||
|         }); |  | ||||||
|       expect(updated.body.meta).toEqual({}); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it.skip('update json field by path based update', async () => { |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const post = await Post.create({ meta: { a: 1, b: 'c', c: { d: false } } }); |  | ||||||
|       const updated = await agent |  | ||||||
|         .put(`/posts/${post.id}?options[json]=merge`).send({ |  | ||||||
|           meta: { |  | ||||||
|             b: 'b', |  | ||||||
|             c: { d: true } |  | ||||||
|           } |  | ||||||
|         }); |  | ||||||
|       // console.log(updated.body);
 |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // TODO(question): json 字段的覆盖/合并策略
 |  | ||||||
|     it.skip('update with fields overwrite default values', async () => { |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const post = await Post.create(); |  | ||||||
|       const response = await agent |  | ||||||
|         .put(`/posts:update1/${post.id}`).send({ |  | ||||||
|           meta: { a: 1 }, |  | ||||||
|         }); |  | ||||||
|       expect(response.body.meta).toEqual({ a: 1 }); |  | ||||||
| 
 |  | ||||||
|       const result = await agent |  | ||||||
|         .get(`/posts/${post.id}`); |  | ||||||
| 
 |  | ||||||
|       expect(result.body.meta).toEqual({ a: 1 }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     // TODO(bug): action 的默认值处理时机不对
 |  | ||||||
|     it.skip('update with different fields to default values', async () => { |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const post = await Post.create({ |  | ||||||
|         meta: { location: 'Beijing' } |  | ||||||
|       }); |  | ||||||
|       const response = await agent |  | ||||||
|         .put(`/posts:update1/${post.id}`).send({ |  | ||||||
|           meta: { a: 1 }, |  | ||||||
|         }); |  | ||||||
|       expect(response.body.meta).toEqual({ location: 'Beijing', a: 1 }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('update with options.fields.expect in action', async () => { |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const post = await Post.create(); |  | ||||||
|       const response = await agent |  | ||||||
|         .put(`/posts:update1/${post.id}`).send({ |  | ||||||
|           title: 'title11112222', |  | ||||||
|         }); |  | ||||||
|       expect(response.body.title).toBe(null); |  | ||||||
|       expect(response.body.meta).toEqual({ |  | ||||||
|         location: 'Kunming' |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       const result = await agent |  | ||||||
|         .get(`/posts/${post.id}`); |  | ||||||
| 
 |  | ||||||
|       expect(result.body.title).toBe(null); |  | ||||||
|       expect(result.body.meta).toEqual({ |  | ||||||
|         location: 'Kunming' |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     it('update with options.fields.only in action', async () => { |  | ||||||
|       const Post = db.getModel('posts'); |  | ||||||
|       const post = await Post.create(); |  | ||||||
|       const response = await agent |  | ||||||
|         .put(`/posts:update2/${post.id}`).send({ |  | ||||||
|           title: 'title11112222', |  | ||||||
|           meta: { a: 1 } |  | ||||||
|         }); |  | ||||||
|       expect(response.body.title).toBe('title11112222'); |  | ||||||
|       expect(response.body.meta).toBe(null); |  | ||||||
| 
 |  | ||||||
|       const result = await agent |  | ||||||
|         .get(`/posts/${post.id}`); |  | ||||||
| 
 |  | ||||||
|       expect(result.body.title).toBe('title11112222'); |  | ||||||
|       expect(result.body.meta).toBe(null); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   it('hasOne', async () => { |  | ||||||
|     const User = db.getModel('users'); |  | ||||||
|     const user = await User.create(); |  | ||||||
|     await user.updateAssociations({ |  | ||||||
|       profile: { email: 'email1122' } |  | ||||||
|     }); |  | ||||||
|     const response = await agent |  | ||||||
|       .put(`/users/${user.id}/profile`).send({ |  | ||||||
|         email: 'email1111', |  | ||||||
|       }); |  | ||||||
|     expect(response.body.email).toEqual('email1111'); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   it('hasOne without exist target', async () => { |  | ||||||
|     const User = db.getModel('users'); |  | ||||||
|     const user = await User.create(); |  | ||||||
|     const response = await agent |  | ||||||
|       .put(`/users/${user.id}/profile`).send({ |  | ||||||
|         email: 'email1122', |  | ||||||
|       }); |  | ||||||
|     expect(response.body).toEqual({}); |  | ||||||
|   }); |  | ||||||
| 
 |  | ||||||
|   it('hasMany1', async () => { |  | ||||||
|     const Post = db.getModel('posts'); |  | ||||||
|     const post = await Post.create(); |  | ||||||
|     await post.updateAssociations({ |  | ||||||
|       comments: [ |  | ||||||
|         { content: 'content111222' }, |  | ||||||
|       ], |       ], | ||||||
|     }); |     }); | ||||||
|     const [comment] = await post.getComments(); |     api.database.table({ | ||||||
|     const response = await agent |       name: 'comments', | ||||||
|       .put(`/posts/${post.id}/comments/${comment.id}`).send({ content: 'content111222333' }); |       fields: [ | ||||||
|     expect(response.body.post_id).toBe(post.id); |         { type: 'string', name: 'content' }, | ||||||
|     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' }, |  | ||||||
|       ], |       ], | ||||||
|     }); |     }); | ||||||
|     const [tag] = await post.getTags(); |     await api.database.sync(); | ||||||
|     let response = await agent |   }); | ||||||
|       .post(`/posts/${post.id}/tags:update/${tag.id}`).send({ | 
 | ||||||
|         name: 'tag11223344', |   afterEach(async () => { | ||||||
|         posts_tags: { |     return api.destroy(); | ||||||
|           test: 'test1', |   }); | ||||||
|  | 
 | ||||||
|  |   it('update', async () => { | ||||||
|  |     const Post = api.database.getModel('posts'); | ||||||
|  |     const post = await Post.create(); | ||||||
|  |     await api.resource('posts').update({ | ||||||
|  |       resourceKey: post.id, | ||||||
|  |       values: { | ||||||
|  |         title: 't1', | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|     const [tag1] = await post.getTags(); |     const post2 = await Post.findByPk(post.id); | ||||||
|     expect(tag1.posts_tags.test).toBe('test1'); |     expect(post2.toJSON()).toMatchObject({ | ||||||
|     expect(response.body.name).toBe('tag11223344'); |       title: 't1', | ||||||
|     response = await agent |     }); | ||||||
|       .post(`/posts/${post.id}/tags:update/${tag.id}`).send({ |   }); | ||||||
|         posts_tags: { | 
 | ||||||
|           test: 'test112233', |   it('update associations', async () => { | ||||||
|  |     const [Post, Comment] = api.database.getModels(['posts', 'comments']); | ||||||
|  |     const post = await Post.create(); | ||||||
|  |     const comment = await Comment.create(); | ||||||
|  |     await post.updateAssociations({ | ||||||
|  |       comments: [comment] | ||||||
|  |     }); | ||||||
|  |     await api.resource('posts.comments').update({ | ||||||
|  |       resourceKey: comment.id, | ||||||
|  |       associatedKey: post.id, | ||||||
|  |       values: { | ||||||
|  |         content: 'c2', | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|     const [tag2] = await post.getTags(); |     const comment2 = await Comment.findByPk(comment.id); | ||||||
|     expect(tag2.posts_tags.test).toBe('test112233'); |     expect(comment2.toJSON()).toMatchObject({ | ||||||
|  |       content: 'c2', | ||||||
|  |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -1,4 +1,3 @@ | |||||||
| import { initDatabase, agent } from './index'; |  | ||||||
| import { filterByFields } from '../utils'; | import { filterByFields } from '../utils'; | ||||||
| 
 | 
 | ||||||
| describe('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'; | export * from './add'; | ||||||
| import Database from '@nocobase/database'; | export * from './create'; | ||||||
| import { Action } from '@nocobase/resourcer'; | export * from './destroy'; | ||||||
| 
 | export * from './get'; | ||||||
| export type Next = () => Promise<any>; | export * from './list'; | ||||||
| 
 | export * from './remove'; | ||||||
| export interface Context extends Koa.Context { | export * from './set'; | ||||||
|   db: Database; | export * from './sort'; | ||||||
|   action: Action; | export * from './toggle'; | ||||||
|   body: any; | export * from './update'; | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export { default as common } from './common'; |  | ||||||
| export { default as associate } from './associate'; |  | ||||||
|  | |||||||
							
								
								
									
										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 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 utils from './utils'; | ||||||
| export * as actions from './actions'; | export * as actions from './actions'; | ||||||
| export * as middlewares from './middlewares'; | 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; | export default actions; | ||||||
|  | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { Context, Next } from '../actions'; | import { Context, Next } from '..'; | ||||||
| import { Action } from '@nocobase/resourcer'; | import { Action } from '@nocobase/resourcer'; | ||||||
| import { HASONE, HASMANY, BELONGSTO, BELONGSTOMANY } from '@nocobase/database'; | 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 './associated'; | ||||||
| export * from './data-wrapping'; |  | ||||||
|  | |||||||
| @ -1,21 +1,59 @@ | |||||||
| import qs from 'qs'; | import qs from 'qs'; | ||||||
| import supertest from 'supertest'; | import supertest from 'supertest'; | ||||||
| import Application, { ApplicationOptions } from '@nocobase/server'; | import Application, { ApplicationOptions } from '@nocobase/server'; | ||||||
| import { ActionParams } from '@nocobase/resourcer'; |  | ||||||
| import { getConfig } from './mockDatabase'; | 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 { | interface Resource { | ||||||
|   get: (params?: ActionParams) => Promise<supertest.Response>; |   get: (params?: ActionParams) => Promise<supertest.Response>; | ||||||
|   list: (params?: ActionParams) => Promise<supertest.Response>; |   list: (params?: ActionParams) => Promise<supertest.Response>; | ||||||
|   create: (params?: ActionParams) => Promise<supertest.Response>; |   create: (params?: ActionParams) => Promise<supertest.Response>; | ||||||
|   update: (params?: ActionParams) => Promise<supertest.Response>; |   update: (params?: ActionParams) => Promise<supertest.Response>; | ||||||
|   destroy: (params?: ActionParams) => Promise<supertest.Response>; |   destroy: (params?: ActionParams) => Promise<supertest.Response>; | ||||||
|  |   sort: (params?: SortActionParams) => Promise<supertest.Response>; | ||||||
|   [name: string]: (params?: ActionParams) => Promise<supertest.Response>; |   [name: string]: (params?: ActionParams) => Promise<supertest.Response>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class MockServer extends Application { | export class MockServer extends Application { | ||||||
|  | 
 | ||||||
|  |   protected agentInstance: supertest.SuperAgentTest; | ||||||
|  | 
 | ||||||
|   agent() { |   agent() { | ||||||
|     return supertest.agent(this.callback()); |     if (!this.agentInstance) { | ||||||
|  |       this.agentInstance = supertest.agent(this.callback()); | ||||||
|  |     } | ||||||
|  |     return this.agentInstance; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   resource(name: string) { |   resource(name: string) { | ||||||
| @ -34,7 +72,7 @@ export class MockServer extends Application { | |||||||
|           } = params; |           } = params; | ||||||
|           let url = prefix; |           let url = prefix; | ||||||
|           if (keys.length > 1) { |           if (keys.length > 1) { | ||||||
|             url = `/${keys[0]}/${associatedKey}/${keys[1]}}` |             url = `/${keys[0]}/${associatedKey}/${keys[1]}` | ||||||
|           } else { |           } else { | ||||||
|             url = `/${name}`; |             url = `/${name}`; | ||||||
|           } |           } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user