feat: add filter and transaction for destroy action (#35)

* feat: add filter and transaction for destroy action

* fix: batch destroy in to-many relactionship
This commit is contained in:
Junyi 2020-12-08 21:20:30 +08:00 committed by GitHub
parent 0d3d30e0c2
commit 32a8483336
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 159 additions and 87 deletions

View File

@ -9,6 +9,7 @@ describe('destroy', () => {
afterAll(() => db.close()); afterAll(() => db.close());
describe('single', () => {
it('common1', async () => { it('common1', async () => {
const Post = db.getModel('posts'); const Post = db.getModel('posts');
const post = await Post.create(); const post = await Post.create();
@ -18,7 +19,29 @@ describe('destroy', () => {
expect(response.body).toBe(post.id); expect(response.body).toBe(post.id);
}); });
it('hasOne1', async () => { 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 = db.getModel('users');
const user = await User.create(); const user = await User.create();
await user.updateAssociations({ await user.updateAssociations({
@ -31,8 +54,10 @@ describe('destroy', () => {
const profile = await user.getProfile(); const profile = await user.getProfile();
expect(profile).toBeNull(); expect(profile).toBeNull();
}); });
});
it('hasMany1', async () => { describe('hasMany', () => {
it('delete single item in has-many list', async () => {
const Post = db.getModel('posts'); const Post = db.getModel('posts');
const post = await Post.create(); const post = await Post.create();
await post.updateAssociations({ await post.updateAssociations({
@ -40,14 +65,34 @@ describe('destroy', () => {
{content: 'content111222'}, {content: 'content111222'},
], ],
}); });
let [comment] = await post.getComments(); const [comment] = await post.getComments();
await agent await agent
.delete(`/posts/${post.id}/comments/${comment.id}`); .delete(`/posts/${post.id}/comments/${comment.id}`);
const count = await post.countComments(); const count = await post.countComments();
expect(count).toBe(0); expect(count).toBe(0);
}); });
it('belongsTo1', async () => { it('delete batch items in has-many list', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
comments: [
{ content: 'content1', status: 'published' },
{ content: 'content2', status: 'draft'},
{ content: 'content3', status: 'published' },
{ content: 'content4', status: 'draft'},
],
});
await agent
.delete(`/posts/${post.id}/comments?filter[status]=draft`);
const comments = await post.getComments();
expect(comments.length).toBe(2);
expect(comments.map(({ content }) => content)).toEqual(['content1', 'content3']);
});
});
describe('belongsTo', () => {
it('delete belongs-to item', async () => {
const Post = db.getModel('posts'); const Post = db.getModel('posts');
const post = await Post.create(); const post = await Post.create();
await post.updateAssociations({ await post.updateAssociations({
@ -57,8 +102,10 @@ describe('destroy', () => {
const user = await post.getUser(); const user = await post.getUser();
expect(user).toBeNull(); expect(user).toBeNull();
}); });
});
it('belongsToMany', async () => { describe('belongsToMany', () => {
it('delete single target item', async () => {
const Post = db.getModel('posts'); const Post = db.getModel('posts');
const post = await Post.create(); const post = await Post.create();
await post.updateAssociations({ await post.updateAssociations({
@ -71,5 +118,31 @@ describe('destroy', () => {
.delete(`/posts/${post.id}/tags:destroy/${tag.id}`); .delete(`/posts/${post.id}/tags:destroy/${tag.id}`);
const tags = await post.getTags(); const tags = await post.getTags();
expect(tags.length).toBe(0); 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);
});
}); });
}); });

View File

@ -144,19 +144,17 @@ export async function get(ctx: Context, next: Next) {
associated, associated,
resourceField, resourceField,
associatedName, associatedName,
} = ctx.action.params as { resourceName,
associated: Model, resourceKey,
associatedName: string, resourceKeyAttribute,
resourceField: Relation, fields = []
values: any, } = ctx.action.params;
};
if (associated && resourceField) { if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName); const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) { if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`); throw new Error(`${associatedName} associated model invalid`);
} }
const getAccessor = resourceField.getAccessors().get; const getAccessor = resourceField.getAccessors().get;
const { resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const TargetModel = ctx.db.getModel(resourceField.getTarget()); const TargetModel = ctx.db.getModel(resourceField.getTarget());
const options = TargetModel.parseApiJson({ const options = TargetModel.parseApiJson({
fields, fields,
@ -175,7 +173,6 @@ export async function get(ctx: Context, next: Next) {
ctx.body = model; ctx.body = model;
} }
} else { } else {
const { resourceName, resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const Model = ctx.db.getModel(resourceName); const Model = ctx.db.getModel(resourceName);
const options = Model.parseApiJson({ const options = Model.parseApiJson({
fields, fields,
@ -302,53 +299,52 @@ export async function destroy(ctx: Context, next: Next) {
associated, associated,
resourceField, resourceField,
associatedName, associatedName,
} = ctx.action.params as { resourceName,
associated: Model, resourceKey,
associatedName: string, resourceKeyAttribute,
resourceField: Relation, filter
values: any, } = ctx.action.params;
}; const transaction = await ctx.db.sequelize.transaction();
const commonOptions = { transaction, context: ctx };
if (associated && resourceField) { if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName); const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) { if (!(associated instanceof AssociatedModel)) {
await transaction.rollback();
throw new Error(`${associatedName} associated model invalid`); throw new Error(`${associatedName} associated model invalid`);
} }
const {get: getAccessor, remove: removeAccessor, set: setAccessor} = resourceField.getAccessors(); const {get: getAccessor, remove: removeAccessor, set: setAccessor} = resourceField.getAccessors();
const { resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const TargetModel = ctx.db.getModel(resourceField.getTarget()); const TargetModel = ctx.db.getModel(resourceField.getTarget());
const options = TargetModel.parseApiJson({ const { where } = TargetModel.parseApiJson({ filter, context: ctx });
fields,
});
if (resourceField instanceof HasOne || resourceField instanceof BelongsTo) { if (resourceField instanceof HasOne || resourceField instanceof BelongsTo) {
const model: Model = await associated[getAccessor]({ ...options, context: ctx }); const model: Model = await associated[getAccessor](commonOptions);
await associated[setAccessor](null); await associated[setAccessor](null, commonOptions);
ctx.body = await model.destroy(); // @ts-ignore
ctx.body = await model.destroy(commonOptions);
} else if (resourceField instanceof HasMany || resourceField instanceof BelongsToMany) { } else if (resourceField instanceof HasMany || resourceField instanceof BelongsToMany) {
const [model]: Model[] = await associated[getAccessor]({ const primaryKey = resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute;
...options, const models: Model[] = await associated[getAccessor]({
where: { where: resourceKey ? { [primaryKey]: resourceKey } : where,
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey, ...commonOptions
}, });
context: ctx, 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 { } else {
const { resourceName, resourceKey, resourceKeyAttribute, values } = ctx.action.params;
const Model = ctx.db.getModel(resourceName); 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({ const data = await Model.destroy({
where: { where: resourceKey ? { [primaryKey]: resourceKey } : where,
[resourceKeyAttribute || Model.primaryKeyAttribute]: {
[Op.in]: resourceKeys,
},
},
// @ts-ignore hooks 里添加 context // @ts-ignore hooks 里添加 context
context: ctx, ...commonOptions,
}); });
ctx.body = data; ctx.body = data;
} }
await transaction.commit();
await next(); await next();
} }

View File

@ -74,6 +74,7 @@ export function parseRequest(request: ParseRequest, options: ParseOptions = {}):
'/:resourceName': { '/:resourceName': {
get: accessors.list, get: accessors.list,
post: accessors.create, post: accessors.create,
delete: accessors.delete
}, },
'/:resourceName/:resourceKey': { '/:resourceName/:resourceKey': {
get: accessors.get, get: accessors.get,
@ -84,6 +85,7 @@ export function parseRequest(request: ParseRequest, options: ParseOptions = {}):
'/:associatedName/:associatedKey/:resourceName': { '/:associatedName/:associatedKey/:resourceName': {
get: accessors.list, get: accessors.list,
post: accessors.create, post: accessors.create,
delete: accessors.delete,
}, },
'/:associatedName/:associatedKey/:resourceName/:resourceKey': { '/:associatedName/:associatedKey/:resourceName/:resourceKey': {
get: accessors.get, get: accessors.get,
@ -106,6 +108,7 @@ export function parseRequest(request: ParseRequest, options: ParseOptions = {}):
'/:associatedName/:associatedKey/:resourceName': { '/:associatedName/:associatedKey/:resourceName': {
get: accessors.list, get: accessors.list,
post: accessors.create, post: accessors.create,
delete: accessors.delete,
}, },
'/:associatedName/:associatedKey/:resourceName/:resourceKey': { '/:associatedName/:associatedKey/:resourceName/:resourceKey': {
get: accessors.get, get: accessors.get,