mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 08:55:33 +00:00
chore: update field with primary key attribute (#3852)
* chore: update field with primary key attribute * chore: test * fix: build * chore: test * chore: test * chore: test * chore: test * chore: test * fix: test * fix: test * fix: test * chore: unique index name * fix: test * chore: test
This commit is contained in:
parent
2d7a427c5a
commit
b1aa6cff5e
@ -75,6 +75,7 @@ describe('primary key', () => {
|
||||
await assertPrimaryKey('someField', true);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(process.env['DB_DIALECT'] === 'sqlite')('primary key not in sqlite', () => {
|
||||
let db: Database;
|
||||
|
||||
|
@ -357,10 +357,6 @@ export class Collection<
|
||||
);
|
||||
}
|
||||
|
||||
if (this.options.autoGenId !== false && options.primaryKey) {
|
||||
this.model.removeAttribute('id');
|
||||
}
|
||||
|
||||
this.removeField(name);
|
||||
this.fields.set(name, field);
|
||||
this.emit('field.afterAdd', field);
|
||||
|
@ -12,6 +12,8 @@ export class SyncRunner {
|
||||
private readonly database: Database;
|
||||
private tableDescMap = {};
|
||||
|
||||
private uniqueAttributes: string[] = [];
|
||||
|
||||
constructor(private model: typeof Model) {
|
||||
this.collection = model.collection;
|
||||
this.database = model.database;
|
||||
@ -63,6 +65,16 @@ export class SyncRunner {
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
const beforeColumns = await this.queryInterface.describeTable(this.tableName, options);
|
||||
await this.handlePrimaryKeyBeforeSync(beforeColumns, options);
|
||||
await this.handleUniqueFieldBeforeSync(beforeColumns, options);
|
||||
} catch (e) {
|
||||
if (!e.message.includes('No description found')) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const syncResult = await this.performSync(options);
|
||||
const columns = await this.queryInterface.describeTable(this.tableName, options);
|
||||
|
||||
@ -73,11 +85,48 @@ export class SyncRunner {
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
async handlePrimaryKey(columns, options) {
|
||||
if (!this.database.inDialect('postgres')) {
|
||||
async handleUniqueFieldBeforeSync(beforeColumns, options) {
|
||||
if (!this.database.inDialect('sqlite')) {
|
||||
return;
|
||||
}
|
||||
// find new attributes with unique true
|
||||
const newAttributes = Object.keys(this.rawAttributes).filter((key) => {
|
||||
return !Object.keys(beforeColumns).includes(this.rawAttributes[key].field) && this.rawAttributes[key].unique;
|
||||
});
|
||||
|
||||
this.uniqueAttributes = newAttributes;
|
||||
|
||||
// set unique false for new attributes to skip sequelize sync error
|
||||
for (const newAttribute of newAttributes) {
|
||||
this.rawAttributes[newAttribute].unique = false;
|
||||
}
|
||||
}
|
||||
|
||||
async handlePrimaryKeyBeforeSync(columns, options) {
|
||||
const columnsBePrimaryKey = Object.keys(columns)
|
||||
.filter((key) => {
|
||||
return columns[key].primaryKey == true;
|
||||
})
|
||||
.sort();
|
||||
|
||||
const columnsWillBePrimaryKey = Object.keys(this.rawAttributes)
|
||||
.filter((key) => {
|
||||
return this.rawAttributes[key].primaryKey == true;
|
||||
})
|
||||
.map((key) => {
|
||||
return this.rawAttributes[key].field;
|
||||
})
|
||||
.sort();
|
||||
|
||||
if (columnsBePrimaryKey.length == 1 && !columnsWillBePrimaryKey.includes(columnsBePrimaryKey[0])) {
|
||||
// remove primary key
|
||||
if (this.database.inDialect('mariadb', 'mysql')) {
|
||||
await this.sequelize.query(`ALTER TABLE ${this.collection.quotedTableName()} DROP PRIMARY KEY;`, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handlePrimaryKey(columns, options) {
|
||||
try {
|
||||
const columnsBePrimaryKey = Object.keys(columns)
|
||||
.filter((key) => {
|
||||
@ -95,22 +144,32 @@ export class SyncRunner {
|
||||
.sort();
|
||||
|
||||
if (columnsWillBePrimaryKey.length == 0) {
|
||||
// skip if no primary key
|
||||
return;
|
||||
}
|
||||
|
||||
if (JSON.stringify(columnsBePrimaryKey) != JSON.stringify(columnsWillBePrimaryKey)) {
|
||||
await this.queryInterface.addConstraint(this.tableName, {
|
||||
type: 'primary key',
|
||||
fields: columnsWillBePrimaryKey,
|
||||
name: `${this.collection.tableName()}_${columnsWillBePrimaryKey.join('_')}_pk`,
|
||||
transaction: options?.transaction,
|
||||
});
|
||||
if (
|
||||
columnsWillBePrimaryKey.length == 1 &&
|
||||
JSON.stringify(columnsBePrimaryKey) != JSON.stringify(columnsWillBePrimaryKey)
|
||||
) {
|
||||
if (this.database.inDialect('mariadb', 'mysql')) {
|
||||
await this.sequelize.query(
|
||||
`ALTER TABLE ${this.collection.quotedTableName()} ADD PRIMARY KEY (${columnsWillBePrimaryKey[0]});`,
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
await this.queryInterface.addConstraint(this.tableName, {
|
||||
type: 'primary key',
|
||||
fields: columnsWillBePrimaryKey,
|
||||
name: `${this.collection.tableName()}_${columnsWillBePrimaryKey.join('_')}_pk`,
|
||||
transaction: options?.transaction,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message.includes('No description found')) {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,6 +235,10 @@ export class SyncRunner {
|
||||
}
|
||||
|
||||
async handleUniqueIndex(options) {
|
||||
for (const uniqueAttribute of this.uniqueAttributes) {
|
||||
this.rawAttributes[uniqueAttribute].unique = true;
|
||||
}
|
||||
|
||||
const existsIndexes: any = await this.queryInterface.showIndex(this.collection.getTableNameWithSchema(), options);
|
||||
const existsUniqueIndexes = existsIndexes.filter((index) => index.unique);
|
||||
|
||||
@ -225,6 +288,7 @@ export class SyncRunner {
|
||||
await this.queryInterface.addIndex(this.tableName, [this.rawAttributes[uniqueAttribute].field], {
|
||||
unique: true,
|
||||
transaction: options?.transaction,
|
||||
name: `${this.collection.tableName()}_${this.rawAttributes[uniqueAttribute].field}_uk`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -196,12 +196,16 @@ describe('gateway', () => {
|
||||
plugins: ['nocobase'],
|
||||
});
|
||||
await waitSecond();
|
||||
|
||||
await app.runAsCLI(['install'], {
|
||||
from: 'user',
|
||||
throwError: true,
|
||||
});
|
||||
|
||||
await app.runAsCLI(['start'], {
|
||||
from: 'user',
|
||||
});
|
||||
|
||||
await waitSecond();
|
||||
clearMessages();
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Database } from '@nocobase/database';
|
||||
import { Database, HasManyRepository } from '@nocobase/database';
|
||||
import { MockServer } from '@nocobase/test';
|
||||
import { createApp } from './index';
|
||||
|
||||
@ -45,4 +45,161 @@ describe('primary key test', function () {
|
||||
const errorMessage = response.body.errors[0].message;
|
||||
expect(errorMessage).toContain('already has primary key');
|
||||
});
|
||||
|
||||
it('should throw error when update field in collection that already has primary key', async () => {
|
||||
await db.getRepository('collections').create({
|
||||
values: {
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'bigInt',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
let err;
|
||||
try {
|
||||
await db.getRepository<HasManyRepository>('collections.fields', 'posts').update({
|
||||
filterByTk: 'title',
|
||||
values: {
|
||||
primaryKey: true,
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).toBeDefined();
|
||||
});
|
||||
|
||||
it.skipIf(process.env['DB_DIALECT'] === 'sqlite')('should add new primary key', async () => {
|
||||
await db.getRepository('collections').create({
|
||||
values: {
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'bigInt',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
const response1 = await app
|
||||
.agent()
|
||||
.resource('collections.fields', 'posts')
|
||||
.update({
|
||||
filterByTk: 'id',
|
||||
values: {
|
||||
primaryKey: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response1.statusCode).toBe(200);
|
||||
|
||||
const model = db.getCollection('posts').model;
|
||||
expect(model.rawAttributes['id'].primaryKey).toBe(false);
|
||||
|
||||
const response2 = await app
|
||||
.agent()
|
||||
.resource('collections.fields', 'posts')
|
||||
.create({
|
||||
values: {
|
||||
primaryKey: true,
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response2.statusCode).toBe(200);
|
||||
|
||||
expect(model.rawAttributes['title'].primaryKey).toBe(true);
|
||||
expect(model.rawAttributes['id'].primaryKey).toBe(false);
|
||||
});
|
||||
|
||||
it.skipIf(process.env['DB_DIALECT'] === 'sqlite')('should update new primary key', async () => {
|
||||
await db.getRepository('collections').create({
|
||||
values: {
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'bigInt',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
const response1 = await app
|
||||
.agent()
|
||||
.resource('collections.fields', 'posts')
|
||||
.update({
|
||||
filterByTk: 'id',
|
||||
values: {
|
||||
primaryKey: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response1.statusCode).toBe(200);
|
||||
|
||||
const model = db.getCollection('posts').model;
|
||||
expect(model.rawAttributes['id'].primaryKey).toBe(false);
|
||||
|
||||
const response2 = await app
|
||||
.agent()
|
||||
.resource('collections.fields', 'posts')
|
||||
.create({
|
||||
values: {
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response2.statusCode).toBe(200);
|
||||
|
||||
const response3 = await app
|
||||
.agent()
|
||||
.resource('collections.fields', 'posts')
|
||||
.update({
|
||||
filterByTk: 'title',
|
||||
values: {
|
||||
primaryKey: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response3.statusCode).toBe(200);
|
||||
|
||||
expect(model.rawAttributes['title'].primaryKey).toBe(true);
|
||||
|
||||
const tableInfo = await db.sequelize
|
||||
.getQueryInterface()
|
||||
.describeTable(db.getCollection('posts').getTableNameWithSchema());
|
||||
|
||||
expect(tableInfo.title.primaryKey).toBe(true);
|
||||
});
|
||||
});
|
||||
|
@ -101,7 +101,7 @@ describe('collections', () => {
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
|
||||
test('remove collection 3', async () => {
|
||||
test.skipIf(process.env['DB_DIALECT'] === 'sqlite')('remove collection 3', async () => {
|
||||
await app
|
||||
.agent()
|
||||
.resource('collections')
|
||||
|
@ -189,6 +189,8 @@ export function afterCreateForForeignKeyField(db: Database) {
|
||||
return async (model, options) => {
|
||||
try {
|
||||
await hook(model, options);
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ export function afterCreateForReverseField(db: Database) {
|
||||
if (!reverseKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reverse = await Field.model.findByPk(reverseKey, { transaction });
|
||||
await reverse.update({ reverseKey: model.get('key') }, { hooks: false, transaction });
|
||||
};
|
||||
|
@ -25,3 +25,25 @@ export function beforeCreateForValidateField(db: Database) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function beforeUpdateForValidateField(db: Database) {
|
||||
return async (model, { transaction }) => {
|
||||
const isPrimaryKey = model.get('primaryKey');
|
||||
if (isPrimaryKey) {
|
||||
const collection = db.getCollection(model.get('collectionName'));
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const primaryKey = collection.model.primaryKeyAttribute;
|
||||
|
||||
if (primaryKey !== model.get('name') && collection.model.rawAttributes[primaryKey]) {
|
||||
throw new Error(
|
||||
`update field ${model.get('name')} failed, collection ${
|
||||
collection.name
|
||||
} already has primary key ${primaryKey}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -62,40 +62,6 @@ export class FieldModel extends MagicAttributeModel {
|
||||
});
|
||||
}
|
||||
|
||||
async migrate({ isNew, ...options }: MigrateOptions = {}) {
|
||||
let field;
|
||||
try {
|
||||
field = await this.load({
|
||||
transaction: options.transaction,
|
||||
});
|
||||
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
const collection = this.getFieldCollection();
|
||||
|
||||
if (isNew && collection.model.rawAttributes[this.get('name')] && this.get('unique')) {
|
||||
// trick: set unique to false to avoid auto sync unique index
|
||||
collection.model.rawAttributes[this.get('name')].unique = false;
|
||||
}
|
||||
|
||||
await field.sync(options);
|
||||
|
||||
if (isNew && this.get('unique')) {
|
||||
await this.syncUniqueIndex({
|
||||
transaction: options.transaction,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// field sync failed, delete from memory
|
||||
if (isNew && field) {
|
||||
// update field should not remove field from memory
|
||||
field.remove();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async remove(options?: any) {
|
||||
const collection = this.getFieldCollection();
|
||||
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
beforeDestroyForeignKey,
|
||||
beforeInitOptions,
|
||||
} from './hooks';
|
||||
import { beforeCreateForValidateField } from './hooks/beforeCreateForValidateField';
|
||||
import { beforeCreateForValidateField, beforeUpdateForValidateField } from './hooks/beforeCreateForValidateField';
|
||||
import { beforeCreateForViewCollection } from './hooks/beforeCreateForViewCollection';
|
||||
import { CollectionModel, FieldModel } from './models';
|
||||
import collectionActions from './resourcers/collections';
|
||||
@ -120,21 +120,11 @@ export class CollectionManagerPlugin extends Plugin {
|
||||
this.app.db.on('fields.beforeCreate', beforeCreateForValidateField(this.app.db));
|
||||
|
||||
this.app.db.on('fields.afterCreate', afterCreateForReverseField(this.app.db));
|
||||
|
||||
this.app.db.on('fields.afterCreate', async (model: FieldModel, { context, transaction }) => {
|
||||
if (context) {
|
||||
await model.migrate({
|
||||
isNew: true,
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// after migrate
|
||||
this.app.db.on('fields.afterCreate', afterCreateForForeignKeyField(this.app.db));
|
||||
this.app.db.on('fields.beforeUpdate', beforeUpdateForValidateField(this.app.db));
|
||||
|
||||
this.app.db.on('fields.beforeUpdate', async (model, options) => {
|
||||
const newValue = options.values;
|
||||
|
||||
if (
|
||||
model.get('reverseKey') &&
|
||||
lodash.get(newValue, 'reverseField') &&
|
||||
@ -147,6 +137,7 @@ export class CollectionManagerPlugin extends Plugin {
|
||||
throw new Error('cant update field without a reverseField key');
|
||||
}
|
||||
}
|
||||
|
||||
// todo: 目前只支持一对多
|
||||
if (model.get('sortable') && model.get('type') === 'hasMany') {
|
||||
model.set('sortBy', model.get('foreignKey') + 'Sort');
|
||||
@ -185,9 +176,36 @@ export class CollectionManagerPlugin extends Plugin {
|
||||
}
|
||||
});
|
||||
|
||||
this.app.db.on('fields.afterSaveWithAssociations', async (model: FieldModel, { context, transaction }) => {
|
||||
const afterCreateForForeignKeyFieldHook = afterCreateForForeignKeyField(this.app.db);
|
||||
|
||||
this.app.db.on('fields.afterCreate', async (model: FieldModel, options) => {
|
||||
const { context, transaction } = options;
|
||||
if (context) {
|
||||
await model.load({ transaction });
|
||||
await afterCreateForForeignKeyFieldHook(model, options);
|
||||
}
|
||||
});
|
||||
|
||||
this.app.db.on('fields.afterUpdate', async (model: FieldModel, options) => {
|
||||
const { context, transaction } = options;
|
||||
if (context) {
|
||||
await model.load({ transaction });
|
||||
}
|
||||
});
|
||||
|
||||
this.app.db.on('fields.afterSaveWithAssociations', async (model: FieldModel, options) => {
|
||||
const { context, transaction } = options;
|
||||
if (context) {
|
||||
const collection = this.app.db.getCollection(model.get('collectionName'));
|
||||
const syncOptions = {
|
||||
transaction,
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
},
|
||||
};
|
||||
|
||||
await collection.sync(syncOptions);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { defineCollection } from '@nocobase/database';
|
||||
export default defineCollection({
|
||||
dumpRules: 'required',
|
||||
name: 'customRequests',
|
||||
autoGenId: false,
|
||||
fields: [
|
||||
{
|
||||
type: 'uid',
|
||||
|
Loading…
Reference in New Issue
Block a user