fix(database): use warning instead of error when any of appends parameters invalid (#8923)

This commit is contained in:
Junyi
2026-03-20 21:38:01 +08:00
committed by GitHub
parent 41510e9dcb
commit 1d97b46e2a
4 changed files with 197 additions and 3 deletions
@@ -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';
}
}
+19 -3
View File
@@ -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);
+6
View File
@@ -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) {