mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-12-02 20:27:49 +08:00
feat: set field (#1237)
* feat: set field * feat: array field repository * feat: ArrayFieldRepository * fix: add set parse * test: array field repository * chore: update submodule * chore: field bind & unbind Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
0c80992f7d
commit
393ada2bc5
@ -25,6 +25,7 @@ describe('add action', () => {
|
||||
{ type: 'hasOne', name: 'profile' },
|
||||
{ type: 'belongsToMany', name: 'tags', through: 'posts_tags' },
|
||||
{ type: 'string', name: 'status', defaultValue: 'draft' },
|
||||
{ type: 'set', name: 'set_field' },
|
||||
],
|
||||
});
|
||||
|
||||
@ -51,6 +52,51 @@ describe('add action', () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
test('add values to set field', async () => {
|
||||
const p1 = await Post.repository.create({});
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts.set_field', p1.get('id'))
|
||||
.add({
|
||||
values: ['a', 'b'],
|
||||
});
|
||||
|
||||
expect(response.statusCode).toEqual(200);
|
||||
|
||||
const listResponse = await app.agent().resource('posts.set_field', p1.get('id')).list({
|
||||
paginate: false,
|
||||
});
|
||||
|
||||
expect(listResponse.status).toEqual(200);
|
||||
expect(listResponse.body).toEqual(['a', 'b']);
|
||||
|
||||
await app
|
||||
.agent()
|
||||
.resource('posts.set_field', p1.get('id'))
|
||||
.remove({
|
||||
values: ['b'],
|
||||
});
|
||||
|
||||
const listResponse1 = await app.agent().resource('posts.set_field', p1.get('id')).list({
|
||||
paginate: false,
|
||||
});
|
||||
|
||||
expect(listResponse1.body).toEqual(['a']);
|
||||
|
||||
await app
|
||||
.agent()
|
||||
.resource('posts.set_field', p1.get('id'))
|
||||
.set({
|
||||
values: ['b', 'c', 'd'],
|
||||
});
|
||||
|
||||
const listResponse2 = await app.agent().resource('posts.set_field', p1.get('id')).list({
|
||||
paginate: false,
|
||||
});
|
||||
|
||||
expect(listResponse2.body).toEqual(['b', 'c', 'd']);
|
||||
});
|
||||
|
||||
test('add belongs to many', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
|
@ -1,11 +1,22 @@
|
||||
import { Context } from '..';
|
||||
import { getRepositoryFromParams } from '../utils';
|
||||
import { BelongsToManyRepository, MultipleRelationRepository, HasManyRepository } from '@nocobase/database';
|
||||
import {
|
||||
BelongsToManyRepository,
|
||||
MultipleRelationRepository,
|
||||
HasManyRepository,
|
||||
ArrayFieldRepository,
|
||||
} from '@nocobase/database';
|
||||
|
||||
export async function add(ctx: Context, next) {
|
||||
const repository = getRepositoryFromParams(ctx);
|
||||
|
||||
if (!(repository instanceof MultipleRelationRepository || repository instanceof HasManyRepository)) {
|
||||
if (
|
||||
!(
|
||||
repository instanceof MultipleRelationRepository ||
|
||||
repository instanceof HasManyRepository ||
|
||||
repository instanceof ArrayFieldRepository
|
||||
)
|
||||
) {
|
||||
return await next();
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import path from 'path';
|
||||
import { Database, Model } from '..';
|
||||
import { ArrayFieldRepository } from '../field-repository/array-field-repository';
|
||||
import { mockDatabase } from './index';
|
||||
|
||||
describe('database', () => {
|
||||
@ -47,6 +48,15 @@ describe('database', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should get array field repository', async () => {
|
||||
db.collection({
|
||||
name: 'tests',
|
||||
fields: [{ type: 'set', name: 'set-field' }],
|
||||
});
|
||||
|
||||
expect(db.getRepository('tests.set-field', '1')).toBeInstanceOf(ArrayFieldRepository);
|
||||
});
|
||||
|
||||
test('import', async () => {
|
||||
await db.import({
|
||||
directory: path.resolve(__dirname, './fixtures/collections'),
|
||||
|
@ -0,0 +1,94 @@
|
||||
import { mockDatabase } from '../index';
|
||||
import Database from '../../database';
|
||||
import { ArrayFieldRepository } from '../../field-repository/array-field-repository';
|
||||
|
||||
describe('Array field repository', () => {
|
||||
let db: Database;
|
||||
|
||||
let TestCollection;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
TestCollection = db.collection({
|
||||
name: 'test',
|
||||
fields: [
|
||||
{
|
||||
type: 'set',
|
||||
name: 'set-field',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('should add item into fields', async () => {
|
||||
const a1 = await TestCollection.repository.create({});
|
||||
|
||||
const fieldRepository = new ArrayFieldRepository(TestCollection, 'set-field', a1.get('id'));
|
||||
await fieldRepository.add({
|
||||
values: 'a',
|
||||
});
|
||||
|
||||
expect(await fieldRepository.get()).toEqual(['a']);
|
||||
});
|
||||
|
||||
it('should remove item', async () => {
|
||||
const a1 = await TestCollection.repository.create({});
|
||||
|
||||
const fieldRepository = new ArrayFieldRepository(TestCollection, 'set-field', a1.get('id'));
|
||||
await fieldRepository.add({
|
||||
values: ['a', 'b', 'c'],
|
||||
});
|
||||
|
||||
expect(await fieldRepository.get()).toEqual(['a', 'b', 'c']);
|
||||
await fieldRepository.remove({
|
||||
values: ['c'],
|
||||
});
|
||||
|
||||
expect(await fieldRepository.get()).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('should set items', async () => {
|
||||
const a1 = await TestCollection.repository.create({});
|
||||
|
||||
const fieldRepository = new ArrayFieldRepository(TestCollection, 'set-field', a1.get('id'));
|
||||
await fieldRepository.add({
|
||||
values: ['a', 'b', 'c'],
|
||||
});
|
||||
|
||||
expect(await fieldRepository.get()).toEqual(['a', 'b', 'c']);
|
||||
await fieldRepository.set({
|
||||
values: ['d', 'e'],
|
||||
});
|
||||
|
||||
expect(await fieldRepository.get()).toEqual(['d', 'e']);
|
||||
});
|
||||
|
||||
it('should toggle item', async () => {
|
||||
const a1 = await TestCollection.repository.create({});
|
||||
|
||||
const fieldRepository = new ArrayFieldRepository(TestCollection, 'set-field', a1.get('id'));
|
||||
await fieldRepository.add({
|
||||
values: ['a', 'b', 'c'],
|
||||
});
|
||||
|
||||
expect(await fieldRepository.get()).toEqual(['a', 'b', 'c']);
|
||||
|
||||
await fieldRepository.toggle({
|
||||
value: 'c',
|
||||
});
|
||||
|
||||
expect(await fieldRepository.get()).toEqual(['a', 'b']);
|
||||
|
||||
await fieldRepository.toggle({
|
||||
value: 'c',
|
||||
});
|
||||
|
||||
expect(await fieldRepository.get()).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
});
|
37
packages/core/database/src/__tests__/fields/set.test.ts
Normal file
37
packages/core/database/src/__tests__/fields/set.test.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { mockDatabase } from '../';
|
||||
import { Database } from '../../database';
|
||||
|
||||
describe('set field', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('should set Set field', async () => {
|
||||
const A = db.collection({
|
||||
name: 'a',
|
||||
fields: [
|
||||
{
|
||||
type: 'set',
|
||||
name: 'set',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
const a = await A.repository.create({});
|
||||
|
||||
a.set('set', ['a', 'b', 'c', 'a']);
|
||||
|
||||
await a.save();
|
||||
|
||||
const setValue = a.get('set');
|
||||
expect(setValue).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
});
|
@ -15,13 +15,14 @@ import {
|
||||
Sequelize,
|
||||
SyncOptions,
|
||||
Transactionable,
|
||||
Utils
|
||||
Utils,
|
||||
} from 'sequelize';
|
||||
import { SequelizeStorage, Umzug } from 'umzug';
|
||||
import { Collection, CollectionOptions, RepositoryType } from './collection';
|
||||
import { ImporterReader, ImportFileExtension } from './collection-importer';
|
||||
import ReferencesMap from './features/ReferencesMap';
|
||||
import { referentialIntegrityCheck } from './features/referential-integrity-check';
|
||||
import { ArrayFieldRepository } from './field-repository/array-field-repository';
|
||||
import * as FieldTypes from './fields';
|
||||
import { Field, FieldContext, RelationField } from './fields';
|
||||
import { InheritedCollection } from './inherited-collection';
|
||||
@ -57,7 +58,7 @@ import {
|
||||
SyncListener,
|
||||
UpdateListener,
|
||||
UpdateWithAssociationsListener,
|
||||
ValidateListener
|
||||
ValidateListener,
|
||||
} from './types';
|
||||
|
||||
export interface MergeOptions extends merge.Options {}
|
||||
@ -375,6 +376,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
|
||||
getRepository<R extends Repository>(name: string): R;
|
||||
getRepository<R extends RelationRepository>(name: string, relationId: string | number): R;
|
||||
getRepository<R extends ArrayFieldRepository>(name: string, relationId: string | number): R;
|
||||
|
||||
getRepository<R extends RelationRepository>(name: string, relationId?: string | number): Repository | R {
|
||||
if (relationId) {
|
||||
|
@ -0,0 +1,127 @@
|
||||
import { Transactionable } from 'sequelize/types';
|
||||
import { Collection } from '../collection';
|
||||
import lodash from 'lodash';
|
||||
import { transactionWrapperBuilder } from '../decorators/transaction-decorator';
|
||||
import { ArrayField } from '../fields';
|
||||
|
||||
const transaction = transactionWrapperBuilder(function () {
|
||||
return this.collection.model.sequelize.transaction();
|
||||
});
|
||||
|
||||
export class ArrayFieldRepository {
|
||||
constructor(protected collection: Collection, protected fieldName: string, protected targetValue: string | number) {
|
||||
const field = collection.getField(fieldName);
|
||||
if (!(field instanceof ArrayField)) {
|
||||
throw new Error('Field must be of type Array');
|
||||
}
|
||||
}
|
||||
|
||||
@transaction()
|
||||
async get(options?: Transactionable) {
|
||||
const instance = await this.getInstance(options);
|
||||
return instance.get(this.fieldName);
|
||||
}
|
||||
|
||||
@transaction()
|
||||
async find(options?: Transactionable) {
|
||||
return await this.get(options);
|
||||
}
|
||||
|
||||
@transaction((args, transaction) => {
|
||||
return {
|
||||
values: args[0],
|
||||
transaction,
|
||||
};
|
||||
})
|
||||
async set(
|
||||
options: Transactionable & {
|
||||
values: Array<string | number> | string | number;
|
||||
},
|
||||
) {
|
||||
const { transaction } = options;
|
||||
|
||||
const instance = await this.getInstance({
|
||||
transaction,
|
||||
});
|
||||
|
||||
instance.set(this.fieldName, lodash.castArray(options.values));
|
||||
await instance.save({ transaction });
|
||||
}
|
||||
|
||||
@transaction((args, transaction) => {
|
||||
return {
|
||||
value: args[0],
|
||||
transaction,
|
||||
};
|
||||
})
|
||||
async toggle(
|
||||
options: Transactionable & {
|
||||
value: string | number;
|
||||
},
|
||||
) {
|
||||
const { transaction } = options;
|
||||
|
||||
const instance = await this.getInstance({
|
||||
transaction,
|
||||
});
|
||||
|
||||
const oldValue = instance.get(this.fieldName) || [];
|
||||
const newValue = oldValue.includes(options.value)
|
||||
? lodash.without(oldValue, options.value)
|
||||
: [...oldValue, options.value];
|
||||
instance.set(this.fieldName, newValue);
|
||||
await instance.save({ transaction });
|
||||
}
|
||||
|
||||
@transaction((args, transaction) => {
|
||||
return {
|
||||
values: args[0],
|
||||
transaction,
|
||||
};
|
||||
})
|
||||
async add(
|
||||
options: Transactionable & {
|
||||
values: Array<string | number> | string | number;
|
||||
},
|
||||
) {
|
||||
const { transaction } = options;
|
||||
|
||||
const instance = await this.getInstance({
|
||||
transaction,
|
||||
});
|
||||
|
||||
const oldValue = instance.get(this.fieldName) || [];
|
||||
|
||||
const newValue = [...oldValue, ...lodash.castArray(options.values)];
|
||||
instance.set(this.fieldName, newValue);
|
||||
await instance.save({ transaction });
|
||||
}
|
||||
|
||||
@transaction((args, transaction) => {
|
||||
return {
|
||||
values: args[0],
|
||||
transaction,
|
||||
};
|
||||
})
|
||||
async remove(
|
||||
options: Transactionable & {
|
||||
values: Array<string | number> | string | number;
|
||||
},
|
||||
) {
|
||||
const { transaction } = options;
|
||||
|
||||
const instance = await this.getInstance({
|
||||
transaction,
|
||||
});
|
||||
|
||||
const oldValue = instance.get(this.fieldName) || [];
|
||||
instance.set(this.fieldName, lodash.without(oldValue, ...lodash.castArray(options.values)));
|
||||
await instance.save({ transaction });
|
||||
}
|
||||
|
||||
protected getInstance(options: Transactionable) {
|
||||
return this.collection.repository.findOne({
|
||||
filterByTk: this.targetValue,
|
||||
});
|
||||
}
|
||||
}
|
@ -10,23 +10,23 @@ export class ArrayField extends Field {
|
||||
return DataTypes.JSON;
|
||||
}
|
||||
|
||||
sortValue(model) {
|
||||
sortValue = (model) => {
|
||||
const oldValue = model.get(this.options.name);
|
||||
|
||||
if (oldValue) {
|
||||
const newValue = oldValue.sort();
|
||||
model.set(this.options.name, newValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bind() {
|
||||
super.bind();
|
||||
this.on('beforeSave', this.sortValue.bind(this));
|
||||
this.on('beforeSave', this.sortValue);
|
||||
}
|
||||
|
||||
unbind() {
|
||||
super.unbind();
|
||||
this.off('beforeSave', this.sortValue.bind(this));
|
||||
this.off('beforeSave', this.sortValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
DoubleFieldOptions,
|
||||
FloatFieldOptions,
|
||||
IntegerFieldOptions,
|
||||
RealFieldOptions
|
||||
RealFieldOptions,
|
||||
} from './number-field';
|
||||
import { PasswordFieldOptions } from './password-field';
|
||||
import { RadioFieldOptions } from './radio-field';
|
||||
@ -26,8 +26,10 @@ import { UUIDFieldOptions } from './uuid-field';
|
||||
import { VirtualFieldOptions } from './virtual-field';
|
||||
import { FormulaFieldOptions } from './formula-field';
|
||||
import { SequenceFieldOptions } from './sequence-field';
|
||||
import { SetFieldOptions } from './set-field';
|
||||
|
||||
export * from './array-field';
|
||||
export * from './set-field';
|
||||
export * from './belongs-to-field';
|
||||
export * from './belongs-to-many-field';
|
||||
export * from './boolean-field';
|
||||
@ -68,6 +70,7 @@ export type FieldOptions =
|
||||
| VirtualFieldOptions
|
||||
| FormulaFieldOptions
|
||||
| ArrayFieldOptions
|
||||
| SetFieldOptions
|
||||
| TimeFieldOptions
|
||||
| DateFieldOptions
|
||||
| UidFieldOptions
|
||||
|
25
packages/core/database/src/fields/set-field.ts
Normal file
25
packages/core/database/src/fields/set-field.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { ArrayField } from './array-field';
|
||||
import { BaseColumnFieldOptions } from './field';
|
||||
|
||||
export interface SetFieldOptions extends BaseColumnFieldOptions {
|
||||
type: 'set';
|
||||
}
|
||||
|
||||
export class SetField extends ArrayField {
|
||||
beforeSave = (model) => {
|
||||
const oldValue = model.get(this.options.name);
|
||||
if (oldValue) {
|
||||
model.set(this.options.name, [...new Set(oldValue)]);
|
||||
}
|
||||
};
|
||||
|
||||
bind() {
|
||||
super.bind();
|
||||
this.on('beforeSave', this.beforeSave);
|
||||
}
|
||||
|
||||
unbind() {
|
||||
super.unbind();
|
||||
this.off('beforeSave', this.beforeSave);
|
||||
}
|
||||
}
|
@ -15,3 +15,4 @@ export * from './relation-repository/multiple-relation-repository';
|
||||
export * from './relation-repository/single-relation-repository';
|
||||
export * from './repository';
|
||||
export * from './update-associations';
|
||||
export * from './field-repository/array-field-repository';
|
||||
|
@ -10,14 +10,15 @@ import {
|
||||
ModelCtor,
|
||||
Op,
|
||||
Transactionable,
|
||||
UpdateOptions as SequelizeUpdateOptions
|
||||
UpdateOptions as SequelizeUpdateOptions,
|
||||
} from 'sequelize';
|
||||
import { WhereOperators } from 'sequelize/types/lib/model';
|
||||
import { Collection } from './collection';
|
||||
import { Database } from './database';
|
||||
import mustHaveFilter from './decorators/must-have-filter-decorator';
|
||||
import { transactionWrapperBuilder } from './decorators/transaction-decorator';
|
||||
import { RelationField } from './fields';
|
||||
import { ArrayFieldRepository } from './field-repository/array-field-repository';
|
||||
import { ArrayField, RelationField } from './fields';
|
||||
import FilterParser from './filter-parser';
|
||||
import { Model } from './model';
|
||||
import operators from './operators';
|
||||
@ -161,19 +162,29 @@ const transaction = transactionWrapperBuilder(function () {
|
||||
class RelationRepositoryBuilder<R extends RelationRepository> {
|
||||
collection: Collection;
|
||||
associationName: string;
|
||||
association: Association;
|
||||
association: Association | { associationType: string };
|
||||
|
||||
builderMap = {
|
||||
HasOne: HasOneRepository,
|
||||
BelongsTo: BelongsToRepository,
|
||||
BelongsToMany: BelongsToManyRepository,
|
||||
HasMany: HasManyRepository,
|
||||
ArrayField: ArrayFieldRepository,
|
||||
};
|
||||
|
||||
constructor(collection: Collection, associationName: string) {
|
||||
this.collection = collection;
|
||||
this.associationName = associationName;
|
||||
this.association = this.collection.model.associations[this.associationName];
|
||||
|
||||
if (!this.association) {
|
||||
const field = collection.getField(associationName);
|
||||
if (field && field instanceof ArrayField) {
|
||||
this.association = {
|
||||
associationType: 'ArrayField',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected builder() {
|
||||
|
@ -286,6 +286,7 @@ export class Resourcer {
|
||||
}
|
||||
try {
|
||||
const resource = this.getResource(getNameByParams(params));
|
||||
|
||||
// 为关系资源时,暂时需要再执行一遍 parseRequest
|
||||
if (resource.options.type && resource.options.type !== 'single') {
|
||||
params = parseRequest(
|
||||
@ -299,6 +300,7 @@ export class Resourcer {
|
||||
accessors: this.options.accessors || accessors,
|
||||
},
|
||||
);
|
||||
|
||||
if (!params) {
|
||||
return next();
|
||||
}
|
||||
|
@ -140,6 +140,13 @@ export function parseRequest(request: ParseRequest, options: ParseOptions = {}):
|
||||
delete: accessors.remove,
|
||||
},
|
||||
},
|
||||
set: {
|
||||
'/:associatedName/:associatedIndex/:resourceName': {
|
||||
get: accessors.list,
|
||||
post: accessors.add,
|
||||
delete: accessors.remove,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const params: ParsedParams = {};
|
||||
|
Loading…
Reference in New Issue
Block a user