mirror of
https://github.com/nocobase/nocobase.git
synced 2026-09-01 14:57:36 +08:00
fix(database): use warning instead of error when any of appends parameters invalid (#8923)
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { createMockDatabase, Database } from '@nocobase/database';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
describe('find with invalid appends', () => {
|
||||
let db: Database;
|
||||
let warnSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = await createMockDatabase();
|
||||
await db.clean({ drop: true });
|
||||
warnSpy = vi.spyOn(db.logger, 'warn');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
warnSpy.mockRestore();
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('should warn and skip when appending a non-existent association', async () => {
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
fields: [{ name: 'name', type: 'string' }],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
await User.repository.create({ values: { name: 'u1' } });
|
||||
|
||||
const rows = await User.repository.find({
|
||||
appends: ['nonExistentRelation'],
|
||||
});
|
||||
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].get('name')).toBe('u1');
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
expect(warnSpy.mock.calls[0][0]).toContain('nonExistentRelation');
|
||||
});
|
||||
|
||||
it('should warn and skip multiple non-existent associations', async () => {
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
fields: [{ name: 'name', type: 'string' }],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
await User.repository.create({ values: { name: 'u1' } });
|
||||
|
||||
const rows = await User.repository.find({
|
||||
appends: ['badRelation1', 'badRelation2'],
|
||||
});
|
||||
|
||||
expect(rows.length).toBe(1);
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
expect(warnSpy.mock.calls[0][0]).toContain('badRelation1');
|
||||
expect(warnSpy.mock.calls[0][0]).toContain('badRelation2');
|
||||
});
|
||||
|
||||
it('should still load valid appends when mixed with non-existent ones', async () => {
|
||||
db.collection({
|
||||
name: 'profiles',
|
||||
fields: [{ name: 'bio', type: 'string' }],
|
||||
});
|
||||
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'profile', type: 'belongsTo', target: 'profiles' },
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
await User.repository.create({
|
||||
values: { name: 'u1', profile: { bio: 'hello' } },
|
||||
});
|
||||
|
||||
const rows = await User.repository.find({
|
||||
appends: ['profile', 'nonExistent'],
|
||||
});
|
||||
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].get('profile')).toBeTruthy();
|
||||
expect(rows[0].get('profile').get('bio')).toBe('hello');
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
expect(warnSpy.mock.calls[0][0]).toContain('nonExistent');
|
||||
expect(warnSpy.mock.calls[0][0]).not.toContain('profile');
|
||||
});
|
||||
|
||||
it('should warn when nested append has non-existent association', async () => {
|
||||
db.collection({
|
||||
name: 'profiles',
|
||||
fields: [{ name: 'bio', type: 'string' }],
|
||||
});
|
||||
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'profile', type: 'belongsTo', target: 'profiles' },
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
await User.repository.create({
|
||||
values: { name: 'u1', profile: { bio: 'hello' } },
|
||||
});
|
||||
|
||||
const rows = await User.repository.find({
|
||||
appends: ['profile.nonExistentNested'],
|
||||
});
|
||||
|
||||
expect(rows.length).toBe(1);
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
expect(warnSpy.mock.calls[0][0]).toContain('nonExistentNested');
|
||||
});
|
||||
|
||||
it('should not warn when all appends are valid', async () => {
|
||||
db.collection({
|
||||
name: 'profiles',
|
||||
fields: [{ name: 'bio', type: 'string' }],
|
||||
});
|
||||
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'profile', type: 'belongsTo', target: 'profiles' },
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync();
|
||||
|
||||
await User.repository.create({
|
||||
values: { name: 'u1', profile: { bio: 'hello' } },
|
||||
});
|
||||
|
||||
const rows = await User.repository.find({
|
||||
appends: ['profile'],
|
||||
});
|
||||
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].get('profile')).toBeTruthy();
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export class AssociationNotFoundError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'AssociationNotFoundError';
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import FilterParser from './filter-parser';
|
||||
import { Appends, Except, FindOptions } from './repository';
|
||||
import qs from 'qs';
|
||||
import { BelongsToArrayAssociation } from './belongs-to-array/belongs-to-array-repository';
|
||||
import { AssociationNotFoundError } from './errors/association-not-found-error';
|
||||
|
||||
const debug = require('debug')('noco-database');
|
||||
|
||||
@@ -30,6 +31,7 @@ export class OptionsParser {
|
||||
model: ModelStatic<any>;
|
||||
filterParser: FilterParser;
|
||||
context: OptionsParserContext;
|
||||
associationNotFoundWarnings: string[] = [];
|
||||
|
||||
constructor(options: FindOptions, context: OptionsParserContext) {
|
||||
const { collection } = context;
|
||||
@@ -372,7 +374,7 @@ export class OptionsParser {
|
||||
if (appendFields.length == 2) {
|
||||
const association = associations[appendFields[0]];
|
||||
if (!association) {
|
||||
throw new Error(`association ${appendFields[0]} in ${model.name} not found`);
|
||||
throw new AssociationNotFoundError(`association ${appendFields[0]} in ${model.name} not found`);
|
||||
}
|
||||
|
||||
const associationModel = associations[appendFields[0]].target;
|
||||
@@ -419,7 +421,13 @@ export class OptionsParser {
|
||||
// association not exists
|
||||
const association = associations[appendAssociation];
|
||||
if (!association) {
|
||||
throw new Error(`association ${appendAssociation} in ${model.name} not found`);
|
||||
throw new AssociationNotFoundError(`association ${appendAssociation} in ${model.name} not found`);
|
||||
}
|
||||
const targetCollectionName = this.database.getCollectionByModelName(association.target.name)?.name;
|
||||
if (!targetCollectionName) {
|
||||
throw new AssociationNotFoundError(
|
||||
`target collection for association ${appendAssociation} in ${model.name} not found`,
|
||||
);
|
||||
}
|
||||
let includeOptions = {
|
||||
association: appendAssociation,
|
||||
@@ -488,7 +496,15 @@ export class OptionsParser {
|
||||
|
||||
// handle every appends
|
||||
for (const append of sortedAppends) {
|
||||
setInclude(this.model, filterParams, append);
|
||||
try {
|
||||
setInclude(this.model, filterParams, append);
|
||||
} catch (error) {
|
||||
if (error instanceof AssociationNotFoundError) {
|
||||
this.associationNotFoundWarnings.push(error.message);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
debug('filter params: %o', filterParams);
|
||||
|
||||
@@ -51,6 +51,7 @@ import { updateAssociations, updateModelByValues } from './update-associations';
|
||||
import { UpdateGuard } from './update-guard';
|
||||
import { valuesToFilter } from './utils/filter-utils';
|
||||
import { processIncludes } from './utils';
|
||||
import { AssociationNotFoundError } from './errors/association-not-found-error';
|
||||
|
||||
const debug = require('debug')('noco-database');
|
||||
|
||||
@@ -945,6 +946,11 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
});
|
||||
|
||||
const params = parser.toSequelizeParams({ parseSort: _.isBoolean(options?.parseSort) ? options.parseSort : true });
|
||||
|
||||
if (parser.associationNotFoundWarnings.length > 0) {
|
||||
this.database.logger.warn(parser.associationNotFoundWarnings.join('; '));
|
||||
}
|
||||
|
||||
debug('sequelize query params %o', params);
|
||||
|
||||
if (options.where && params.where) {
|
||||
|
||||
Reference in New Issue
Block a user