fix: sync collection field default value (#907)
This commit is contained in:
parent
3e6d1a3427
commit
76f5754e20
@ -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,
|
||||||
|
@ -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();
|
const response2 = await app.agent().resource('test1').create();
|
||||||
expect(response2.body.data.field1).toBe('cba');
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 }) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user