fix: load view collection belongs to association with source options (#3912)

* chore: view collection belongs to source field test

* chore: test

* fix: load field with source attribute

* chore: test
This commit is contained in:
ChengLei Shao 2024-04-05 09:49:23 +08:00 committed by GitHub
parent e7187e536d
commit 4ac2875d51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 214 additions and 13 deletions

View File

@ -1,10 +1,7 @@
import { Collection } from '../../collection';
import Database from '../../database';
import { InheritedCollection } from '../../inherited-collection';
import { mockDatabase } from '../index';
import pgOnly from './helper';
pgOnly()('sync inherits', () => {
describe.runIf(process.env['DB_DIALECT'] === 'postgres')('sync inherits', () => {
let db: Database;
beforeEach(async () => {

View File

@ -1,3 +0,0 @@
const pgOnly = () => (process.env.DB_DIALECT == 'postgres' ? describe : describe.skip);
export default pgOnly;

View File

@ -2,9 +2,8 @@ import { vi } from 'vitest';
import { uid } from '@nocobase/utils';
import { Database, mockDatabase } from '../../index';
import { ViewCollection } from '../../view-collection';
import pgOnly from '../inhertits/helper';
pgOnly()('', () => {
describe.runIf(process.env['DB_DIALECT'] === 'postgres')('pg only view', () => {
let db: Database;
beforeEach(async () => {
@ -454,4 +453,90 @@ describe('create view', () => {
const viewNameField = ViewCollection.getField('name');
expect(viewNameField.options.patterns).toEqual(UserCollection.getField('name').options.patterns);
});
it('should set belongs to field via source', async () => {
const User = db.collection({
name: 'users',
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'hasMany',
name: 'posts',
target: 'posts',
foreignKey: 'userId',
},
],
});
const Post = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'belongsTo',
name: 'user',
foreignKey: 'userId',
target: 'users',
},
],
});
await db.sync();
await User.repository.create({
values: {
name: 'foo',
posts: [
{
title: 'bar',
},
{
title: 'baz',
},
],
},
});
const viewName = 'posts_view';
const dropViewSQL = `DROP VIEW IF EXISTS ${viewName}`;
await db.sequelize.query(dropViewSQL);
const viewSQL = `
CREATE VIEW ${viewName} as SELECT users.* FROM ${Post.quotedTableName()} as users
`;
await db.sequelize.query(viewSQL);
// create view collection
const ViewCollection = db.collection({
name: viewName,
view: true,
fields: [
{
name: 'title',
type: 'string',
source: 'posts.name',
},
{
name: 'user',
type: 'belongsTo',
source: 'posts.user',
},
],
schema: db.inDialect('postgres') ? 'public' : undefined,
});
const post = await ViewCollection.repository.findOne({
appends: ['user'],
});
expect(post['user']['name']).toBe('foo');
});
});

View File

@ -325,13 +325,15 @@ export class Collection<
this.db.logger.warn(
`source collection "${sourceCollectionName}" not found for field "${name}" at collection "${this.name}"`,
);
return null;
} else {
const sourceField = sourceCollection.fields.get(sourceFieldName);
if (!sourceField) {
this.db.logger.warn(
`source field "${sourceFieldName}" not found for field "${name}" at collection "${this.name}"`,
`Source field "${sourceFieldName}" not found for field "${name}" at collection "${this.name}". Source collection: "${sourceCollectionName}"`,
);
return null;
} else {
options = { ...lodash.omit(sourceField.options, ['name', 'primaryKey']), ...options };
}

View File

@ -149,6 +149,101 @@ describe('view collection', function () {
}
});
it('should load view collection belongs to field', async () => {
await collectionRepository.create({
values: {
name: 'users',
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'hasMany',
name: 'posts',
target: 'posts',
foreignKey: 'userId',
},
],
},
context: {},
});
await collectionRepository.create({
values: {
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'belongsTo',
name: 'user',
foreignKey: 'userId',
target: 'users',
},
],
},
context: {},
});
await db.getRepository('users').create({
values: [
{
name: 'u1',
posts: [
{
title: 'p1',
},
],
},
],
});
const Post = db.getCollection('posts');
const viewName = `test_view_${uid(6)}`;
await db.sequelize.query(`DROP VIEW IF EXISTS ${viewName}`);
const viewSQL = `
CREATE VIEW ${viewName} as SELECT users.* FROM ${Post.quotedTableName()} as users
`;
await db.sequelize.query(viewSQL);
await collectionRepository.create({
values: {
name: viewName,
view: true,
fields: [
{
name: 'title',
type: 'string',
source: 'posts.title',
},
{
name: 'user',
type: 'belongsTo',
source: 'posts.user',
},
],
schema: db.inDialect('postgres') ? 'public' : undefined,
},
context: {},
});
// recall loadFields
await app.runCommand('restart');
db = app.db;
const viewCollection = db.getCollection(viewName);
await viewCollection.repository.find({
appends: ['user'],
});
});
it('should use view collection as through collection', async () => {
const User = await collectionRepository.create({
values: {
@ -168,8 +263,6 @@ describe('view collection', function () {
const UserCollection = db.getCollection('users');
console.log(UserCollection);
await db.getRepository('users').create({
values: [{ name: 'u1' }, { name: 'u2' }],
});

View File

@ -91,11 +91,14 @@ export class CollectionRepository extends Repository {
submodule: 'CollectionRepository',
method: 'load',
});
this.app.setMaintainingMessage(`load ${instanceName} collection`);
await nameMap[instanceName].load({ skipField });
}
const fieldWithSourceAttributes = new Map<string, Array<string>>();
// load view fields
for (const viewCollectionName of viewCollections) {
this.database.logger.debug(`load collection fields`, {
@ -103,8 +106,20 @@ export class CollectionRepository extends Repository {
method: 'load',
viewCollectionName,
});
const skipField = (() => {
const fields = nameMap[viewCollectionName].get('fields');
return fields.filter((field) => field.options?.source).map((field) => field.get('name'));
})();
this.app.setMaintainingMessage(`load ${viewCollectionName} collection fields`);
await nameMap[viewCollectionName].loadFields({});
if (lodash.isArray(skipField) && skipField.length) {
fieldWithSourceAttributes.set(viewCollectionName, skipField);
}
await nameMap[viewCollectionName].loadFields({ skipField });
}
// load lazy collection field
@ -117,6 +132,18 @@ export class CollectionRepository extends Repository {
this.app.setMaintainingMessage(`load ${collectionName} collection fields`);
await nameMap[collectionName].loadFields({ includeFields: skipField });
}
// load source attribute fields
for (const [collectionName, skipField] of fieldWithSourceAttributes) {
this.database.logger.debug(`load collection fields`, {
submodule: 'CollectionRepository',
method: 'load',
collectionName,
});
this.app.setMaintainingMessage(`load ${collectionName} collection fields`);
await nameMap[collectionName].loadFields({ includeFields: skipField });
}
}
async db2cm(collectionName: string) {