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

View File

@ -1,9 +1,12 @@
import { MockServer } from '@nocobase/test';
import { createApp } from '..';
import { Collection } from '@nocobase/database';
describe('field defaultValue', () => {
let app: MockServer;
let TestCollection: Collection;
beforeEach(async () => {
app = await createApp();
await app
@ -14,13 +17,9 @@ describe('field defaultValue', () => {
name: 'test1',
},
});
});
afterEach(async () => {
await app.destroy();
});
TestCollection = app.db.getCollection('test1');
it('should be updated', async () => {
await app
.agent()
.resource('collections.fields', 'test1')
@ -31,6 +30,19 @@ describe('field defaultValue', () => {
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();
expect(response1.body.data.field1).toBe('abc');
await app
@ -43,7 +55,28 @@ describe('field defaultValue', () => {
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 { SyncOptions, Transactionable, UniqueConstraintError, Utils } from 'sequelize';
import Database, { Collection, Field, MagicAttributeModel } from '@nocobase/database';
import { SyncOptions, Transactionable, UniqueConstraintError } from 'sequelize';
interface LoadOptions extends Transactionable {
// TODO
@ -13,16 +13,23 @@ interface MigrateOptions extends SyncOptions, Transactionable {
async function migrate(field: Field, options: MigrateOptions): Promise<void> {
const { unique } = field.options;
const { model } = field.collection;
const ukName = `${model.tableName}_${field.name}_uk`;
const queryInterface = model.sequelize.getQueryInterface();
const fieldAttribute = model.rawAttributes[field.name];
// @ts-ignore
const existedConstraints = await queryInterface.showConstraint(model.tableName, ukName, { transaction: options.transaction }) as any[];
const constraintBefore = existedConstraints.find(item => item.constraintName === ukName);
const existedConstraints = (await queryInterface.showConstraint(model.tableName, ukName, {
transaction: options.transaction,
})) as any[];
const constraintBefore = existedConstraints.find((item) => item.constraintName === ukName);
if (typeof fieldAttribute?.unique !== 'undefined') {
if (constraintBefore && !unique) {
await queryInterface.removeConstraint(model.tableName, ukName, { transaction: options.transaction });
}
fieldAttribute.unique = Boolean(constraintBefore);
}
@ -33,19 +40,24 @@ async function migrate(field: Field, options: MigrateOptions): Promise<void> {
type: 'unique',
fields: [field.name],
name: ukName,
transaction: options.transaction
transaction: options.transaction,
});
}
if (typeof fieldAttribute?.unique !== 'undefined') {
fieldAttribute.unique = unique;
}
// @ts-ignore
const updatedConstraints = await queryInterface.showConstraint(model.tableName, ukName, { transaction: options.transaction }) as any[];
const indexAfter = updatedConstraints.find(item => item.constraintName === ukName);
const updatedConstraints = (await queryInterface.showConstraint(model.tableName, ukName, {
transaction: options.transaction,
})) as any[];
const indexAfter = updatedConstraints.find((item) => item.constraintName === ukName);
if (unique && !indexAfter) {
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)) {
return;
}
const collection = this.db.getCollection(collectionName);
const name = this.get('name');
if (skipExist && collection.hasField(name)) {
return collection.getField(name);
}
const options = this.get();
if (options.uiSchemaUid) {
const UISchema = this.db.getModel('uiSchemas');
const uiSchema = await UISchema.findByPk(options.uiSchemaUid, {
@ -74,6 +89,7 @@ export class FieldModel extends MagicAttributeModel {
});
Object.assign(options, { uiSchema: uiSchema.get() });
}
return collection.setField(name, options);
}
@ -81,6 +97,7 @@ export class FieldModel extends MagicAttributeModel {
const field = await this.load({
transaction: options.transaction,
});
if (!field) {
return;
}
@ -98,11 +115,11 @@ export class FieldModel extends MagicAttributeModel {
}
async remove(options?: any) {
const collectionName = this.get('collectionName');
if (!this.db.hasCollection(collectionName)) {
const collection = this.getFieldCollection();
if (!collection) {
return;
}
const collection = this.db.getCollection(collectionName);
const field = collection.getField(this.get('name'));
if (!field) {
return;
@ -111,4 +128,42 @@ export class FieldModel extends MagicAttributeModel {
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,
});
});
for (const key in beforeInitOptions) {
if (Object.prototype.hasOwnProperty.call(beforeInitOptions, key)) {
const fn = beforeInitOptions[key];
this.app.db.on(`fields.${key}.beforeInitOptions`, fn);
}
}
this.app.db.on('fields.afterCreate', afterCreateForReverseField(this.app.db));
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 }) => {
const prevOptions = model.previous('options');
const currentOptions = model.get('options');
if (context) {
const { unique: prev } = model.previous('options');
const { unique: next } = model.get('options');
const prev = prevOptions['unique'];
const next = currentOptions['unique'];
if (Boolean(prev) !== Boolean(next)) {
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 }) => {