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:
parent
d37cadee6a
commit
242977983b
@ -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';
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
24
packages/core/database/src/operators/child-collection.ts
Normal file
24
packages/core/database/src/operators/child-collection.ts
Normal 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>;
|
@ -7,4 +7,5 @@ export default {
|
||||
...require('./ne').default,
|
||||
...require('./notIn').default,
|
||||
...require('./boolean').default,
|
||||
...require('./child-collection').default,
|
||||
};
|
||||
|
@ -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]> {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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'));
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user