diff --git a/packages/database/src/__tests__/fields/context-field.test.ts b/packages/database/src/__tests__/fields/context-field.test.ts new file mode 100644 index 000000000..26aa95b8c --- /dev/null +++ b/packages/database/src/__tests__/fields/context-field.test.ts @@ -0,0 +1,140 @@ +import { DataTypes } from 'sequelize'; +import { mockDatabase } from '../'; +import { Database } from '../../'; + +describe('context field', () => { + let db: Database; + + beforeEach(() => { + db = mockDatabase(); + }); + + afterEach(async () => { + await db.close(); + }); + + describe('dataType', () => { + it('case 1, string', async () => { + const Test = db.collection({ + name: 'tests', + fields: [ + { + type: 'context', + name: 'clientIp', + dataIndex: 'request.ip', + dataType: 'string', + }, + ], + }); + const attribute = Test.model.rawAttributes['clientIp']; + expect(attribute.type).toBeInstanceOf(DataTypes.STRING); + }); + + it('case 2, integer', async () => { + const Test = db.collection({ + name: 'tests', + fields: [ + { + type: 'context', + name: 'userId', + dataIndex: 'state.currentUser.id', + dataType: 'integer', + }, + ], + }); + const attribute = Test.model.rawAttributes['userId']; + expect(attribute.type).toBeInstanceOf(DataTypes.INTEGER); + }); + + it('case 3, json', async () => { + const Test = db.collection({ + name: 'tests', + fields: [ + { + type: 'context', + name: 'ua', + dataIndex: 'userAgent', + dataType: 'json', + }, + ], + }); + const attribute = Test.model.rawAttributes['ua']; + expect(attribute.type).toBeInstanceOf(DataTypes.JSON); + }); + }); + + describe('create and update', () => { + it('case 1', async () => { + const Test = db.collection({ + name: 'tests', + fields: [ + { + type: 'context', + name: 'clientIp', + dataIndex: 'request.ip', + dataType: 'string', + }, + ], + }); + await db.sync(); + const t1 = await Test.repository.create({ + values: {}, + context: { + request: { + ip: '11.22.33.44', + }, + }, + }); + expect(t1.get('clientIp')).toBe('11.22.33.44'); + const [t2] = await Test.repository.update({ + filterByPk: t1.get('id') as any, + values: {}, + context: { + request: { + ip: '11.22.33.55', + }, + }, + }); + expect(t2.get('clientIp')).toBe('11.22.33.55'); + const t3 = await Test.repository.findOne(); + expect(t3.get('clientIp')).toBe('11.22.33.55'); + }); + + it('case 2, createOnly = true', async () => { + const Test = db.collection({ + name: 'tests', + fields: [ + { + type: 'context', + name: 'clientIp', + dataIndex: 'request.ip', + dataType: 'string', + createOnly: true, + }, + ], + }); + await db.sync(); + const t1 = await Test.repository.create({ + values: {}, + context: { + request: { + ip: '11.22.33.44', + }, + }, + }); + expect(t1.get('clientIp')).toBe('11.22.33.44'); + const [t2] = await Test.repository.update({ + filterByPk: t1.get('id') as any, + values: {}, + context: { + request: { + ip: '11.22.33.55', + }, + }, + }); + expect(t2.get('clientIp')).toBe('11.22.33.44'); + const t3 = await Test.repository.findOne(); + expect(t3.get('clientIp')).toBe('11.22.33.44'); + }); + }); +}); diff --git a/packages/database/src/fields/context-field.ts b/packages/database/src/fields/context-field.ts new file mode 100644 index 000000000..24202d8fb --- /dev/null +++ b/packages/database/src/fields/context-field.ts @@ -0,0 +1,44 @@ +import { lodash } from '@umijs/utils'; +import { DataTypes, Model } from 'sequelize'; +import { BaseColumnFieldOptions, Field } from './field'; + +export class ContextField extends Field { + get dataType() { + const type: string = this.options.dataType || 'string'; + return DataTypes[type.toUpperCase()] || DataTypes.STRING; + } + + init() { + const { name, dataIndex } = this.options; + this.listener = async (model: Model, options) => { + const { context } = options; + model.set(name, lodash.get(context, dataIndex)); + model.changed(name, true); + }; + } + + bind() { + super.bind(); + const { createOnly } = this.options; + this.on('beforeCreate', this.listener); + if (!createOnly) { + this.on('beforeUpdate', this.listener); + } + } + + unbind() { + super.unbind(); + const { createOnly } = this.options; + this.off('beforeCreate', this.listener); + if (!createOnly) { + this.off('beforeUpdate', this.listener); + } + } +} + +export interface ContextFieldOptions extends BaseColumnFieldOptions { + type: 'context'; + dataIndex: string; + dataType?: string; + createOnly?: boolean; +} diff --git a/packages/database/src/fields/index.ts b/packages/database/src/fields/index.ts index dc4272969..27c54f614 100644 --- a/packages/database/src/fields/index.ts +++ b/packages/database/src/fields/index.ts @@ -21,6 +21,7 @@ import { DateFieldOptions } from './date-field'; import { ArrayFieldOptions } from './array-field'; import { BaseFieldOptions } from './field'; import { PasswordFieldOptions } from './password-field'; +import { ContextFieldOptions } from './context-field'; export * from './array-field'; export * from './belongs-to-field'; @@ -39,6 +40,7 @@ export * from './time-field'; export * from './uid-field'; export * from './virtual-field'; export * from './password-field'; +export * from './context-field'; export * from './field'; export type FieldOptions = @@ -59,6 +61,7 @@ export type FieldOptions = | TimeFieldOptions | DateFieldOptions | PasswordFieldOptions + | ContextFieldOptions | BelongsToFieldOptions | HasOneFieldOptions | HasManyFieldOptions diff --git a/packages/database/src/relation-repository/belongs-to-many-repository.ts b/packages/database/src/relation-repository/belongs-to-many-repository.ts index 343353f35..969019eb4 100644 --- a/packages/database/src/relation-repository/belongs-to-many-repository.ts +++ b/packages/database/src/relation-repository/belongs-to-many-repository.ts @@ -38,6 +38,7 @@ export class BelongsToManyRepository extends MultipleRelationRepository implemen const sourceModel = await this.getSourceModel(transaction); const createOptions = { + ...options, through: values[this.throughName()], transaction, }; diff --git a/packages/database/src/relation-repository/multiple-relation-repository.ts b/packages/database/src/relation-repository/multiple-relation-repository.ts index d83dce55f..2bc5069e9 100644 --- a/packages/database/src/relation-repository/multiple-relation-repository.ts +++ b/packages/database/src/relation-repository/multiple-relation-repository.ts @@ -145,13 +145,14 @@ export abstract class MultipleRelationRepository extends RelationRepository { for (const instance of instances) { await updateModelByValues(instance, values, { + ...options, sanitized: true, sourceModel: this.sourceModel, transaction, }); } - return true; + return instances; } async destroy(options?: PK | DestroyOptions): Promise { diff --git a/packages/database/src/relation-repository/relation-repository.ts b/packages/database/src/relation-repository/relation-repository.ts index 1a208a967..74d565ebf 100644 --- a/packages/database/src/relation-repository/relation-repository.ts +++ b/packages/database/src/relation-repository/relation-repository.ts @@ -39,7 +39,7 @@ export abstract class RelationRepository { const sourceModel = await this.getSourceModel(); - const instance = await sourceModel[createAccessor](guard.sanitize(options.values)); + const instance = await sourceModel[createAccessor](guard.sanitize(options.values), options); await updateAssociations(instance, values, options); diff --git a/packages/database/src/repository.ts b/packages/database/src/repository.ts index 3868a46ba..41107f563 100644 --- a/packages/database/src/repository.ts +++ b/packages/database/src/repository.ts @@ -356,7 +356,7 @@ export class Repository { diff --git a/packages/database/src/update-associations.ts b/packages/database/src/update-associations.ts index 519716ed9..ed29e2097 100644 --- a/packages/database/src/update-associations.ts +++ b/packages/database/src/update-associations.ts @@ -68,7 +68,7 @@ export async function updateModelByValues(instance: Model, values: UpdateValue, values = guard.sanitize(values); } - await instance.update(values); + await instance.update(values, options); await updateAssociations(instance, values, options); }