diff --git a/packages/actions/src/__tests__/destroy.test.ts b/packages/actions/src/__tests__/destroy.test.ts index b639fc29f..aee4db3b6 100644 --- a/packages/actions/src/__tests__/destroy.test.ts +++ b/packages/actions/src/__tests__/destroy.test.ts @@ -9,67 +9,140 @@ describe('destroy', () => { afterAll(() => db.close()); - it('common1', async () => { - const Post = db.getModel('posts'); - const post = await Post.create(); - const response = await agent - .delete(`/posts/${post.id}`); - // console.log(response.body); - expect(response.body).toBe(post.id); + describe('single', () => { + it('common1', async () => { + const Post = db.getModel('posts'); + const post = await Post.create(); + const response = await agent + .delete(`/posts/${post.id}`); + // console.log(response.body); + expect(response.body).toBe(post.id); + }); + + 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'} + ]); + }); }); - it('hasOne1', async () => { - const User = db.getModel('users'); - const user = await User.create(); - await user.updateAssociations({ - profile: { - email: 'email1122', - } + 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(); }); - const response = await agent - .delete(`/users/${user.id}/profile`); - const profile = await user.getProfile(); - expect(profile).toBeNull(); }); - it('hasMany1', async () => { - const Post = db.getModel('posts'); - const post = await Post.create(); - await post.updateAssociations({ - comments: [ - {content: 'content111222'}, - ], + describe('hasMany', () => { + it('delete single item in has-many list', async () => { + const Post = db.getModel('posts'); + const post = await Post.create(); + await post.updateAssociations({ + comments: [ + {content: 'content111222'}, + ], + }); + const [comment] = await post.getComments(); + await agent + .delete(`/posts/${post.id}/comments/${comment.id}`); + const count = await post.countComments(); + expect(count).toBe(0); + }); + + it('delete batch items in has-many list', async () => { + const Post = db.getModel('posts'); + const post = await Post.create(); + await post.updateAssociations({ + comments: [ + { content: 'content1', status: 'published' }, + { content: 'content2', status: 'draft'}, + { content: 'content3', status: 'published' }, + { content: 'content4', status: 'draft'}, + ], + }); + await agent + .delete(`/posts/${post.id}/comments?filter[status]=draft`); + const comments = await post.getComments(); + expect(comments.length).toBe(2); + expect(comments.map(({ content }) => content)).toEqual(['content1', 'content3']); }); - let [comment] = await post.getComments(); - await agent - .delete(`/posts/${post.id}/comments/${comment.id}`); - const count = await post.countComments(); - expect(count).toBe(0); }); - it('belongsTo1', async () => { - const Post = db.getModel('posts'); - const post = await Post.create(); - await post.updateAssociations({ - user: {name: 'name121234'}, + describe('belongsTo', () => { + it('delete belongs-to item', async () => { + const Post = db.getModel('posts'); + const post = await Post.create(); + await post.updateAssociations({ + user: {name: 'name121234'}, + }); + await agent.delete(`/posts/${post.id}/user:destroy`); + const user = await post.getUser(); + expect(user).toBeNull(); }); - await agent.delete(`/posts/${post.id}/user:destroy`); - 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'}, - ], + 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); }); - 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); }); }); diff --git a/packages/actions/src/actions/common.ts b/packages/actions/src/actions/common.ts index d860c34f8..be19482f4 100644 --- a/packages/actions/src/actions/common.ts +++ b/packages/actions/src/actions/common.ts @@ -144,19 +144,17 @@ export async function get(ctx: Context, next: Next) { associated, resourceField, associatedName, - } = ctx.action.params as { - associated: Model, - associatedName: string, - resourceField: Relation, - values: any, - }; + 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 { resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params; const TargetModel = ctx.db.getModel(resourceField.getTarget()); const options = TargetModel.parseApiJson({ fields, @@ -175,7 +173,6 @@ export async function get(ctx: Context, next: Next) { ctx.body = model; } } else { - const { resourceName, resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params; const Model = ctx.db.getModel(resourceName); const options = Model.parseApiJson({ fields, @@ -302,53 +299,52 @@ export async function destroy(ctx: Context, next: Next) { associated, resourceField, associatedName, - } = ctx.action.params as { - associated: Model, - associatedName: string, - resourceField: Relation, - values: any, - }; + resourceName, + resourceKey, + resourceKeyAttribute, + filter + } = ctx.action.params; + const transaction = await ctx.db.sequelize.transaction(); + const commonOptions = { 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, 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, - }); + const { where } = TargetModel.parseApiJson({ filter, context: ctx }); if (resourceField instanceof HasOne || resourceField instanceof BelongsTo) { - const model: Model = await associated[getAccessor]({ ...options, context: ctx }); - await associated[setAccessor](null); - ctx.body = await model.destroy(); + const model: Model = await associated[getAccessor](commonOptions); + await associated[setAccessor](null, commonOptions); + // @ts-ignore + ctx.body = await model.destroy(commonOptions); } 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, + const primaryKey = resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute; + const models: Model[] = await associated[getAccessor]({ + where: resourceKey ? { [primaryKey]: resourceKey } : where, + ...commonOptions + }); + await associated[removeAccessor](models, commonOptions); + // @ts-ignore + ctx.body = await TargetModel.destroy({ + where: { [primaryKey]: { [Op.in]: models.map(item => item[primaryKey]) } }, + ...commonOptions }); - await associated[removeAccessor](model); - ctx.body = await model.destroy(); } } else { - const { resourceName, resourceKey, resourceKeyAttribute, values } = ctx.action.params; const Model = ctx.db.getModel(resourceName); - const resourceKeys = resourceKey ? resourceKey.split(',') : values[`${resourceKeyAttribute || Model.primaryKeyAttribute}s`]; + const { where } = Model.parseApiJson({ filter, context: ctx }); + const primaryKey = resourceKeyAttribute || Model.primaryKeyAttribute; const data = await Model.destroy({ - where: { - [resourceKeyAttribute || Model.primaryKeyAttribute]: { - [Op.in]: resourceKeys, - }, - }, + where: resourceKey ? { [primaryKey]: resourceKey } : where, // @ts-ignore hooks 里添加 context - context: ctx, + ...commonOptions, }); ctx.body = data; } + await transaction.commit(); await next(); } diff --git a/packages/resourcer/src/utils.ts b/packages/resourcer/src/utils.ts index dc9547671..30190abf3 100644 --- a/packages/resourcer/src/utils.ts +++ b/packages/resourcer/src/utils.ts @@ -74,6 +74,7 @@ export function parseRequest(request: ParseRequest, options: ParseOptions = {}): '/:resourceName': { get: accessors.list, post: accessors.create, + delete: accessors.delete }, '/:resourceName/:resourceKey': { get: accessors.get, @@ -84,6 +85,7 @@ export function parseRequest(request: ParseRequest, options: ParseOptions = {}): '/:associatedName/:associatedKey/:resourceName': { get: accessors.list, post: accessors.create, + delete: accessors.delete, }, '/:associatedName/:associatedKey/:resourceName/:resourceKey': { get: accessors.get, @@ -106,6 +108,7 @@ export function parseRequest(request: ParseRequest, options: ParseOptions = {}): '/:associatedName/:associatedKey/:resourceName': { get: accessors.list, post: accessors.create, + delete: accessors.delete, }, '/:associatedName/:associatedKey/:resourceName/:resourceKey': { get: accessors.get,