🐛 fix(database): prevent empty array insertion in aiModel batch operations (#9491)

* 🐛 fix(database): prevent empty array insertion in aiModel batch operations

- Add validation to batchUpdateAiModels to return early if models array is empty
- Add validation to batchToggleAiModels to return early if models array is empty
- Add validation to updateModelsOrder to return early if sortMap array is empty
- Fixes 'values() must be called with at least one value' error when OpenRouter returns empty model list

Fixes #9429

Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>

*  test(database): add tests for empty array validation in aiModel batch operations

- Add test for batchUpdateAiModels with empty array returning empty result
- Add test for batchToggleAiModels with empty array returning early
- Add test for updateModelsOrder with empty sortMap returning early

These tests verify the fix for issue #9429 where empty arrays caused
"values() must be called with at least one value" database errors.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>

* 🐛 fix(test): remove invalid sort property access in aiModel test

- Remove test assertion accessing sort property on AiProviderModelListItem
- AiProviderModelListItem interface doesn't include sort property
- Fix TypeScript error: Property 'sort' does not exist on type 'AiProviderModelListItem'

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>

* ♻️ refactor(database): extract shared validation helper for empty arrays

- Add private isEmptyArray() helper method to AiModelModel class
- Replace duplicate empty array checks in batch methods with shared helper
- Improve code maintainability and reduce duplication
- Address Sourcery AI feedback for better code organization

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>

* 🐛 fix(database): respect enabled parameter in aiModel create method

The create method was forcing enabled: true regardless of input.
This fix allows explicit enabled: false while maintaining true as default.

Fixes failing test: batchToggleAiModels empty array validation.

Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>
This commit is contained in:
Arvin Xu
2025-10-05 17:42:39 +02:00
committed by GitHub
parent c25492e377
commit eb50c8b781
2 changed files with 68 additions and 1 deletions
@@ -254,6 +254,15 @@ describe('AiModelModel', () => {
expect(allModels.find((m) => m.id === 'existing-model')?.displayName).toBe('Old Name');
expect(allModels.find((m) => m.id === 'new-model')?.displayName).toBe('New Model');
});
it('should return empty array when models array is empty', async () => {
const result = await aiProviderModel.batchUpdateAiModels('openai', []);
expect(result).toEqual([]);
// Verify no models were created
const allModels = await aiProviderModel.query();
expect(allModels).toHaveLength(0);
});
});
describe('batchToggleAiModels', () => {
@@ -274,6 +283,23 @@ describe('AiModelModel', () => {
const models = await aiProviderModel.query();
expect(models.every((m) => m.enabled)).toBe(true);
});
it('should return early when models array is empty', async () => {
// Create an initial model to verify it's not affected
await aiProviderModel.create({
id: 'model1',
providerId: 'openai',
enabled: false,
});
const result = await aiProviderModel.batchToggleAiModels('openai', [], true);
expect(result).toBeUndefined();
// Verify existing models were not affected
const models = await aiProviderModel.query();
expect(models).toHaveLength(1);
expect(models[0].enabled).toBe(false);
});
});
describe('clearRemoteModels', () => {
@@ -324,5 +350,22 @@ describe('AiModelModel', () => {
const model = await aiProviderModel.findById('image-model');
expect(model?.type).toBe('image');
});
it('should return early when sortMap array is empty', async () => {
// Create an initial model to verify it's not affected
await aiProviderModel.create({
id: 'model1',
providerId: 'openai',
sort: 1,
});
const result = await aiProviderModel.updateModelsOrder('openai', []);
expect(result).toBeUndefined();
// Verify existing models were not affected (check by querying the created model directly)
const models = await aiProviderModel.getModelListByProviderId('openai');
expect(models).toHaveLength(1);
expect(models[0].id).toBe('model1');
});
});
});
+25 -1
View File
@@ -19,12 +19,21 @@ export class AiModelModel {
this.db = db;
}
/**
* Helper method to validate if array is empty and return early if needed
* @param array - Array to validate
* @returns true if array is empty, false otherwise
*/
private isEmptyArray(array: unknown[]): boolean {
return array.length === 0;
}
create = async (params: NewAiModelItem) => {
const [result] = await this.db
.insert(aiModels)
.values({
...params,
enabled: true, // enabled by default
enabled: params.enabled ?? true, // enabled by default, but respect explicit value
source: AiModelSourceEnum.Custom,
userId: this.userId,
})
@@ -148,6 +157,11 @@ export class AiModelModel {
};
batchUpdateAiModels = async (providerId: string, models: AiProviderModelListItem[]) => {
// Early return if models array is empty to prevent database insertion error
if (this.isEmptyArray(models)) {
return [];
}
const records = models.map(({ id, ...model }) => ({
...model,
id,
@@ -166,6 +180,11 @@ export class AiModelModel {
};
batchToggleAiModels = async (providerId: string, models: string[], enabled: boolean) => {
// Early return if models array is empty to prevent database insertion error
if (this.isEmptyArray(models)) {
return;
}
return this.db.transaction(async (trx) => {
// 1. insert models that are not in the db
const insertedRecords = await trx
@@ -222,6 +241,11 @@ export class AiModelModel {
}
updateModelsOrder = async (providerId: string, sortMap: AiModelSortMap[]) => {
// Early return if sortMap array is empty
if (this.isEmptyArray(sortMap)) {
return;
}
await this.db.transaction(async (tx) => {
const updates = sortMap.map(({ id, sort, type }) => {
const now = new Date();