chore: inhertis api with difference schema (#1545)

* refactor: getTableNameWithSchema

* feat: allow create collection with same name in difference schema

* feat: support multiple schema with inherit

* feat: repository after find hook & relation repository __colection attribute

* chore: debug info

* chore: tmp commit

* fix: tableoid to tablename error

* feat: filter child table by tableoid

* feat: filter child table by collection name

* fix: sync runner

* chore: test

* fix: jest empty test error
This commit is contained in:
ChengLei Shao 2023-04-06 09:05:47 +08:00 committed by GitHub
parent d37cadee6a
commit 242977983b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 336 additions and 28 deletions

View File

@ -328,6 +328,50 @@ describe('collection sync', () => {
expect(error).toBeInstanceOf(IdentifierError);
});
it('should throw error when collection has same table name and same schema', async () => {
const c1 = db.collection({
name: 'test',
tableName: 'test',
schema: 'public',
});
let err;
try {
const c2 = db.collection({
name: 'test2',
tableName: 'test',
schema: 'public',
});
} catch (e) {
err = e;
}
expect(err.message).toContain('have same tableName');
});
it('should allow same table name in difference schema', async () => {
const c1 = db.collection({
name: 'test',
tableName: 'test',
schema: 'public',
});
let err;
try {
const c2 = db.collection({
name: 'test2',
tableName: 'test',
schema: 'other_schema',
});
} catch (e) {
err = e;
}
expect(err).toBeFalsy();
});
test('limit field name length', async () => {
const longFieldName =
'this_is_a_very_long_field_name_that_should_be_truncated_this_is_a_very_long_field_name_that_should_be_truncated';

View File

@ -1,6 +1,7 @@
import Database from '../../database';
import { InheritedCollection } from '../../inherited-collection';
import { mockDatabase } from '../index';
import { BelongsToManyRepository } from '@nocobase/database';
import pgOnly from './helper';
pgOnly()('collection inherits', () => {
@ -15,6 +16,119 @@ pgOnly()('collection inherits', () => {
await db.close();
});
it('should list data filtered by child type', async () => {
const rootCollection = db.collection({
name: 'root',
fields: [{ name: 'name', type: 'string' }],
});
const child1Collection = db.collection({
name: 'child1',
inherits: ['root'],
});
const child2Collection = db.collection({
name: 'child2',
inherits: ['root'],
});
await db.sync();
await rootCollection.repository.create({
values: {
name: 'root1',
},
});
await child1Collection.repository.create({
values: [{ name: 'child1-1' }, { name: 'child1-2' }],
});
await child2Collection.repository.create({
values: [{ name: 'child2-1' }, { name: 'child2-2' }],
});
const records = await rootCollection.repository.find({
filter: {
'tableoid.$childIn': [child1Collection.name],
},
});
expect(records.every((r) => r.get('__collection') === child1Collection.name)).toBe(true);
const records2 = await rootCollection.repository.find({
filter: {
'tableoid.$childNotIn': [child1Collection.name],
},
});
expect(records2.every((r) => r.get('__collection') !== child1Collection.name)).toBe(true);
});
it('should list collection name in relation repository', async () => {
const personTagCollection = db.collection({
name: 'personTags',
fields: [{ name: 'name', type: 'string' }],
});
const studentTagCollection = db.collection({
name: 'studentTags',
inherits: ['personTags'],
fields: [{ name: 'school', type: 'string' }],
});
const personCollection = db.collection({
name: 'person',
fields: [{ name: 'tags', type: 'belongsToMany', target: 'personTags' }],
});
const studentCollection = db.collection({
name: 'student',
inherits: ['person'],
fields: [],
});
await db.sync();
const studentTag1 = await studentTagCollection.repository.create({
values: {
name: 'studentTag',
school: 'school1',
},
});
const personTag1 = await personTagCollection.repository.create({
values: {
name: 'personTag',
},
});
await studentCollection.repository.create({
values: {
tags: [
{
id: studentTag1.get('id'),
},
{
id: personTag1.get('id'),
},
],
},
});
const person1 = await personCollection.repository.findOne({});
const tags = await personCollection.repository
.relation<BelongsToManyRepository>('tags')
.of(person1.get('id'))
.find({});
const personTag = tags.find((tag) => tag.get('name') === 'personTag');
const studentTag = tags.find((tag) => tag.get('name') === 'studentTag');
expect(personTag.get('__collection')).toEqual(personTagCollection.name);
expect(studentTag.get('__collection')).toEqual(studentTagCollection.name);
});
it('should create inherits with table name contains upperCase', async () => {
db.collection({
name: 'parent',

View File

@ -140,7 +140,11 @@ export class Collection<
private checkTableName() {
const tableName = this.tableName();
for (const [k, collection] of this.db.collections) {
if (collection.name != this.options.name && tableName === collection.tableName()) {
if (
collection.name != this.options.name &&
tableName === collection.tableName() &&
collection.collectionSchema() === this.collectionSchema()
) {
throw new Error(`collection ${collection.name} and ${this.name} have same tableName "${tableName}"`);
}
}
@ -609,6 +613,22 @@ export class Collection<
return tableName;
}
public tableNameAsString(options?: { ignorePublicSchema: boolean }) {
const tableNameWithSchema = this.getTableNameWithSchema();
if (lodash.isString(tableNameWithSchema)) {
return tableNameWithSchema;
}
const schema = tableNameWithSchema.schema;
const tableName = tableNameWithSchema.tableName;
if (options?.ignorePublicSchema && schema === 'public') {
return tableName;
}
return `${schema}.${tableName}`;
}
public getTableNameWithSchemaAsString() {
const tableName = this.model.tableName;

View File

@ -310,6 +310,12 @@ export class Database extends EventEmitter implements AsyncEmitter {
}
initListener() {
this.on('afterConnect', async (client) => {
if (this.inDialect('postgres')) {
await client.query('SET search_path = public');
}
});
this.on('beforeDefine', (model, options) => {
if (this.options.underscored && options.underscored === undefined) {
options.underscored = true;
@ -381,6 +387,32 @@ export class Database extends EventEmitter implements AsyncEmitter {
}
});
this.on('afterRepositoryFind', ({ findOptions, dataCollection, data }) => {
if (dataCollection.isParent()) {
for (const row of data) {
const rowCollectionName = this.tableNameCollectionMap.get(
findOptions.raw
? `${row['__schemaName']}.${row['__tableName']}`
: `${row.get('__schemaName')}.${row.get('__tableName')}`,
).name;
if (!rowCollectionName) {
throw new Error(
`Can not find collection by table name ${rowCollectionName}, current collections: ${Array.from(
this.tableNameCollectionMap.keys(),
).join(', ')}`,
);
}
findOptions.raw
? (row['__collection'] = rowCollectionName)
: row.set('__collection', rowCollectionName, {
raw: true,
});
}
}
});
registerBuiltInListeners(this);
}

View File

@ -0,0 +1,24 @@
import { Op, Sequelize } from 'sequelize';
const mapVal = (values, db) =>
values.map((v) => {
const collection = db.getCollection(v);
return Sequelize.literal(`'${collection.tableNameAsString()}'::regclass`);
});
export default {
$childIn(values, ctx: any) {
const db = ctx.db;
return {
[Op.in]: mapVal(values, db),
};
},
$childNotIn(values, ctx: any) {
const db = ctx.db;
return {
[Op.notIn]: mapVal(values, db),
};
},
} as Record<string, any>;

View File

@ -7,4 +7,5 @@ export default {
...require('./ne').default,
...require('./notIn').default,
...require('./boolean').default,
...require('./child-collection').default,
};

View File

@ -80,10 +80,18 @@ export abstract class MultipleRelationRepository extends RelationRepository {
});
}
return await sourceModel[getAccessor]({
const data = await sourceModel[getAccessor]({
...findOptions,
transaction,
});
await this.collection.db.emitAsync('afterRepositoryFind', {
findOptions: options,
dataCollection: this.collection,
data,
});
return data;
}
async findAndCount(options?: FindAndCountOptions): Promise<[any[], number]> {

View File

@ -337,21 +337,11 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
});
}
if (this.collection.isParent()) {
for (const row of rows) {
const rowCollectionName = this.database.tableNameCollectionMap.get(
options.raw
? `${row['__schemaName']}.${row['__tableName']}`
: `${row.get('__schemaName')}.${row.get('__tableName')}`,
).name;
options.raw
? (row['__collection'] = rowCollectionName)
: row.set('__collection', rowCollectionName, {
raw: true,
});
}
}
await this.collection.db.emitAsync('afterRepositoryFind', {
findOptions: options,
dataCollection: this.collection,
data: rows,
});
return rows;
}

View File

@ -65,7 +65,6 @@ export class SyncRunner {
const match = regex.exec(columnDefault);
const sequenceName = match[1];
const sequenceCurrentValResult = await queryInterface.sequelize.query(
`select last_value
from ${sequenceName}`,
@ -97,19 +96,16 @@ export class SyncRunner {
const tableName = sequenceTable.tableName;
const schemaName = sequenceTable.schema;
const queryName = Boolean(tableName.match(/[A-Z]/)) && !tableName.includes(`"`) ? `"${tableName}"` : tableName;
const idColumnQuery = await queryInterface.sequelize.query(
`SELECT column_name
const idColumnSql = `SELECT column_name
FROM information_schema.columns
WHERE table_name = '${queryName}'
WHERE table_name = '${tableName}'
and column_name = 'id'
and table_schema = '${schemaName}';
`,
{
transaction,
},
);
`;
const idColumnQuery = await queryInterface.sequelize.query(idColumnSql, {
transaction,
});
if (idColumnQuery[0].length == 0) {
continue;

View File

@ -51,4 +51,83 @@ pgOnly()('Inherited Collection with schema options', () => {
context: {},
});
});
it('should list parent collection with children in difference schema in same table name', async () => {
await collectionRepository.create({
values: {
name: 'fakeParent',
schema: 'fake_schema',
tableName: 'parent',
},
});
const parent = await collectionRepository.create({
values: {
name: 'parent',
schema: 'rootSchema',
fields: [
{
type: 'string',
name: 'name',
},
],
},
context: {},
});
const child1 = await collectionRepository.create({
values: {
name: 'child1',
schema: 'child_1',
inherits: [parent.get('name')],
},
context: {},
});
// same table name with "otherTable" but in different schema
const child2 = await collectionRepository.create({
values: {
name: 'child2',
schema: 'public',
tableName: 'child2',
inherits: [parent.get('name')],
},
context: {},
});
const otherTable = await collectionRepository.create({
values: {
name: 'otherTable',
schema: 'other_schema',
tableName: 'child2',
},
context: {},
});
await db.getCollection('parent').repository.create({
values: {
name: 'parent',
},
});
await db.getCollection('child2').repository.create({
values: {
name: 'child2-1',
},
});
await db.getCollection('child1').repository.create({
values: {
name: 'chid1-1',
},
});
await db.getCollection('otherTable').repository.create({
values: {},
});
const list = await db.getCollection('parent').repository.find({});
const child2Record = list.find((item) => item.get('name') === 'child2-1');
expect(child2Record.get('__collection')).toBe(child2.get('name'));
});
});