feat: update option must have filter or filterByTk (#847)

* feat: update option must have filter or filterByTk

* fix: typo

* fix: typo

(cherry picked from commit 83e6f93e1e3a188781530ec7492de1ed917a4dc1)

# Conflicts:
#	packages/plugins/acl/src/server.ts
#	packages/plugins/collection-manager/src/__tests__/field-options/indexes.test.ts
This commit is contained in:
ChengLei Shao 2022-10-10 15:34:00 +08:00 committed by chenos
parent ceed13d77e
commit 3082a7d6f8
14 changed files with 270 additions and 226 deletions

View File

@ -3,9 +3,9 @@ import { getRepositoryFromParams } from '../utils';
export async function update(ctx: Context, next) { export async function update(ctx: Context, next) {
const repository = getRepositoryFromParams(ctx); const repository = getRepositoryFromParams(ctx);
const { filterByTk, values, whitelist, blacklist, filter, updateAssociationValues } = ctx.action.params; const { forceUpdate, filterByTk, values, whitelist, blacklist, filter, updateAssociationValues } = ctx.action.params;
const instance = await repository.update({ ctx.body = await repository.update({
filterByTk, filterByTk,
values, values,
whitelist, whitelist,
@ -13,8 +13,8 @@ export async function update(ctx: Context, next) {
filter, filter,
updateAssociationValues, updateAssociationValues,
context: ctx, context: ctx,
forceUpdate,
}); });
ctx.body = instance;
await next(); await next();
} }

View File

@ -87,6 +87,7 @@ describe('has one repository', () => {
avatar: 'new_updated_avatar', avatar: 'new_updated_avatar',
}, },
}); });
expect((await UserProfileRepository.find())['avatar']).toEqual('new_updated_avatar'); expect((await UserProfileRepository.find())['avatar']).toEqual('new_updated_avatar');
await UserProfileRepository.destroy(); await UserProfileRepository.destroy();
expect(await UserProfileRepository.find()).toBeNull(); expect(await UserProfileRepository.find()).toBeNull();

View File

@ -57,4 +57,39 @@ describe('update', () => {
expect(p1.toJSON()['tags']).toEqual([]); expect(p1.toJSON()['tags']).toEqual([]);
}); });
it('should not update items without filter or filterByPk', async () => {
await db.getRepository('posts').create({
values: {
title: 'p1',
},
});
let error;
try {
await db.getRepository('posts').update({
values: {
title: 'p3',
},
});
} catch (e) {
error = e;
}
expect(error).not.toBeUndefined();
const p1 = await db.getRepository('posts').findOne();
expect(p1.toJSON()['title']).toEqual('p1');
await db.getRepository('posts').update({
values: {
title: 'p3',
},
filterByTk: p1.get('id') as number,
});
await p1.reload();
expect(p1.toJSON()['title']).toEqual('p3');
});
}); });

View File

@ -0,0 +1,17 @@
const mustHaveFilter = () => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const oldValue = descriptor.value;
descriptor.value = function () {
const options = arguments[0];
if (!options?.filter && !options?.filterByTk && !options?.forceUpdate) {
throw new Error(`must provide filter or filterByTk for ${propertyKey} call, or set forceUpdate to true`);
}
return oldValue.apply(this, arguments);
};
return descriptor;
};
export default mustHaveFilter;

View File

@ -9,7 +9,7 @@ import {
FindOptions, FindOptions,
TargetKey, TargetKey,
TK, TK,
UpdateOptions UpdateOptions,
} from '../repository'; } from '../repository';
import { updateModelByValues } from '../update-associations'; import { updateModelByValues } from '../update-associations';
import { UpdateGuard } from '../update-guard'; import { UpdateGuard } from '../update-guard';

View File

@ -7,7 +7,7 @@ import FilterParser from '../filter-parser';
import { Model } from '../model'; import { Model } from '../model';
import { OptionsParser } from '../options-parser'; import { OptionsParser } from '../options-parser';
import { CreateOptions, Filter, FindOptions } from '../repository'; import { CreateOptions, Filter, FindOptions } from '../repository';
import { transactionWrapperBuilder } from '../transaction-decorator'; import { transactionWrapperBuilder } from '../decorators/transaction-decorator';
import { updateAssociations } from '../update-associations'; import { updateAssociations } from '../update-associations';
import { UpdateGuard } from '../update-guard'; import { UpdateGuard } from '../update-guard';

View File

@ -13,6 +13,8 @@ import {
} from 'sequelize'; } from 'sequelize';
import { Collection } from './collection'; import { Collection } from './collection';
import { Database } from './database'; import { Database } from './database';
import mustHaveFilter from './decorators/must-have-filter-decorator';
import { transactionWrapperBuilder } from './decorators/transaction-decorator';
import { RelationField } from './fields'; import { RelationField } from './fields';
import FilterParser from './filter-parser'; import FilterParser from './filter-parser';
import { Model } from './model'; import { Model } from './model';
@ -22,7 +24,6 @@ import { BelongsToRepository } from './relation-repository/belongs-to-repository
import { HasManyRepository } from './relation-repository/hasmany-repository'; import { HasManyRepository } from './relation-repository/hasmany-repository';
import { HasOneRepository } from './relation-repository/hasone-repository'; import { HasOneRepository } from './relation-repository/hasone-repository';
import { RelationRepository } from './relation-repository/relation-repository'; import { RelationRepository } from './relation-repository/relation-repository';
import { transactionWrapperBuilder } from './transaction-decorator';
import { updateAssociations, updateModelByValues } from './update-associations'; import { updateAssociations, updateModelByValues } from './update-associations';
import { UpdateGuard } from './update-guard'; import { UpdateGuard } from './update-guard';
@ -345,7 +346,8 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
* @param options * @param options
*/ */
@transaction() @transaction()
async update(options: UpdateOptions) { @mustHaveFilter()
async update(options: UpdateOptions & { forceUpdate?: boolean }) {
const transaction = await this.getTransaction(options); const transaction = await this.getTransaction(options);
const guard = UpdateGuard.fromOptions(this.model, options); const guard = UpdateGuard.fromOptions(this.model, options);

View File

@ -26,14 +26,18 @@ describe('acl', () => {
const UserRepo = db.getCollection('users').repository; const UserRepo = db.getCollection('users').repository;
admin = await UserRepo.create({ admin = await UserRepo.create({
values: { values: {
roles: ['admin'] roles: ['admin'],
} },
}); });
const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin; const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin;
adminAgent = app.agent().auth(userPlugin.jwtService.sign({
adminAgent = app.agent().auth(
userPlugin.jwtService.sign({
userId: admin.get('id'), userId: admin.get('id'),
}), { type: 'bearer' }); }),
{ type: 'bearer' },
);
uiSchemaRepository = db.getRepository('uiSchemas'); uiSchemaRepository = db.getRepository('uiSchemas');
}); });
@ -41,7 +45,7 @@ describe('acl', () => {
it('should works with universal actions', async () => { it('should works with universal actions', async () => {
await db.getRepository('roles').create({ await db.getRepository('roles').create({
values: { values: {
name: 'new' name: 'new',
}, },
}); });
@ -54,15 +58,14 @@ describe('acl', () => {
).toBeNull(); ).toBeNull();
// grant universal action // grant universal action
await adminAgent await adminAgent.resource('roles').update({
.resource('roles')
.update({
resourceIndex: 'new', resourceIndex: 'new',
values: { values: {
strategy: { strategy: {
actions: ['create'], actions: ['create'],
}, },
}, },
forceUpdate: true,
}); });
expect( expect(
@ -104,9 +107,7 @@ describe('acl', () => {
}, },
}); });
await adminAgent await adminAgent.resource('roles.resources', 'new').create({
.resource('roles.resources', 'new')
.create({
values: { values: {
name: 'c1', name: 'c1',
usingActionsConfig: true, usingActionsConfig: true,
@ -150,9 +151,9 @@ describe('acl', () => {
}); });
// create c1 published scope // create c1 published scope
const { body: { data: publishedScope } } = await adminAgent const {
.resource('rolesResourcesScopes') body: { data: publishedScope },
.create({ } = await adminAgent.resource('rolesResourcesScopes').create({
values: { values: {
resourceName: 'c1', resourceName: 'c1',
name: 'published', name: 'published',
@ -165,9 +166,7 @@ describe('acl', () => {
// await db.getRepository('rolesResourcesScopes').findOne(); // await db.getRepository('rolesResourcesScopes').findOne();
// set admin resources // set admin resources
await adminAgent await adminAgent.resource('roles.resources', 'new').create({
.resource('roles.resources', 'new')
.create({
values: { values: {
name: 'c1', name: 'c1',
usingActionsConfig: true, usingActionsConfig: true,
@ -215,9 +214,7 @@ describe('acl', () => {
}); });
// revoke action // revoke action
const response = await adminAgent const response = await adminAgent.resource('roles.resources', role.get('name')).list({
.resource('roles.resources', role.get('name'))
.list({
appends: ['actions'], appends: ['actions'],
}); });
@ -226,9 +223,7 @@ describe('acl', () => {
const actions = response.body.data[0].actions; const actions = response.body.data[0].actions;
const collectionName = response.body.data[0].name; const collectionName = response.body.data[0].name;
await adminAgent await adminAgent.resource('roles.resources', role.get('name')).update({
.resource('roles.resources', role.get('name'))
.update({
filterByTk: collectionName, filterByTk: collectionName,
values: { values: {
name: 'c1', name: 'c1',
@ -254,7 +249,7 @@ describe('acl', () => {
it('should revoke resource when collection destroy', async () => { it('should revoke resource when collection destroy', async () => {
await db.getRepository('roles').create({ await db.getRepository('roles').create({
values: { values: {
name: 'new' name: 'new',
}, },
}); });
@ -272,9 +267,7 @@ describe('acl', () => {
}, },
}); });
await adminAgent await adminAgent.resource('roles.resources').create({
.resource('roles.resources')
.create({
associatedIndex: 'new', associatedIndex: 'new',
values: { values: {
name: 'posts', name: 'posts',
@ -314,7 +307,7 @@ describe('acl', () => {
it('should revoke actions when not using actions config', async () => { it('should revoke actions when not using actions config', async () => {
await db.getRepository('roles').create({ await db.getRepository('roles').create({
values: { values: {
name: 'new' name: 'new',
}, },
}); });
@ -325,9 +318,7 @@ describe('acl', () => {
}, },
}); });
await adminAgent await adminAgent.resource('roles.resources').create({
.resource('roles.resources')
.create({
associatedIndex: 'new', associatedIndex: 'new',
values: { values: {
name: 'posts', name: 'posts',
@ -352,9 +343,7 @@ describe('acl', () => {
action: 'create', action: 'create',
}); });
await adminAgent await adminAgent.resource('roles.resources', 'new').update({
.resource('roles.resources', 'new')
.update({
filterByTk: ( filterByTk: (
await db.getRepository('rolesResources').findOne({ await db.getRepository('rolesResources').findOne({
filter: { filter: {
@ -376,9 +365,7 @@ describe('acl', () => {
}), }),
).toBeNull(); ).toBeNull();
await adminAgent await adminAgent.resource('roles.resources', 'new').update({
.resource('roles.resources', 'new')
.update({
filterByTk: ( filterByTk: (
await db.getRepository('rolesResources').findOne({ await db.getRepository('rolesResources').findOne({
filter: { filter: {
@ -408,7 +395,7 @@ describe('acl', () => {
it('should add fields when field created', async () => { it('should add fields when field created', async () => {
await db.getRepository('roles').create({ await db.getRepository('roles').create({
values: { values: {
name: 'new' name: 'new',
}, },
}); });
@ -426,9 +413,7 @@ describe('acl', () => {
}, },
}); });
await adminAgent await adminAgent.resource('roles.resources').create({
.resource('roles.resources')
.create({
associatedIndex: 'new', associatedIndex: 'new',
values: { values: {
name: 'posts', name: 'posts',
@ -494,14 +479,17 @@ describe('acl', () => {
const UserRepo = db.getCollection('users').repository; const UserRepo = db.getCollection('users').repository;
const user = await UserRepo.create({ const user = await UserRepo.create({
values: { values: {
roles: ['new'] roles: ['new'],
} },
}); });
const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin; const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin;
const userAgent = app.agent().auth(userPlugin.jwtService.sign({ const userAgent = app.agent().auth(
userPlugin.jwtService.sign({
userId: user.get('id'), userId: user.get('id'),
}), { type: 'bearer' }); }),
{ type: 'bearer' },
);
const schema = { const schema = {
'x-uid': 'test', 'x-uid': 'test',

View File

@ -17,7 +17,6 @@ export async function prepareApp() {
app.plugin(PluginUiSchema); app.plugin(PluginUiSchema);
app.plugin(PluginErrorHandler); app.plugin(PluginErrorHandler);
app.plugin(PluginCollectionManager); app.plugin(PluginCollectionManager);
app.plugin(PluginACL); app.plugin(PluginACL);
await app.loadAndInstall(); await app.loadAndInstall();

View File

@ -32,14 +32,17 @@ describe('role api', () => {
const UserRepo = db.getCollection('users').repository; const UserRepo = db.getCollection('users').repository;
admin = await UserRepo.create({ admin = await UserRepo.create({
values: { values: {
roles: ['admin'] roles: ['admin'],
} },
}); });
const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin; const userPlugin = app.getPlugin('@nocobase/plugin-users') as UsersPlugin;
adminAgent = app.agent().auth(userPlugin.jwtService.sign({ adminAgent = app.agent().auth(
userPlugin.jwtService.sign({
userId: admin.get('id'), userId: admin.get('id'),
}), { type: 'bearer' }); }),
{ type: 'bearer' },
);
}); });
it('should list actions', async () => { it('should list actions', async () => {
@ -49,9 +52,8 @@ describe('role api', () => {
it('should grant universal role actions', async () => { it('should grant universal role actions', async () => {
// grant role actions // grant role actions
const response = await adminAgent const response = await adminAgent.resource('roles').update({
.resource('roles') forceUpdate: true,
.update({
values: { values: {
strategy: { strategy: {
actions: ['create:all', 'view:own'], actions: ['create:all', 'view:own'],

View File

@ -416,10 +416,12 @@ export class PluginACL extends Plugin {
} }
const User = this.db.getCollection('users'); const User = this.db.getCollection('users');
await User.repository.update({ await User.repository.update({
values: { values: {
roles: ['root', 'admin', 'member'], roles: ['root', 'admin', 'member'],
}, },
forceUpdate: true,
}); });
const RolesUsers = this.db.getCollection('rolesUsers'); const RolesUsers = this.db.getCollection('rolesUsers');

View File

@ -8,9 +8,7 @@ describe('field indexes', () => {
beforeEach(async () => { beforeEach(async () => {
app = await createApp(); app = await createApp();
agent = app.agent(); agent = app.agent();
await agent await agent.resource('collections').create({
.resource('collections')
.create({
values: { values: {
name: 'test1', name: 'test1',
}, },
@ -21,60 +19,59 @@ describe('field indexes', () => {
await app.destroy(); await app.destroy();
}); });
it.only('create unique constraint after added dulplicated records', async () => { it('create unique constraint after added duplicated records', async () => {
const tableName = 'test1'; const tableName = 'test1';
// create an field with unique constraint // create an field with unique constraint
const field = await agent const field = await agent.resource('collections.fields', tableName).create({
.resource('collections.fields', tableName)
.create({
values: { values: {
name: 'title', name: 'title',
type: 'string' type: 'string',
}, },
}); });
// create a record // create a record
const response1 = await agent.resource(tableName).create({ const response1 = await agent.resource(tableName).create({
values: { title: 't1' } values: { title: 't1' },
}); });
// create another same record // create another same record
const response2 = await agent.resource(tableName).create({ const response2 = await agent.resource(tableName).create({
values: { title: 't1' } values: { title: 't1' },
}); });
const response3 = await agent.resource('fields').update({ const response3 = await agent.resource('fields').update({
filterByTk: field.id, filterByTk: field.body.data.key,
values: { values: {
unique: true unique: true,
} },
}); });
expect(response3.status).toBe(400); expect(response3.status).toBe(400);
const response4 = await agent.resource(tableName).create({ const response4 = await agent.resource(tableName).create({
values: { title: 't1' } values: { title: 't1' },
}); });
expect(response4.status).toBe(200); expect(response4.status).toBe(200);
expect(response4.body.data.title).toBe('t1'); expect(response4.body.data.title).toBe('t1');
}); });
it('field value cannot be duplicated with unique index', async () => { it('field value cannot be duplicated with unique index', async () => {
const tableName = 'test1'; const tableName = 'test1';
// create an field with unique constraint // create a field with unique constraint
const field = await agent const field = await agent.resource('collections.fields', tableName).create({
.resource('collections.fields', tableName)
.create({
values: { values: {
name: 'title', name: 'title',
type: 'string', type: 'string',
unique: true unique: true,
}, },
}); });
// create a record // create a record
const response1 = await agent.resource(tableName).create({ const response1 = await agent.resource(tableName).create({
values: { values: {
title: 't1' title: 't1',
} },
}); });
expect(response1.status).toBe(200); expect(response1.status).toBe(200);
expect(response1.body.data.title).toBe('t1'); expect(response1.body.data.title).toBe('t1');
@ -82,48 +79,49 @@ describe('field indexes', () => {
// create another record with the same value on unique field should fail // create another record with the same value on unique field should fail
const response2 = await agent.resource(tableName).create({ const response2 = await agent.resource(tableName).create({
values: { values: {
title: 't1' title: 't1',
} },
}); });
expect(response2.status).toBe(400); expect(response2.status).toBe(400);
// update field to remove unique constraint // update field to remove unique constraint
await agent.resource('fields').update({ await agent.resource('fields').update({
filterByTk: field.id, filterByTk: field.body.data.key,
values: { values: {
unique: false unique: false,
} },
}); });
// create another record with the same value on unique field should be ok // create another record with the same value on unique field should be ok
const response3 = await agent.resource(tableName).create({ const response3 = await agent.resource(tableName).create({
values: { values: {
title: 't1' title: 't1',
} },
}); });
expect(response3.status).toBe(200); expect(response3.status).toBe(200);
expect(response3.body.data.title).toBe('t1'); expect(response3.body.data.title).toBe('t1');
// update field to add unique constraint should fail because of duplicated records // update field to add unique constraint should fail because of duplicated records
const response4 = await agent.resource('fields').update({ const response4 = await agent.resource('fields').update({
filterByTk: field.id, filterByTk: field.body.data.key,
values: { values: {
unique: true unique: true,
} },
}); });
expect(response4.status).toBe(400); expect(response4.status).toBe(400);
// remove a duplicated record // remove a duplicated record
await agent.resource(tableName).destroy({ await agent.resource(tableName).destroy({
filterByTk: response3.body.data.id filterByTk: response3.body.data.id,
}); });
// update field to add unique constraint should be ok // update field to add unique constraint should be ok
const response6 = await agent.resource('fields').update({ const response6 = await agent.resource('fields').update({
filterByTk: field.id, filterByTk: field.body.data.key,
values: { values: {
unique: true unique: true,
} },
}); });
expect(response6.status).toBe(200); expect(response6.status).toBe(200);
}); });

View File

@ -11,7 +11,7 @@ import {
afterCreateForReverseField, afterCreateForReverseField,
beforeCreateForChildrenCollection, beforeCreateForChildrenCollection,
beforeCreateForReverseField, beforeCreateForReverseField,
beforeInitOptions beforeInitOptions,
} from './hooks'; } from './hooks';
import { CollectionModel, FieldModel } from './models'; import { CollectionModel, FieldModel } from './models';
@ -67,7 +67,7 @@ export class CollectionManagerPlugin extends Plugin {
if (context) { if (context) {
await model.migrate({ await model.migrate({
isNew: true, isNew: true,
transaction transaction,
}); });
} }
}); });
@ -76,7 +76,7 @@ export class CollectionManagerPlugin extends Plugin {
if (context) { if (context) {
await model.migrate({ await model.migrate({
isNew: true, isNew: true,
transaction transaction,
}); });
} }
}); });