fix: sync collection field default value (#907)

This commit is contained in:
ChengLei Shao 2022-10-14 09:56:16 +08:00 committed by chenos
parent 3e6d1a3427
commit 76f5754e20
4 changed files with 122 additions and 22 deletions

View File

@ -5,7 +5,7 @@ import {
ModelIndexesOptions, ModelIndexesOptions,
QueryInterfaceOptions, QueryInterfaceOptions,
SyncOptions, SyncOptions,
Transactionable Transactionable,
} from 'sequelize'; } from 'sequelize';
import { Collection } from '../collection'; import { Collection } from '../collection';
import { Database } from '../database'; import { Database } from '../database';
@ -53,7 +53,6 @@ export abstract class Field {
this.init(); this.init();
} }
// TODO
async sync(syncOptions: SyncOptions) { async sync(syncOptions: SyncOptions) {
await this.collection.sync({ await this.collection.sync({
...syncOptions, ...syncOptions,

View File

@ -1,9 +1,12 @@
import { MockServer } from '@nocobase/test'; import { MockServer } from '@nocobase/test';
import { createApp } from '..'; import { createApp } from '..';
import { Collection } from '@nocobase/database';
describe('field defaultValue', () => { describe('field defaultValue', () => {
let app: MockServer; let app: MockServer;
let TestCollection: Collection;
beforeEach(async () => { beforeEach(async () => {
app = await createApp(); app = await createApp();
await app await app
@ -14,13 +17,9 @@ describe('field defaultValue', () => {
name: 'test1', name: 'test1',
}, },
}); });
});
afterEach(async () => { TestCollection = app.db.getCollection('test1');
await app.destroy();
});
it('should be updated', async () => {
await app await app
.agent() .agent()
.resource('collections.fields', 'test1') .resource('collections.fields', 'test1')
@ -31,6 +30,19 @@ describe('field defaultValue', () => {
defaultValue: 'abc', defaultValue: 'abc',
}, },
}); });
});
afterEach(async () => {
await app.destroy();
});
it('should be updated', async () => {
const dialect = app.db.sequelize.getDialect();
if (dialect != 'postgres') {
return;
}
const response1 = await app.agent().resource('test1').create(); const response1 = await app.agent().resource('test1').create();
expect(response1.body.data.field1).toBe('abc'); expect(response1.body.data.field1).toBe('abc');
await app await app
@ -43,7 +55,28 @@ describe('field defaultValue', () => {
defaultValue: 'cba', defaultValue: 'cba',
}, },
}); });
const response2 = await app.agent().resource('test1').create();
expect(response2.body.data.field1).toBe('cba'); const response2 = await app.agent().resource('test1').create();
expect(response2.body.data.field1).toBe('cba');
const results = await app.db.sequelize.getQueryInterface().describeTable(TestCollection.model.tableName);
expect(results.field1.defaultValue).toBe('cba');
});
it('should be cleaned', async () => {
await app
.agent()
.resource('collections.fields', 'test1')
.update({
filterByTk: 'field1',
values: {
type: 'string',
defaultValue: null,
},
});
const response2 = await app.agent().resource('test1').create();
expect(response2.body.data.field1).toBeNull();
}); });
}); });

View File

@ -1,5 +1,5 @@
import Database, { Field, MagicAttributeModel } from '@nocobase/database'; import Database, { Collection, Field, MagicAttributeModel } from '@nocobase/database';
import { SyncOptions, Transactionable, UniqueConstraintError, Utils } from 'sequelize'; import { SyncOptions, Transactionable, UniqueConstraintError } from 'sequelize';
interface LoadOptions extends Transactionable { interface LoadOptions extends Transactionable {
// TODO // TODO
@ -13,16 +13,23 @@ interface MigrateOptions extends SyncOptions, Transactionable {
async function migrate(field: Field, options: MigrateOptions): Promise<void> { async function migrate(field: Field, options: MigrateOptions): Promise<void> {
const { unique } = field.options; const { unique } = field.options;
const { model } = field.collection; const { model } = field.collection;
const ukName = `${model.tableName}_${field.name}_uk`; const ukName = `${model.tableName}_${field.name}_uk`;
const queryInterface = model.sequelize.getQueryInterface(); const queryInterface = model.sequelize.getQueryInterface();
const fieldAttribute = model.rawAttributes[field.name]; const fieldAttribute = model.rawAttributes[field.name];
// @ts-ignore // @ts-ignore
const existedConstraints = await queryInterface.showConstraint(model.tableName, ukName, { transaction: options.transaction }) as any[]; const existedConstraints = (await queryInterface.showConstraint(model.tableName, ukName, {
const constraintBefore = existedConstraints.find(item => item.constraintName === ukName); transaction: options.transaction,
})) as any[];
const constraintBefore = existedConstraints.find((item) => item.constraintName === ukName);
if (typeof fieldAttribute?.unique !== 'undefined') { if (typeof fieldAttribute?.unique !== 'undefined') {
if (constraintBefore && !unique) { if (constraintBefore && !unique) {
await queryInterface.removeConstraint(model.tableName, ukName, { transaction: options.transaction }); await queryInterface.removeConstraint(model.tableName, ukName, { transaction: options.transaction });
} }
fieldAttribute.unique = Boolean(constraintBefore); fieldAttribute.unique = Boolean(constraintBefore);
} }
@ -33,19 +40,24 @@ async function migrate(field: Field, options: MigrateOptions): Promise<void> {
type: 'unique', type: 'unique',
fields: [field.name], fields: [field.name],
name: ukName, name: ukName,
transaction: options.transaction transaction: options.transaction,
}); });
} }
if (typeof fieldAttribute?.unique !== 'undefined') { if (typeof fieldAttribute?.unique !== 'undefined') {
fieldAttribute.unique = unique; fieldAttribute.unique = unique;
} }
// @ts-ignore // @ts-ignore
const updatedConstraints = await queryInterface.showConstraint(model.tableName, ukName, { transaction: options.transaction }) as any[]; const updatedConstraints = (await queryInterface.showConstraint(model.tableName, ukName, {
const indexAfter = updatedConstraints.find(item => item.constraintName === ukName); transaction: options.transaction,
})) as any[];
const indexAfter = updatedConstraints.find((item) => item.constraintName === ukName);
if (unique && !indexAfter) { if (unique && !indexAfter) {
throw new UniqueConstraintError({ throw new UniqueConstraintError({
fields: { [field.name]: undefined } fields: { [field.name]: undefined },
}); });
} }
} }
@ -61,12 +73,15 @@ export class FieldModel extends MagicAttributeModel {
if (!this.db.hasCollection(collectionName)) { if (!this.db.hasCollection(collectionName)) {
return; return;
} }
const collection = this.db.getCollection(collectionName); const collection = this.db.getCollection(collectionName);
const name = this.get('name'); const name = this.get('name');
if (skipExist && collection.hasField(name)) { if (skipExist && collection.hasField(name)) {
return collection.getField(name); return collection.getField(name);
} }
const options = this.get(); const options = this.get();
if (options.uiSchemaUid) { if (options.uiSchemaUid) {
const UISchema = this.db.getModel('uiSchemas'); const UISchema = this.db.getModel('uiSchemas');
const uiSchema = await UISchema.findByPk(options.uiSchemaUid, { const uiSchema = await UISchema.findByPk(options.uiSchemaUid, {
@ -74,6 +89,7 @@ export class FieldModel extends MagicAttributeModel {
}); });
Object.assign(options, { uiSchema: uiSchema.get() }); Object.assign(options, { uiSchema: uiSchema.get() });
} }
return collection.setField(name, options); return collection.setField(name, options);
} }
@ -81,6 +97,7 @@ export class FieldModel extends MagicAttributeModel {
const field = await this.load({ const field = await this.load({
transaction: options.transaction, transaction: options.transaction,
}); });
if (!field) { if (!field) {
return; return;
} }
@ -98,11 +115,11 @@ export class FieldModel extends MagicAttributeModel {
} }
async remove(options?: any) { async remove(options?: any) {
const collectionName = this.get('collectionName'); const collection = this.getFieldCollection();
if (!this.db.hasCollection(collectionName)) { if (!collection) {
return; return;
} }
const collection = this.db.getCollection(collectionName);
const field = collection.getField(this.get('name')); const field = collection.getField(this.get('name'));
if (!field) { if (!field) {
return; return;
@ -111,4 +128,42 @@ export class FieldModel extends MagicAttributeModel {
transaction: options.transaction, transaction: options.transaction,
}); });
} }
async syncDefaultValue(
options: Transactionable & {
defaultValue: any;
},
) {
const collection = this.getFieldCollection();
if (!collection) {
return;
}
const field = collection.getField(this.get('name'));
const queryInterface = collection.db.sequelize.getQueryInterface();
await queryInterface.changeColumn(
collection.model.tableName,
this.get('name'),
{
type: field.dataType,
defaultValue: options.defaultValue,
},
{
transaction: options.transaction,
},
);
}
protected getFieldCollection(): Collection | null {
const collectionName = this.get('collectionName');
if (!this.db.hasCollection(collectionName)) {
return;
}
return this.db.getCollection(collectionName);
}
} }

View File

@ -60,12 +60,14 @@ export class CollectionManagerPlugin extends Plugin {
database: this.app.db, database: this.app.db,
}); });
}); });
for (const key in beforeInitOptions) { for (const key in beforeInitOptions) {
if (Object.prototype.hasOwnProperty.call(beforeInitOptions, key)) { if (Object.prototype.hasOwnProperty.call(beforeInitOptions, key)) {
const fn = beforeInitOptions[key]; const fn = beforeInitOptions[key];
this.app.db.on(`fields.${key}.beforeInitOptions`, fn); this.app.db.on(`fields.${key}.beforeInitOptions`, fn);
} }
} }
this.app.db.on('fields.afterCreate', afterCreateForReverseField(this.app.db)); this.app.db.on('fields.afterCreate', afterCreateForReverseField(this.app.db));
this.app.db.on('collections.afterCreateWithAssociations', async (model, { context, transaction }) => { this.app.db.on('collections.afterCreateWithAssociations', async (model, { context, transaction }) => {
@ -87,13 +89,24 @@ export class CollectionManagerPlugin extends Plugin {
}); });
this.app.db.on('fields.afterUpdate', async (model: FieldModel, { context, transaction }) => { this.app.db.on('fields.afterUpdate', async (model: FieldModel, { context, transaction }) => {
const prevOptions = model.previous('options');
const currentOptions = model.get('options');
if (context) { if (context) {
const { unique: prev } = model.previous('options'); const prev = prevOptions['unique'];
const { unique: next } = model.get('options'); const next = currentOptions['unique'];
if (Boolean(prev) !== Boolean(next)) { if (Boolean(prev) !== Boolean(next)) {
await model.migrate({ transaction }); await model.migrate({ transaction });
} }
} }
const prevDefaultValue = prevOptions['defaultValue'];
const currentDefaultValue = currentOptions['defaultValue'];
if (prevDefaultValue != currentDefaultValue) {
await model.syncDefaultValue({ transaction, defaultValue: currentDefaultValue });
}
}); });
this.app.db.on('fields.afterSaveWithAssociations', async (model, { context, transaction }) => { this.app.db.on('fields.afterSaveWithAssociations', async (model, { context, transaction }) => {