From 4ac2875d51351b8d935d9fb8fdfdd374d5141094 Mon Sep 17 00:00:00 2001 From: ChengLei Shao Date: Fri, 5 Apr 2024 09:49:23 +0800 Subject: [PATCH] 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 --- .../collection-inherits-sync.test.ts | 5 +- .../src/__tests__/inhertits/helper.ts | 3 - .../__tests__/view/view-collection.test.ts | 89 ++++++++++++++++- packages/core/database/src/collection.ts | 4 +- .../__tests__/view/view-collection.test.ts | 97 ++++++++++++++++++- .../repositories/collection-repository.ts | 29 +++++- 6 files changed, 214 insertions(+), 13 deletions(-) delete mode 100644 packages/core/database/src/__tests__/inhertits/helper.ts diff --git a/packages/core/database/src/__tests__/inhertits/collection-inherits-sync.test.ts b/packages/core/database/src/__tests__/inhertits/collection-inherits-sync.test.ts index 1930b42e5f..5462884225 100644 --- a/packages/core/database/src/__tests__/inhertits/collection-inherits-sync.test.ts +++ b/packages/core/database/src/__tests__/inhertits/collection-inherits-sync.test.ts @@ -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 () => { diff --git a/packages/core/database/src/__tests__/inhertits/helper.ts b/packages/core/database/src/__tests__/inhertits/helper.ts deleted file mode 100644 index c9a195ae8a..0000000000 --- a/packages/core/database/src/__tests__/inhertits/helper.ts +++ /dev/null @@ -1,3 +0,0 @@ -const pgOnly = () => (process.env.DB_DIALECT == 'postgres' ? describe : describe.skip); - -export default pgOnly; diff --git a/packages/core/database/src/__tests__/view/view-collection.test.ts b/packages/core/database/src/__tests__/view/view-collection.test.ts index a4ccaffa2d..cdb8938505 100644 --- a/packages/core/database/src/__tests__/view/view-collection.test.ts +++ b/packages/core/database/src/__tests__/view/view-collection.test.ts @@ -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'); + }); }); diff --git a/packages/core/database/src/collection.ts b/packages/core/database/src/collection.ts index 39430e4019..dc997bc651 100644 --- a/packages/core/database/src/collection.ts +++ b/packages/core/database/src/collection.ts @@ -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 }; } diff --git a/packages/plugins/@nocobase/plugin-collection-manager/src/server/__tests__/view/view-collection.test.ts b/packages/plugins/@nocobase/plugin-collection-manager/src/server/__tests__/view/view-collection.test.ts index 223dad0293..5d7ddae2b6 100644 --- a/packages/plugins/@nocobase/plugin-collection-manager/src/server/__tests__/view/view-collection.test.ts +++ b/packages/plugins/@nocobase/plugin-collection-manager/src/server/__tests__/view/view-collection.test.ts @@ -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' }], }); diff --git a/packages/plugins/@nocobase/plugin-collection-manager/src/server/repositories/collection-repository.ts b/packages/plugins/@nocobase/plugin-collection-manager/src/server/repositories/collection-repository.ts index 962c8d9b08..44d9547789 100644 --- a/packages/plugins/@nocobase/plugin-collection-manager/src/server/repositories/collection-repository.ts +++ b/packages/plugins/@nocobase/plugin-collection-manager/src/server/repositories/collection-repository.ts @@ -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>(); + // 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) {