feat: collection manager schema env (#1506)

* feat: database manager schema env support

* feat: skip db2cm collection in custom schema

* fix: test

* test: through table custom schema

* chore: fields hooks

* chore: reload through collection on autoCreate

* fix: test error

* fix: skip test case

* chore: children test

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
ChengLei Shao 2023-03-01 17:55:37 +08:00 committed by GitHub
parent acc5656490
commit 9048b2a58b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 98 additions and 110 deletions

View File

@ -153,6 +153,15 @@ export class Model<TModelAttributes extends {} = any, TCreationAttributes extend
static async sync(options) {
const model = this as any;
const _schema = model._schema;
if (_schema && _schema != 'public') {
await this.sequelize.query(`CREATE SCHEMA IF NOT EXISTS "${_schema}";`, {
raw: true,
transaction: options?.transaction,
});
}
// fix sequelize sync with model that not have any column
if (Object.keys(model.tableAttributes).length === 0) {
if (this.database.inDialect('sqlite', 'mysql')) {

View File

@ -1,6 +1,7 @@
import Database, { Collection as DBCollection } from '@nocobase/database';
import Application from '@nocobase/server';
import { createApp } from '.';
import CollectionManagerPlugin from '@nocobase/plugin-collection-manager';
describe('collections repository', () => {
let db: Database;
@ -19,6 +20,49 @@ describe('collections repository', () => {
await app.destroy();
});
it('should set collection schema from env', async () => {
if (!db.inDialect('postgres')) {
return;
}
const plugin = app.getPlugin<CollectionManagerPlugin>('collection-manager');
plugin.schema = 'testSchema';
await Collection.repository.create({
values: {
name: 'posts',
},
context: {},
});
const postsCollection = db.getCollection('posts');
expect(postsCollection.options.schema).toEqual('testSchema');
await Collection.repository.create({
values: {
name: 'tags',
},
context: {},
});
await Field.repository.create({
values: {
name: 'posts',
type: 'belongsToMany',
target: 'posts',
through: 'posts_tags',
foreignKey: 'tag_id',
otherKey: 'post_id',
interface: 'm2m',
collectionName: 'tags',
},
context: {},
});
const throughCollection = db.getCollection('posts_tags');
expect(throughCollection.options.schema).toEqual('testSchema');
});
test('create underscored field', async () => {
if (process.env.DB_UNDERSCORED !== 'true') {
return;

View File

@ -1,78 +0,0 @@
import Database, { Collection as DBCollection } from '@nocobase/database';
import Application from '@nocobase/server';
import { createApp } from '..';
describe('children options', () => {
let db: Database;
let app: Application;
let Collection: DBCollection;
let Field: DBCollection;
beforeEach(async () => {
app = await createApp();
db = app.db;
Collection = db.getCollection('collections');
Field = db.getCollection('fields');
await Collection.repository.create({
values: {
name: 'tests',
},
});
await Collection.repository.create({
values: {
name: 'foos',
},
});
});
afterEach(async () => {
await app.destroy();
});
it('when there are no children, the target collection is not created', async () => {
const field = await Field.repository.create({
values: {
type: 'hasMany',
collectionName: 'tests',
},
});
const json = field.toJSON();
expect(json).toMatchObject({
type: 'hasMany',
collectionName: 'tests',
sourceKey: 'id',
targetKey: 'id',
});
expect(json.name).toBeDefined();
expect(json.target).toBeDefined();
expect(json.foreignKey).toBeDefined();
// 无 children 时target collection 不创建
const target = await Collection.model.findOne({
where: {
name: json.target,
},
});
expect(target).toBeNull();
});
it('the collectionName of the child field is the target of the parent field', async () => {
const field = await Field.repository.create({
values: {
type: 'hasMany',
collectionName: 'tests',
children: [{ type: 'string' }, { type: 'string' }],
},
});
const json = field.toJSON();
const target = await Collection.model.findOne({
where: {
name: json.target,
},
});
expect(target).toBeDefined();
// 子字段的 collectionName 是父字段的 target
for (const child of json.children) {
expect(child.collectionName).toBe(json.target);
}
});
});

View File

@ -152,6 +152,7 @@ export function afterCreateForForeignKeyField(db: Database) {
isThrough: true,
sortable: false,
},
context,
transaction,
});
}

View File

@ -66,10 +66,12 @@ export class CollectionRepository extends Repository {
...field.options,
});
}
await this.create({
values: {
...options,
fields,
from: 'db2cm',
},
});
}

View File

@ -1,6 +1,4 @@
import path from 'path';
import lodash from 'lodash';
import { UniqueConstraintError } from 'sequelize';
import PluginErrorHandler from '@nocobase/plugin-error-handler';
@ -11,7 +9,6 @@ import { CollectionRepository } from '.';
import {
afterCreateForForeignKeyField,
afterCreateForReverseField,
beforeCreateForChildrenCollection,
beforeCreateForReverseField,
beforeDestroyForeignKey,
beforeInitOptions,
@ -19,9 +16,17 @@ import {
import { InheritedCollection } from '@nocobase/database';
import { CollectionModel, FieldModel } from './models';
import * as process from 'process';
import lodash from 'lodash';
export class CollectionManagerPlugin extends Plugin {
public schema: string;
async beforeLoad() {
if (process.env.COLLECTION_MANAGER_SCHEMA) {
this.schema = process.env.COLLECTION_MANAGER_SCHEMA;
}
this.app.db.registerModels({
CollectionModel,
FieldModel,
@ -48,25 +53,29 @@ export class CollectionManagerPlugin extends Plugin {
],
});
this.app.db.on('fields.beforeUpdate', async (model, options) => {
const newValue = options.values;
if (
model.get('reverseKey') &&
lodash.get(newValue, 'reverseField') &&
!lodash.get(newValue, 'reverseField.key')
) {
const field = await this.app.db
.getModel('fields')
.findByPk(model.get('reverseKey'), { transaction: options.transaction });
if (field) {
throw new Error('cant update field without a reverseField key');
}
this.app.db.on('collections.beforeCreate', async (model) => {
if (this.app.db.inDialect('postgres') && this.schema && model.get('from') != 'db2cm') {
model.set('schema', this.schema);
}
});
this.app.db.on(
'collections.afterCreateWithAssociations',
async (model: CollectionModel, { context, transaction }) => {
if (context) {
await model.migrate({
transaction,
});
}
},
);
this.app.db.on('collections.beforeDestroy', async (model: CollectionModel, options) => {
await model.remove(options);
});
// 要在 beforeInitOptions 之前处理
this.app.db.on('fields.beforeCreate', beforeCreateForReverseField(this.app.db));
this.app.db.on('fields.beforeCreate', beforeCreateForChildrenCollection(this.app.db));
this.app.db.on('fields.beforeCreate', async (model, options) => {
const collectionName = model.get('collectionName');
@ -91,17 +100,6 @@ export class CollectionManagerPlugin extends Plugin {
this.app.db.on('fields.afterCreate', afterCreateForReverseField(this.app.db));
this.app.db.on(
'collections.afterCreateWithAssociations',
async (model: CollectionModel, { context, transaction }) => {
if (context) {
await model.migrate({
transaction,
});
}
},
);
this.app.db.on('fields.afterCreate', async (model: FieldModel, { context, transaction }) => {
if (context) {
await model.migrate({
@ -114,6 +112,22 @@ export class CollectionManagerPlugin extends Plugin {
// after migrate
this.app.db.on('fields.afterCreate', afterCreateForForeignKeyField(this.app.db));
this.app.db.on('fields.beforeUpdate', async (model, options) => {
const newValue = options.values;
if (
model.get('reverseKey') &&
lodash.get(newValue, 'reverseField') &&
!lodash.get(newValue, 'reverseField.key')
) {
const field = await this.app.db
.getModel('fields')
.findByPk(model.get('reverseKey'), { transaction: options.transaction });
if (field) {
throw new Error('cant update field without a reverseField key');
}
}
});
this.app.db.on('fields.afterUpdate', async (model: FieldModel, { context, transaction }) => {
const prevOptions = model.previous('options');
const currentOptions = model.get('options');
@ -158,10 +172,6 @@ export class CollectionManagerPlugin extends Plugin {
});
});
this.app.db.on('collections.beforeDestroy', async (model: CollectionModel, options) => {
await model.remove(options);
});
this.app.db.on('fields.afterDestroy', async (model: FieldModel, options) => {
const { transaction } = options;
const collectionName = model.get('collectionName');