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);
|
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 () => {
|
test('limit field name length', async () => {
|
||||||
const longFieldName =
|
const longFieldName =
|
||||||
'this_is_a_very_long_field_name_that_should_be_truncated_this_is_a_very_long_field_name_that_should_be_truncated';
|
'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 Database from '../../database';
|
||||||
import { InheritedCollection } from '../../inherited-collection';
|
import { InheritedCollection } from '../../inherited-collection';
|
||||||
import { mockDatabase } from '../index';
|
import { mockDatabase } from '../index';
|
||||||
|
import { BelongsToManyRepository } from '@nocobase/database';
|
||||||
import pgOnly from './helper';
|
import pgOnly from './helper';
|
||||||
|
|
||||||
pgOnly()('collection inherits', () => {
|
pgOnly()('collection inherits', () => {
|
||||||
@ -15,6 +16,119 @@ pgOnly()('collection inherits', () => {
|
|||||||
await db.close();
|
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 () => {
|
it('should create inherits with table name contains upperCase', async () => {
|
||||||
db.collection({
|
db.collection({
|
||||||
name: 'parent',
|
name: 'parent',
|
||||||
|
@ -140,7 +140,11 @@ export class Collection<
|
|||||||
private checkTableName() {
|
private checkTableName() {
|
||||||
const tableName = this.tableName();
|
const tableName = this.tableName();
|
||||||
for (const [k, collection] of this.db.collections) {
|
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}"`);
|
throw new Error(`collection ${collection.name} and ${this.name} have same tableName "${tableName}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -609,6 +613,22 @@ export class Collection<
|
|||||||
return tableName;
|
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() {
|
public getTableNameWithSchemaAsString() {
|
||||||
const tableName = this.model.tableName;
|
const tableName = this.model.tableName;
|
||||||
|
|
||||||
|
@ -310,6 +310,12 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initListener() {
|
initListener() {
|
||||||
|
this.on('afterConnect', async (client) => {
|
||||||
|
if (this.inDialect('postgres')) {
|
||||||
|
await client.query('SET search_path = public');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.on('beforeDefine', (model, options) => {
|
this.on('beforeDefine', (model, options) => {
|
||||||
if (this.options.underscored && options.underscored === undefined) {
|
if (this.options.underscored && options.underscored === undefined) {
|
||||||
options.underscored = true;
|
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);
|
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('./ne').default,
|
||||||
...require('./notIn').default,
|
...require('./notIn').default,
|
||||||
...require('./boolean').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,
|
...findOptions,
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.collection.db.emitAsync('afterRepositoryFind', {
|
||||||
|
findOptions: options,
|
||||||
|
dataCollection: this.collection,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAndCount(options?: FindAndCountOptions): Promise<[any[], number]> {
|
async findAndCount(options?: FindAndCountOptions): Promise<[any[], number]> {
|
||||||
|
@ -337,21 +337,11 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.collection.isParent()) {
|
await this.collection.db.emitAsync('afterRepositoryFind', {
|
||||||
for (const row of rows) {
|
findOptions: options,
|
||||||
const rowCollectionName = this.database.tableNameCollectionMap.get(
|
dataCollection: this.collection,
|
||||||
options.raw
|
data: rows,
|
||||||
? `${row['__schemaName']}.${row['__tableName']}`
|
|
||||||
: `${row.get('__schemaName')}.${row.get('__tableName')}`,
|
|
||||||
).name;
|
|
||||||
|
|
||||||
options.raw
|
|
||||||
? (row['__collection'] = rowCollectionName)
|
|
||||||
: row.set('__collection', rowCollectionName, {
|
|
||||||
raw: true,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,6 @@ export class SyncRunner {
|
|||||||
const match = regex.exec(columnDefault);
|
const match = regex.exec(columnDefault);
|
||||||
|
|
||||||
const sequenceName = match[1];
|
const sequenceName = match[1];
|
||||||
|
|
||||||
const sequenceCurrentValResult = await queryInterface.sequelize.query(
|
const sequenceCurrentValResult = await queryInterface.sequelize.query(
|
||||||
`select last_value
|
`select last_value
|
||||||
from ${sequenceName}`,
|
from ${sequenceName}`,
|
||||||
@ -97,19 +96,16 @@ export class SyncRunner {
|
|||||||
const tableName = sequenceTable.tableName;
|
const tableName = sequenceTable.tableName;
|
||||||
const schemaName = sequenceTable.schema;
|
const schemaName = sequenceTable.schema;
|
||||||
|
|
||||||
const queryName = Boolean(tableName.match(/[A-Z]/)) && !tableName.includes(`"`) ? `"${tableName}"` : tableName;
|
const idColumnSql = `SELECT column_name
|
||||||
|
|
||||||
const idColumnQuery = await queryInterface.sequelize.query(
|
|
||||||
`SELECT column_name
|
|
||||||
FROM information_schema.columns
|
FROM information_schema.columns
|
||||||
WHERE table_name = '${queryName}'
|
WHERE table_name = '${tableName}'
|
||||||
and column_name = 'id'
|
and column_name = 'id'
|
||||||
and table_schema = '${schemaName}';
|
and table_schema = '${schemaName}';
|
||||||
`,
|
`;
|
||||||
{
|
|
||||||
|
const idColumnQuery = await queryInterface.sequelize.query(idColumnSql, {
|
||||||
transaction,
|
transaction,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (idColumnQuery[0].length == 0) {
|
if (idColumnQuery[0].length == 0) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -51,4 +51,83 @@ pgOnly()('Inherited Collection with schema options', () => {
|
|||||||
context: {},
|
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