test: add document repo testing
This commit is contained in:
@@ -1,28 +1,598 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
|
||||||
|
import { DatabaseService, tableNames } from '../database/database.ts';
|
||||||
|
|
||||||
import { DocumentsService } from './documents.ts';
|
import { DocumentsService } from './documents.ts';
|
||||||
|
import type { DocumentUpsert, DocumentSearchOptions, MetaCondition } from './documents.schemas.ts';
|
||||||
|
|
||||||
import { Services } from '#root/utils/services.ts';
|
import { Services } from '#root/utils/services.ts';
|
||||||
|
|
||||||
describe('DocumentsService', () => {
|
describe('DocumentsService', () => {
|
||||||
let services: Services;
|
let services: Services;
|
||||||
let documentsService: DocumentsService;
|
let documentsService: DocumentsService;
|
||||||
|
let databaseService: DatabaseService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
services = new Services();
|
services = new Services();
|
||||||
documentsService = services.get(DocumentsService);
|
documentsService = services.get(DocumentsService);
|
||||||
|
databaseService = services.get(DatabaseService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should upsert a document', async () => {
|
afterEach(async () => {
|
||||||
const document = {
|
const db = await databaseService.getDb();
|
||||||
uri: 'test',
|
await db(tableNames.documents).del();
|
||||||
type: 'test',
|
});
|
||||||
|
|
||||||
|
describe('upsert', () => {
|
||||||
|
it('should insert a new document', async () => {
|
||||||
|
const document: DocumentUpsert = {
|
||||||
|
uri: 'test-doc-1',
|
||||||
|
type: 'article',
|
||||||
metadata: {
|
metadata: {
|
||||||
test: 'test',
|
title: 'Test Article',
|
||||||
|
priority: 5,
|
||||||
|
published: true,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
test: 'test',
|
content: 'This is test content',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
await documentsService.upsert(document);
|
await documentsService.upsert(document);
|
||||||
|
|
||||||
|
const db = await databaseService.getDb();
|
||||||
|
const result = await db(tableNames.documents).where({ uri: 'test-doc-1', type: 'article' }).first();
|
||||||
|
|
||||||
|
// Handle PGLite returning raw result objects instead of just the row
|
||||||
|
const actualResult = result?.rows?.[0] || result;
|
||||||
|
|
||||||
|
expect(actualResult).toBeDefined();
|
||||||
|
expect(actualResult?.uri).toBe('test-doc-1');
|
||||||
|
expect(actualResult?.type).toBe('article');
|
||||||
|
expect(actualResult?.metadata).toEqual(document.metadata);
|
||||||
|
expect(actualResult?.data).toEqual(document.data);
|
||||||
|
expect(actualResult?.createdAt).toBeDefined();
|
||||||
|
expect(actualResult?.updatedAt).toBeDefined();
|
||||||
|
expect(actualResult?.deletedAt).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update an existing document', async () => {
|
||||||
|
const document: DocumentUpsert = {
|
||||||
|
uri: 'test-doc-1',
|
||||||
|
type: 'article',
|
||||||
|
metadata: {
|
||||||
|
title: 'Original Title',
|
||||||
|
priority: 3,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
content: 'Original content',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await documentsService.upsert(document);
|
||||||
|
|
||||||
|
const updatedDocument: DocumentUpsert = {
|
||||||
|
uri: 'test-doc-1',
|
||||||
|
type: 'article',
|
||||||
|
metadata: {
|
||||||
|
title: 'Updated Title',
|
||||||
|
priority: 8,
|
||||||
|
published: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
content: 'Updated content',
|
||||||
|
tags: ['test', 'updated'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await documentsService.upsert(updatedDocument);
|
||||||
|
|
||||||
|
const db = await databaseService.getDb();
|
||||||
|
const result = await db(tableNames.documents).where({ uri: 'test-doc-1', type: 'article' }).first();
|
||||||
|
const actualResult = result?.rows?.[0] || result;
|
||||||
|
|
||||||
|
expect(actualResult?.metadata).toEqual(updatedDocument.metadata);
|
||||||
|
expect(actualResult?.data).toEqual(updatedDocument.data);
|
||||||
|
expect(actualResult?.updatedAt).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow same URI with different types', async () => {
|
||||||
|
const doc1: DocumentUpsert = {
|
||||||
|
uri: 'shared-uri',
|
||||||
|
type: 'article',
|
||||||
|
metadata: { category: 'news' },
|
||||||
|
data: { content: 'Article content' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const doc2: DocumentUpsert = {
|
||||||
|
uri: 'shared-uri',
|
||||||
|
type: 'video',
|
||||||
|
metadata: { duration: 300 },
|
||||||
|
data: { url: 'video-url' },
|
||||||
|
};
|
||||||
|
|
||||||
|
await documentsService.upsert(doc1);
|
||||||
|
await documentsService.upsert(doc2);
|
||||||
|
|
||||||
|
const db = await databaseService.getDb();
|
||||||
|
const results = await db(tableNames.documents).where({ uri: 'shared-uri' });
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results.some((r) => r.type === 'article')).toBe(true);
|
||||||
|
expect(results.some((r) => r.type === 'video')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('search', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Setup test data
|
||||||
|
const testDocuments: DocumentUpsert[] = [
|
||||||
|
{
|
||||||
|
uri: 'doc-1',
|
||||||
|
type: 'article',
|
||||||
|
metadata: {
|
||||||
|
title: 'First Article',
|
||||||
|
priority: 5,
|
||||||
|
published: true,
|
||||||
|
category: 'tech',
|
||||||
|
},
|
||||||
|
data: { content: 'First article content' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'doc-2',
|
||||||
|
type: 'article',
|
||||||
|
metadata: {
|
||||||
|
title: 'Second Article',
|
||||||
|
priority: 3,
|
||||||
|
published: false,
|
||||||
|
category: 'science',
|
||||||
|
},
|
||||||
|
data: { content: 'Second article content' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'doc-3',
|
||||||
|
type: 'video',
|
||||||
|
metadata: {
|
||||||
|
title: 'Test Video',
|
||||||
|
duration: 120,
|
||||||
|
published: true,
|
||||||
|
quality: 'HD',
|
||||||
|
},
|
||||||
|
data: { url: 'video-url' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'doc-4',
|
||||||
|
type: 'article',
|
||||||
|
metadata: {
|
||||||
|
title: 'Third Article',
|
||||||
|
priority: 8,
|
||||||
|
published: true,
|
||||||
|
category: 'tech',
|
||||||
|
},
|
||||||
|
data: { content: 'Third article content' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const doc of testDocuments) {
|
||||||
|
await documentsService.upsert(doc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return all documents when no filters applied', async () => {
|
||||||
|
const options: DocumentSearchOptions = {};
|
||||||
|
const results = await documentsService.search(options);
|
||||||
|
|
||||||
|
expect(results).toHaveLength(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by uris', async () => {
|
||||||
|
const options: DocumentSearchOptions = {
|
||||||
|
uris: ['doc-1', 'doc-3'],
|
||||||
|
};
|
||||||
|
const results = await documentsService.search(options);
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results.map((r) => r.uri).sort()).toEqual(['doc-1', 'doc-3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by types', async () => {
|
||||||
|
const options: DocumentSearchOptions = {
|
||||||
|
types: ['article'],
|
||||||
|
};
|
||||||
|
const results = await documentsService.search(options);
|
||||||
|
expect(results).toHaveLength(3);
|
||||||
|
expect(results.every((r) => r.type === 'article')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply limit', async () => {
|
||||||
|
const options: DocumentSearchOptions = {
|
||||||
|
limit: 2,
|
||||||
|
};
|
||||||
|
const results = await documentsService.search(options);
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply offset', async () => {
|
||||||
|
const options: DocumentSearchOptions = {
|
||||||
|
limit: 2,
|
||||||
|
offset: 1,
|
||||||
|
};
|
||||||
|
const results = await documentsService.search(options);
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should combine multiple filters', async () => {
|
||||||
|
const options: DocumentSearchOptions = {
|
||||||
|
types: ['article'],
|
||||||
|
uris: ['doc-1', 'doc-2', 'doc-4'],
|
||||||
|
limit: 2,
|
||||||
|
};
|
||||||
|
const results = await documentsService.search(options);
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results.every((r) => r.type === 'article')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('meta search', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Setup test data with various metadata types
|
||||||
|
const testDocuments: DocumentUpsert[] = [
|
||||||
|
{
|
||||||
|
uri: 'meta-1',
|
||||||
|
type: 'article',
|
||||||
|
metadata: {
|
||||||
|
title: 'High Priority',
|
||||||
|
priority: 8,
|
||||||
|
published: true,
|
||||||
|
category: 'technology',
|
||||||
|
score: 95.5,
|
||||||
|
},
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'meta-2',
|
||||||
|
type: 'article',
|
||||||
|
metadata: {
|
||||||
|
title: 'Medium Priority',
|
||||||
|
priority: 5,
|
||||||
|
published: false,
|
||||||
|
category: 'science',
|
||||||
|
score: 78.2,
|
||||||
|
},
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'meta-3',
|
||||||
|
type: 'video',
|
||||||
|
metadata: {
|
||||||
|
title: 'Tutorial Video',
|
||||||
|
duration: 300,
|
||||||
|
published: true,
|
||||||
|
category: 'education',
|
||||||
|
hd: true,
|
||||||
|
},
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'meta-4',
|
||||||
|
type: 'article',
|
||||||
|
metadata: {
|
||||||
|
title: 'Draft Article',
|
||||||
|
priority: 2,
|
||||||
|
published: false,
|
||||||
|
category: 'technology',
|
||||||
|
score: 45.8,
|
||||||
|
},
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const doc of testDocuments) {
|
||||||
|
await documentsService.upsert(doc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('number filters', () => {
|
||||||
|
it('should filter by exact number equality', async () => {
|
||||||
|
const meta: MetaCondition = {
|
||||||
|
type: 'number',
|
||||||
|
field: 'priority',
|
||||||
|
filter: { eq: 5 },
|
||||||
|
};
|
||||||
|
const results = await documentsService.search({ meta });
|
||||||
|
|
||||||
|
expect(results).toHaveLength(1);
|
||||||
|
expect(results[0].uri).toBe('meta-2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by number range', async () => {
|
||||||
|
const meta: MetaCondition = {
|
||||||
|
type: 'number',
|
||||||
|
field: 'priority',
|
||||||
|
filter: { gte: 5, lt: 8 },
|
||||||
|
};
|
||||||
|
const results = await documentsService.search({ meta });
|
||||||
|
|
||||||
|
expect(results).toHaveLength(1);
|
||||||
|
expect(results[0].uri).toBe('meta-2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by greater than', async () => {
|
||||||
|
const meta: MetaCondition = {
|
||||||
|
type: 'number',
|
||||||
|
field: 'priority',
|
||||||
|
filter: { gt: 5 },
|
||||||
|
};
|
||||||
|
const results = await documentsService.search({ meta });
|
||||||
|
|
||||||
|
expect(results).toHaveLength(1);
|
||||||
|
expect(results[0].uri).toBe('meta-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by not equal', async () => {
|
||||||
|
const meta: MetaCondition = {
|
||||||
|
type: 'number',
|
||||||
|
field: 'priority',
|
||||||
|
filter: { neq: 5 },
|
||||||
|
};
|
||||||
|
const results = await documentsService.search({ meta });
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results.map((r) => r.uri).sort()).toEqual(['meta-1', 'meta-4']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('text filters', () => {
|
||||||
|
it('should filter by exact text equality', async () => {
|
||||||
|
const meta: MetaCondition = {
|
||||||
|
type: 'text',
|
||||||
|
field: 'category',
|
||||||
|
filter: { eq: 'technology' },
|
||||||
|
};
|
||||||
|
const results = await documentsService.search({ meta });
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results.map((r) => r.uri).sort()).toEqual(['meta-1', 'meta-4']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by text like pattern', async () => {
|
||||||
|
const meta: MetaCondition = {
|
||||||
|
type: 'text',
|
||||||
|
field: 'title',
|
||||||
|
filter: { like: '%Priority%' },
|
||||||
|
};
|
||||||
|
const results = await documentsService.search({ meta });
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results.map((r) => r.uri).sort()).toEqual(['meta-1', 'meta-2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by text not like pattern', async () => {
|
||||||
|
const meta: MetaCondition = {
|
||||||
|
type: 'text',
|
||||||
|
field: 'title',
|
||||||
|
filter: { nlike: '%Priority%' },
|
||||||
|
};
|
||||||
|
const results = await documentsService.search({ meta });
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results.map((r) => r.uri).sort()).toEqual(['meta-3', 'meta-4']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by text not equal', async () => {
|
||||||
|
const meta: MetaCondition = {
|
||||||
|
type: 'text',
|
||||||
|
field: 'category',
|
||||||
|
filter: { neq: 'technology' },
|
||||||
|
};
|
||||||
|
const results = await documentsService.search({ meta });
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results.map((r) => r.uri).sort()).toEqual(['meta-2', 'meta-3']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('boolean filters', () => {
|
||||||
|
it('should filter by boolean true', async () => {
|
||||||
|
const meta: MetaCondition = {
|
||||||
|
type: 'bool',
|
||||||
|
field: 'published',
|
||||||
|
filter: { eq: true },
|
||||||
|
};
|
||||||
|
const results = await documentsService.search({ meta });
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results.map((r) => r.uri).sort()).toEqual(['meta-1', 'meta-3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by boolean false', async () => {
|
||||||
|
const meta: MetaCondition = {
|
||||||
|
type: 'bool',
|
||||||
|
field: 'published',
|
||||||
|
filter: { eq: false },
|
||||||
|
};
|
||||||
|
const results = await documentsService.search({ meta });
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results.map((r) => r.uri).sort()).toEqual(['meta-2', 'meta-4']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.skip('null filters', () => {
|
||||||
|
it('should filter by null values for numbers', async () => {
|
||||||
|
const meta: MetaCondition = {
|
||||||
|
type: 'number',
|
||||||
|
field: 'nonexistent',
|
||||||
|
filter: { nill: true },
|
||||||
|
};
|
||||||
|
const results = await documentsService.search({ meta });
|
||||||
|
|
||||||
|
expect(results).toHaveLength(4); // All documents should match as none have this field
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter by non-null values', async () => {
|
||||||
|
const meta: MetaCondition = {
|
||||||
|
type: 'number',
|
||||||
|
field: 'priority',
|
||||||
|
filter: { nill: false },
|
||||||
|
};
|
||||||
|
const results = await documentsService.search({ meta });
|
||||||
|
|
||||||
|
expect(results).toHaveLength(3); // All articles have priority, video doesn't
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('complex conditions', () => {
|
||||||
|
it('should handle AND conditions', async () => {
|
||||||
|
const meta: MetaCondition = {
|
||||||
|
type: 'and',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
type: 'bool',
|
||||||
|
field: 'published',
|
||||||
|
filter: { eq: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
field: 'category',
|
||||||
|
filter: { eq: 'technology' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const results = await documentsService.search({ meta });
|
||||||
|
|
||||||
|
expect(results).toHaveLength(1);
|
||||||
|
expect(results[0].uri).toBe('meta-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle OR conditions', async () => {
|
||||||
|
const meta: MetaCondition = {
|
||||||
|
type: 'or',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
field: 'priority',
|
||||||
|
filter: { gt: 7 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
field: 'category',
|
||||||
|
filter: { eq: 'education' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const results = await documentsService.search({ meta });
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results.map((r) => r.uri).sort()).toEqual(['meta-1', 'meta-3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle nested AND/OR conditions', async () => {
|
||||||
|
const meta: MetaCondition = {
|
||||||
|
type: 'and',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
type: 'or',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
field: 'category',
|
||||||
|
filter: { eq: 'technology' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
field: 'category',
|
||||||
|
filter: { eq: 'science' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'bool',
|
||||||
|
field: 'published',
|
||||||
|
filter: { eq: false },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const results = await documentsService.search({ meta });
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results.map((r) => r.uri).sort()).toEqual(['meta-2', 'meta-4']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle deeply nested conditions', async () => {
|
||||||
|
const meta: MetaCondition = {
|
||||||
|
type: 'or',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
type: 'and',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
field: 'priority',
|
||||||
|
filter: { gte: 8 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'bool',
|
||||||
|
field: 'published',
|
||||||
|
filter: { eq: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'and',
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
field: 'category',
|
||||||
|
filter: { eq: 'education' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
field: 'duration',
|
||||||
|
filter: { gt: 100 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const results = await documentsService.search({ meta });
|
||||||
|
|
||||||
|
expect(results).toHaveLength(2);
|
||||||
|
expect(results.map((r) => r.uri).sort()).toEqual(['meta-1', 'meta-3']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('should handle empty search options', async () => {
|
||||||
|
const results = await documentsService.search({});
|
||||||
|
expect(results).toBeDefined();
|
||||||
|
expect(Array.isArray(results)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle search with no results', async () => {
|
||||||
|
const options: DocumentSearchOptions = {
|
||||||
|
uris: ['non-existent-uri'],
|
||||||
|
};
|
||||||
|
const results = await documentsService.search(options);
|
||||||
|
expect(results).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle meta search with no matching conditions', async () => {
|
||||||
|
await documentsService.upsert({
|
||||||
|
uri: 'test-doc',
|
||||||
|
type: 'article',
|
||||||
|
metadata: { priority: 5 },
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const meta: MetaCondition = {
|
||||||
|
type: 'number',
|
||||||
|
field: 'priority',
|
||||||
|
filter: { eq: 999 },
|
||||||
|
};
|
||||||
|
const results = await documentsService.search({ meta });
|
||||||
|
expect(results).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class DocumentsService {
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
};
|
};
|
||||||
await db('documents')
|
await db(tableNames.documents)
|
||||||
.insert({ ...baseItem, createdAt: new Date() })
|
.insert({ ...baseItem, createdAt: new Date() })
|
||||||
.onConflict(['uri', 'type'])
|
.onConflict(['uri', 'type'])
|
||||||
.merge({
|
.merge({
|
||||||
@@ -101,9 +101,15 @@ class DocumentsService {
|
|||||||
|
|
||||||
if (nill !== undefined) {
|
if (nill !== undefined) {
|
||||||
if (nill) {
|
if (nill) {
|
||||||
builder.whereNull(`metadata->'${filter.field}'`);
|
// Field doesn't exist or is null
|
||||||
|
builder.where((subBuilder) => {
|
||||||
|
subBuilder
|
||||||
|
.whereRaw(`NOT (metadata ? '${filter.field}')`)
|
||||||
|
.orWhereRaw(`metadata->>'${filter.field}' IS NULL`);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
builder.whereNotNull(`metadata->'${filter.field}'`);
|
// Field exists and is not null
|
||||||
|
builder.whereRaw(`metadata ? '${filter.field}' AND metadata->>'${filter.field}' IS NOT NULL`);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -131,9 +137,15 @@ class DocumentsService {
|
|||||||
|
|
||||||
if (nill !== undefined) {
|
if (nill !== undefined) {
|
||||||
if (nill) {
|
if (nill) {
|
||||||
builder.whereNull(`metadata->'${filter.field}'`);
|
// Field doesn't exist or is null
|
||||||
|
builder.where((subBuilder) => {
|
||||||
|
subBuilder
|
||||||
|
.whereRaw(`NOT (metadata ? '${filter.field}')`)
|
||||||
|
.orWhereRaw(`metadata->>'${filter.field}' IS NULL`);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
builder.whereNotNull(`metadata->'${filter.field}'`);
|
// Field exists and is not null
|
||||||
|
builder.whereRaw(`metadata ? '${filter.field}' AND metadata->>'${filter.field}' IS NOT NULL`);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -155,9 +167,15 @@ class DocumentsService {
|
|||||||
|
|
||||||
if (nill !== undefined) {
|
if (nill !== undefined) {
|
||||||
if (nill) {
|
if (nill) {
|
||||||
builder.whereNull(`metadata->'${filter.field}'`);
|
// Field doesn't exist or is null
|
||||||
|
builder.where((subBuilder) => {
|
||||||
|
subBuilder
|
||||||
|
.whereRaw(`NOT (metadata ? '${filter.field}')`)
|
||||||
|
.orWhereRaw(`metadata->>'${filter.field}' IS NULL`);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
builder.whereNotNull(`metadata->'${filter.field}'`);
|
// Field exists and is not null
|
||||||
|
builder.whereRaw(`metadata ? '${filter.field}' AND metadata->>'${filter.field}' IS NOT NULL`);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user