Update category files
This commit is contained in:
1 parent
86b656ee5c
commit
fea861f042
2 files changed
+83
-51
No files matched your search
@@ -1,40 +1,44 @@
|
|||||||
import { describe, it } from 'node:test';
|
import { describe, it } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { CategoryRepository } from './category.repository';
|
import { CategoryRepository } from './category.repository.ts';
|
||||||
|
import { Pool } from 'pg';
|
||||||
|
|
||||||
describe('CategoryRepository - ltree SQL Generator', () => {
|
// Lightweight mock generator for the pg Pool
|
||||||
const repository = new CategoryRepository();
|
function createMockDb(mockRows: any[]) {
|
||||||
|
const tracker = { lastQuery: { text: '', values: [] as any[] } };
|
||||||
|
const mockPool = {
|
||||||
|
query: async (text: string, values: any[]) => {
|
||||||
|
tracker.lastQuery = { text, values };
|
||||||
|
return { rows: mockRows };
|
||||||
|
}
|
||||||
|
} as unknown as Pool;
|
||||||
|
|
||||||
describe('generateDescendantsQuery()', () => {
|
return { mockPool, tracker };
|
||||||
it('should generate valid SQL using the ltree descenant operator (<@)', () => {
|
}
|
||||||
const parentPath = 'Top.Science';
|
|
||||||
const result = repository.generateDescendantsQuery(parentPath);
|
|
||||||
|
|
||||||
// veriy the generated SQL syntax as correct ltree
|
describe('CategoryRepository - ltree Queries', () => {
|
||||||
assert.match(result.sql, /<@/);
|
it('should format findAllDescendants query using the ltree descendant operator (<@)', async () => {
|
||||||
assert.match(result.sql, /::ltree/);
|
const mockData = [{ id: 2, name: 'Science', path: 'Top.Science' }];
|
||||||
assert.deepEqual(result.values, ['Top.Science']);
|
const { mockPool, tracker } = createMockDb(mockData);
|
||||||
});
|
const repository = new CategoryRepository(mockPool);
|
||||||
|
|
||||||
|
const result = await repository.findAllDescendants('Top.Science');
|
||||||
|
|
||||||
|
// Verify data output matches mock rows
|
||||||
|
assert.deepEqual(result, mockData);
|
||||||
|
|
||||||
|
// Verify generated SQL uses the right operator and bindings
|
||||||
|
assert.match(tracker.lastQuery.text, /<@/);
|
||||||
|
assert.deepEqual(tracker.lastQuery.values, ['Top.Science']);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('generateWildcardQuery()', () => {
|
it('should format findDirectChildren query with level matching requirements', async () => {
|
||||||
it('should generate valid SQL utilizing an lquery matching operator (~)', () => {
|
const { mockPool, tracker } = createMockDb([]);
|
||||||
const result = repository.generateWildcardQuery('Top.*.Astronomy');
|
const repository = new CategoryRepository(mockPool);
|
||||||
|
|
||||||
assert.match(result.sql, /~/);
|
await repository.findDirectChildren('Top');
|
||||||
assert.match(result.sql, /::lquery/);
|
|
||||||
assert.equal(result.values[0], 'Top.*.Astronomy');
|
assert.match(tracker.lastQuery.text, /nlevel\(path\)/);
|
||||||
});
|
assert.deepEqual(tracker.lastQuery.values, ['Top.*', 'Top']);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
describe('isValidLtreePath() validation utility', () => {
|
|
||||||
it('should pass for correctly formatted dot-separated ltree structures', () => {
|
|
||||||
assert.equal(repository.isValidLtreePath('Top.Hobbies.Amateurs_Astronomy'), true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fail if special characters or spaces are introduced into the path', () => {
|
|
||||||
assert.equal(repository.isValidLtreePath('Top.Hobbies.Bad Path!'), false);
|
|
||||||
assert.equal(repository.isValidLtreePath('Top..InvalidDoubleDot'), false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,30 +1,58 @@
|
|||||||
// structural interface to the test table
|
import { Pool } from 'pg';
|
||||||
export interface CatetoryNode {
|
|
||||||
|
export interface CategoryNode {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
path: string; // ltree field
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CategoryRepository {
|
export class CategoryRepository {
|
||||||
// simulate generating a SQL query targeting child paths
|
// Inject the database connection pool
|
||||||
generateDescendantsQuery(parentPath: string): { sql: string; values: string[] } {
|
constructor(private db: Pool) { }
|
||||||
return {
|
|
||||||
sql: 'SELECT id, name, path FROM categories WHERE path <@ $1::ltree',
|
/**
|
||||||
values: [parentPath]
|
* Finds all descendants of a given path (including the path itself).
|
||||||
};
|
* Uses the ltree operator <@ (is-descendant-of).
|
||||||
|
*/
|
||||||
|
async findAllDescendants(parentPath: string): Promise<CategoryNode[]> {
|
||||||
|
const query = `
|
||||||
|
SELECT id, name, path
|
||||||
|
FROM categories
|
||||||
|
WHERE path <@ $1::ltree
|
||||||
|
ORDER BY path ASC;
|
||||||
|
`;
|
||||||
|
const result = await this.db.query(query, [parentPath]);
|
||||||
|
return result.rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateWildcardQuery(pattern: string): { sql: string, values: string[] } {
|
/**
|
||||||
// Simulate genrating a SQL query using ltree wildcards
|
* Finds only the direct children of a given path.
|
||||||
return {
|
* Uses lquery pattern matching (parent.*) and matches the exact next depth level.
|
||||||
sql: 'SELECT id, name, path FROM categories WHERE path ~ $1::lquery',
|
*/
|
||||||
values: [pattern]
|
async findDirectChildren(parentPath: string): Promise<CategoryNode[]> {
|
||||||
};
|
const query = `
|
||||||
|
SELECT id, name, path
|
||||||
|
FROM categories
|
||||||
|
WHERE path ~ $1::lquery
|
||||||
|
AND nlevel(path) = nlevel($2::ltree) + 1;
|
||||||
|
`;
|
||||||
|
const values = [`${parentPath}.*`, parentPath];
|
||||||
|
const result = await this.db.query(query, values);
|
||||||
|
return result.rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper utility to validate a string as an ltree value
|
/**
|
||||||
isValidLtreePath(path: string): boolean {
|
* Finds all ancestors of a path (useful for breadcrumbs).
|
||||||
const ltreeRegex = /^[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/;
|
* Uses the ltree operator @> (is-ancestor-of).
|
||||||
return ltreeRegex.test(path);
|
*/
|
||||||
|
async findAncestors(childPath: string): Promise<CategoryNode[]> {
|
||||||
|
const query = `
|
||||||
|
SELECT id, name, path
|
||||||
|
FROM categories
|
||||||
|
WHERE path @> $1::ltree
|
||||||
|
ORDER BY nlevel(path) ASC;
|
||||||
|
`;
|
||||||
|
const result = await this.db.query(query, [childPath]);
|
||||||
|
return result.rows;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user