feat: infer belongs to association field in view collection (#1756)
support belongs to field in view collection
This commit is contained in:
parent
60c8d531ef
commit
c7b9e6ac51
@ -1,5 +1,6 @@
|
||||
import { Database, mockDatabase } from '@nocobase/database';
|
||||
import { ViewFieldInference } from '../../view/view-inference';
|
||||
import { uid } from '@nocobase/utils';
|
||||
|
||||
describe('view inference', function () {
|
||||
let db: Database;
|
||||
|
@ -454,6 +454,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
})();
|
||||
|
||||
const collection = new collectionKlass(options, { database: this });
|
||||
|
||||
this.collections.set(collection.name, collection);
|
||||
|
||||
this.emit('afterDefineCollection', collection);
|
||||
@ -630,7 +631,9 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
throw Error(`unsupported field type ${type}`);
|
||||
}
|
||||
|
||||
if (options.field && context.collection.options.underscored) {
|
||||
const { collection } = context;
|
||||
|
||||
if (options.field && collection.options.underscored && !collection.isView()) {
|
||||
options.field = snakeCase(options.field);
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@ export class ViewCollection extends Collection {
|
||||
constructor(options: CollectionOptions, context: CollectionContext) {
|
||||
options.autoGenId = false;
|
||||
options.timestamps = false;
|
||||
options.underscored = false;
|
||||
|
||||
super(options, context);
|
||||
}
|
||||
|
@ -30,52 +30,84 @@ export class ViewFieldInference {
|
||||
schema: options.viewSchema,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
return Object.fromEntries(
|
||||
Object.entries(columns).map(([name, column]) => {
|
||||
const usage = columnUsage[name];
|
||||
const rawFields = [];
|
||||
for (const [name, column] of Object.entries(columns)) {
|
||||
const inferResult: any = { name };
|
||||
|
||||
if (usage) {
|
||||
const collectionField = (() => {
|
||||
const tableName = `${usage.table_schema ? `${usage.table_schema}.` : ''}${usage.table_name}`;
|
||||
const collection = db.tableNameCollectionMap.get(tableName);
|
||||
if (!collection) return false;
|
||||
const usage = columnUsage[name];
|
||||
|
||||
const fieldValue = Object.values(collection.model.rawAttributes).find(
|
||||
(field) => field.field === usage.column_name,
|
||||
);
|
||||
if (usage) {
|
||||
const collection = db.tableNameCollectionMap.get(
|
||||
`${usage.table_schema ? `${usage.table_schema}.` : ''}${usage.table_name}`,
|
||||
);
|
||||
|
||||
if (!fieldValue) {
|
||||
return false;
|
||||
}
|
||||
const collectionField = (() => {
|
||||
if (!collection) return false;
|
||||
|
||||
// @ts-ignore
|
||||
const fieldName = fieldValue?.fieldName;
|
||||
const fieldAttribute = Object.values(collection.model.rawAttributes).find(
|
||||
(field) => field.field === usage.column_name,
|
||||
);
|
||||
|
||||
return collection.getField(fieldName);
|
||||
})();
|
||||
|
||||
if (collectionField && collectionField.options.interface) {
|
||||
return [
|
||||
name,
|
||||
{
|
||||
name,
|
||||
type: collectionField.type,
|
||||
source: `${collectionField.collection.name}.${collectionField.name}`,
|
||||
},
|
||||
];
|
||||
if (!fieldAttribute) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const fieldName = fieldAttribute.fieldName;
|
||||
|
||||
return collection.getField(fieldName);
|
||||
})();
|
||||
|
||||
const belongsToAssociationField = (() => {
|
||||
if (!collection) return false;
|
||||
|
||||
const field = Object.values(collection.model.rawAttributes).find(
|
||||
(field) => field.field === usage.column_name,
|
||||
);
|
||||
|
||||
if (!field) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const association = Object.values(collection.model.associations).find(
|
||||
(association) =>
|
||||
association.associationType === 'BelongsTo' && association.foreignKey === (field as any).fieldName,
|
||||
);
|
||||
|
||||
if (!association) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return collection.getField(association.as);
|
||||
})();
|
||||
|
||||
if (belongsToAssociationField) {
|
||||
rawFields.push([
|
||||
belongsToAssociationField.name,
|
||||
{
|
||||
name: belongsToAssociationField.name,
|
||||
type: belongsToAssociationField.type,
|
||||
source: `${belongsToAssociationField.collection.name}.${belongsToAssociationField.name}`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
name,
|
||||
{
|
||||
name,
|
||||
...this.inferToFieldType({ db, name, type: column.type }),
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
if (collectionField) {
|
||||
if (collectionField.options.interface) {
|
||||
inferResult.type = collectionField.type;
|
||||
inferResult.source = `${collectionField.collection.name}.${collectionField.name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!inferResult.type) {
|
||||
Object.assign(inferResult, this.inferToFieldType({ db, name, type: column.type }));
|
||||
}
|
||||
|
||||
rawFields.push([name, inferResult]);
|
||||
}
|
||||
|
||||
return Object.fromEntries(rawFields);
|
||||
}
|
||||
|
||||
static inferToFieldType(options: { db: Database; name: string; type: string }) {
|
||||
|
@ -1,11 +1,16 @@
|
||||
import { MockServer } from '@nocobase/test';
|
||||
import { createApp } from '../index';
|
||||
import { uid } from '@nocobase/utils';
|
||||
import { Database, Repository } from '@nocobase/database';
|
||||
|
||||
describe('view collection', () => {
|
||||
let app: MockServer;
|
||||
let db: Database;
|
||||
let agent;
|
||||
let testViewName;
|
||||
let collectionRepository: Repository;
|
||||
|
||||
let fieldsRepository: Repository;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp({
|
||||
@ -13,6 +18,12 @@ describe('view collection', () => {
|
||||
tablePrefix: '',
|
||||
},
|
||||
});
|
||||
|
||||
db = app.db;
|
||||
|
||||
collectionRepository = app.db.getCollection('collections').repository;
|
||||
fieldsRepository = app.db.getCollection('fields').repository;
|
||||
|
||||
agent = app.agent();
|
||||
testViewName = `view_${uid(6)}`;
|
||||
const dropSQL = `DROP VIEW IF EXISTS ${testViewName}`;
|
||||
@ -408,4 +419,80 @@ SELECT * FROM numbers;
|
||||
const nameField = viewFieldsResponse.body.data.find((item) => item.name === 'name');
|
||||
expect(nameField.uiSchema.title).toEqual('bars');
|
||||
});
|
||||
|
||||
it('should create view collection with belongs to field', async () => {
|
||||
// not support sqlite
|
||||
if (db.inDialect('sqlite')) {
|
||||
return;
|
||||
}
|
||||
await collectionRepository.create({
|
||||
values: {
|
||||
name: 'groups',
|
||||
fields: [{ name: 'name', type: 'string' }],
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
await collectionRepository.create({
|
||||
values: {
|
||||
name: 'users',
|
||||
fields: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ type: 'belongsTo', name: 'group', foreignKey: 'groupId', interface: 'test-interface' },
|
||||
],
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
const User = db.getCollection('users');
|
||||
|
||||
const assoc = User.model.associations.group;
|
||||
const foreignKey = assoc.foreignKey;
|
||||
const foreignField = User.model.rawAttributes[foreignKey].field;
|
||||
|
||||
const viewName = `test_view_${uid(6)}`;
|
||||
await db.sequelize.query(`DROP VIEW IF EXISTS ${viewName}`);
|
||||
const queryInterface = db.sequelize.getQueryInterface();
|
||||
|
||||
const createSQL = `CREATE VIEW ${queryInterface.quoteIdentifier(
|
||||
viewName,
|
||||
)} AS SELECT id, ${queryInterface.quoteIdentifier(foreignField)}, name FROM ${db
|
||||
.getCollection('users')
|
||||
.quotedTableName()}`;
|
||||
|
||||
await db.sequelize.query(createSQL);
|
||||
|
||||
const response = await agent.resource('dbViews').get({
|
||||
filterByTk: viewName,
|
||||
schema: db.inDialect('postgres') ? 'public' : undefined,
|
||||
pageSize: 20,
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
const fields = response.body.data.fields;
|
||||
|
||||
await collectionRepository.create({
|
||||
values: {
|
||||
name: viewName,
|
||||
view: true,
|
||||
fields: Object.values(fields),
|
||||
schema: db.inDialect('postgres') ? 'public' : undefined,
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
const viewFieldsResponse = await agent.resource('collections.fields', viewName).list({});
|
||||
expect(viewFieldsResponse.status).toEqual(200);
|
||||
const viewFields = viewFieldsResponse.body.data;
|
||||
const groupField = viewFields.find((item) => item.name === 'group');
|
||||
|
||||
expect(groupField.type).toEqual('belongsTo');
|
||||
expect(groupField.interface).toEqual('test-interface');
|
||||
|
||||
const listResponse1 = await agent.resource(viewName).list({
|
||||
appends: ['group'],
|
||||
});
|
||||
|
||||
expect(listResponse1.status).toEqual(200);
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Database, { Repository, ViewCollection } from '@nocobase/database';
|
||||
import Database, { Repository, ViewCollection, ViewFieldInference } from '@nocobase/database';
|
||||
import Application from '@nocobase/server';
|
||||
import { createApp } from '../index';
|
||||
import { uid } from '@nocobase/utils';
|
||||
@ -28,6 +28,68 @@ describe('view collection', function () {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should create view collection with belongs to association', async () => {
|
||||
await collectionRepository.create({
|
||||
values: {
|
||||
name: 'groups',
|
||||
fields: [{ name: 'name', type: 'string' }],
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
await collectionRepository.create({
|
||||
values: {
|
||||
name: 'users',
|
||||
fields: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ type: 'belongsTo', name: 'group', foreignKey: 'group_id' },
|
||||
],
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
const User = db.getCollection('users');
|
||||
|
||||
const assoc = User.model.associations.group;
|
||||
const foreignKey = assoc.foreignKey;
|
||||
const foreignField = User.model.rawAttributes[foreignKey].field;
|
||||
|
||||
const viewName = `test_view_${uid(6)}`;
|
||||
await db.sequelize.query(`DROP VIEW IF EXISTS ${viewName}`);
|
||||
|
||||
const createSQL = `CREATE VIEW ${viewName} AS SELECT id, ${foreignField}, name FROM ${db
|
||||
.getCollection('users')
|
||||
.quotedTableName()}`;
|
||||
|
||||
await db.sequelize.query(createSQL);
|
||||
|
||||
const inferredFields = await ViewFieldInference.inferFields({
|
||||
db,
|
||||
viewName,
|
||||
viewSchema: 'public',
|
||||
});
|
||||
|
||||
if (!db.inDialect('sqlite')) {
|
||||
expect(inferredFields['group_id'].type).toBe('bigInt');
|
||||
|
||||
expect(inferredFields['group'].type).toBe('belongsTo');
|
||||
|
||||
await collectionRepository.create({
|
||||
values: {
|
||||
name: viewName,
|
||||
view: true,
|
||||
fields: Object.values(inferredFields),
|
||||
schema: db.inDialect('postgres') ? 'public' : undefined,
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
|
||||
const viewCollection = db.getCollection(viewName);
|
||||
const group = viewCollection.getField('group');
|
||||
expect(group.foreignKey).toEqual('group_id');
|
||||
}
|
||||
});
|
||||
|
||||
it('should use view collection as through collection', async () => {
|
||||
const User = await collectionRepository.create({
|
||||
values: {
|
||||
@ -235,7 +297,7 @@ describe('view collection', function () {
|
||||
name: 'view_collection',
|
||||
viewName: 'test_view',
|
||||
isView: true,
|
||||
fields: [{ type: 'string', name: 'Uppercase', field: 'Uppercase' }],
|
||||
fields: [{ type: 'string', name: 'upper_case', field: 'Uppercase' }],
|
||||
schema: db.inDialect('postgres') ? 'public' : undefined,
|
||||
},
|
||||
context: {},
|
||||
@ -243,7 +305,7 @@ describe('view collection', function () {
|
||||
|
||||
const viewCollection = db.getCollection('view_collection');
|
||||
|
||||
expect(viewCollection.model.rawAttributes['Uppercase'].field).toEqual('Uppercase');
|
||||
expect(viewCollection.model.rawAttributes['upper_case'].field).toEqual('Uppercase');
|
||||
const results = await viewCollection.repository.find();
|
||||
expect(results.length).toBe(1);
|
||||
});
|
||||
|
@ -105,7 +105,18 @@ export function afterCreateForForeignKeyField(db: Database) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, interface: interfaceType, collectionName, target, through, foreignKey, otherKey } = model.get();
|
||||
const {
|
||||
type,
|
||||
interface: interfaceType,
|
||||
collectionName,
|
||||
target,
|
||||
through,
|
||||
foreignKey,
|
||||
otherKey,
|
||||
source,
|
||||
} = model.get();
|
||||
|
||||
if (source) return;
|
||||
|
||||
// foreign key in target collection
|
||||
if (['oho', 'o2m'].includes(interfaceType)) {
|
||||
|
@ -100,6 +100,7 @@ export class CollectionManagerPlugin extends Plugin {
|
||||
});
|
||||
|
||||
this.app.db.on('fields.beforeCreate', async (model, options) => {
|
||||
if (model.get('source')) return;
|
||||
const type = model.get('type');
|
||||
const fn = beforeInitOptions[type];
|
||||
if (fn) {
|
||||
|
Loading…
Reference in New Issue
Block a user