@loomcore/api 0.2.68 → 0.2.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +201 -201
  2. package/dist/databases/models/classified-filter-key.d.ts +8 -0
  3. package/dist/databases/models/classified-filter-key.js +1 -0
  4. package/dist/databases/models/index.d.ts +2 -0
  5. package/dist/databases/models/index.js +2 -0
  6. package/dist/databases/models/join-filter.interface.d.ts +8 -0
  7. package/dist/databases/models/join-filter.interface.js +1 -0
  8. package/dist/databases/mongo-db/queries/mongo-get-by-id.query.js +11 -4
  9. package/dist/databases/mongo-db/queries/mongo-get.query.js +11 -3
  10. package/dist/databases/mongo-db/utils/build-join-match.util.d.ts +4 -0
  11. package/dist/databases/mongo-db/utils/build-join-match.util.js +192 -0
  12. package/dist/databases/mongo-db/utils/build-no-sql-match.util.js +33 -7
  13. package/dist/databases/mongo-db/utils/convert-operations-to-pipeline.util.js +15 -5
  14. package/dist/databases/operations/index.d.ts +1 -0
  15. package/dist/databases/operations/index.js +1 -0
  16. package/dist/databases/operations/join-operation.d.ts +4 -0
  17. package/dist/databases/operations/join-operation.js +1 -0
  18. package/dist/databases/postgres/commands/postgres-batch-update.command.js +2 -3
  19. package/dist/databases/postgres/commands/postgres-delete-many.command.js +1 -1
  20. package/dist/databases/postgres/commands/postgres-full-update-by-id.command.js +1 -3
  21. package/dist/databases/postgres/commands/postgres-partial-update-by-id.command.js +1 -3
  22. package/dist/databases/postgres/commands/postgres-update.command.js +4 -7
  23. package/dist/databases/postgres/queries/postgres-find-one.query.js +2 -2
  24. package/dist/databases/postgres/queries/postgres-find.query.js +2 -2
  25. package/dist/databases/postgres/queries/postgres-get-all.query.js +1 -3
  26. package/dist/databases/postgres/queries/postgres-get-by-id.query.js +2 -5
  27. package/dist/databases/postgres/queries/postgres-get.query.js +6 -11
  28. package/dist/databases/postgres/utils/build-count-query.d.ts +6 -1
  29. package/dist/databases/postgres/utils/build-count-query.js +11 -3
  30. package/dist/databases/postgres/utils/build-join-clauses.d.ts +2 -4
  31. package/dist/databases/postgres/utils/build-join-clauses.js +28 -35
  32. package/dist/databases/postgres/utils/build-join-where.d.ts +9 -0
  33. package/dist/databases/postgres/utils/build-join-where.js +136 -0
  34. package/dist/databases/postgres/utils/build-order-by-clause.d.ts +1 -4
  35. package/dist/databases/postgres/utils/build-order-by-clause.js +2 -3
  36. package/dist/databases/postgres/utils/build-select-clause.js +2 -4
  37. package/dist/databases/postgres/utils/build-where-clause.d.ts +2 -1
  38. package/dist/databases/postgres/utils/build-where-clause.js +18 -57
  39. package/dist/databases/utils/is-join-operation.util.d.ts +3 -0
  40. package/dist/databases/utils/is-join-operation.util.js +6 -0
  41. package/dist/databases/utils/nullish-filter-value.d.ts +9 -0
  42. package/dist/databases/utils/nullish-filter-value.js +15 -0
  43. package/dist/databases/utils/resolve-join-filter.d.ts +13 -0
  44. package/dist/databases/utils/resolve-join-filter.js +75 -0
  45. package/package.json +1 -1
@@ -0,0 +1,136 @@
1
+ import { readFilterValue } from '../../utils/nullish-filter-value.js';
2
+ import { findJoinOperation, parentAliasOf, } from '../../utils/resolve-join-filter.js';
3
+ import { collectOuterOneToOneAliases } from './build-join-clauses.js';
4
+ import { toPostgresStoreName, toSnakeCase } from './convert-keys.util.js';
5
+ export function toSqlColumnName(column) {
6
+ if (column.startsWith('_')) {
7
+ return column;
8
+ }
9
+ if (column === 'id') {
10
+ return '_id';
11
+ }
12
+ return toSnakeCase(column);
13
+ }
14
+ function quoteIdent(value) {
15
+ return `"${value.replace(/"/g, '""')}"`;
16
+ }
17
+ export function appendFilterConditions(qualifiedKey, value, conditions, values, paramIndex) {
18
+ if (value.eq !== undefined) {
19
+ const comparison = readFilterValue(value.eq);
20
+ if (comparison.kind === 'value') {
21
+ conditions.push(`(${qualifiedKey} = $${paramIndex.current} OR (${qualifiedKey} IS NULL AND $${paramIndex.current} IS NULL))`);
22
+ values.push(comparison.value);
23
+ paramIndex.current++;
24
+ }
25
+ else {
26
+ conditions.push(`${qualifiedKey} IS NULL`);
27
+ }
28
+ }
29
+ if (value.ne !== undefined) {
30
+ const comparison = readFilterValue(value.ne);
31
+ if (comparison.kind === 'value') {
32
+ conditions.push(`(${qualifiedKey} != $${paramIndex.current} OR (${qualifiedKey} IS NOT NULL AND $${paramIndex.current} IS NULL) OR (${qualifiedKey} IS NULL AND $${paramIndex.current} IS NOT NULL))`);
33
+ values.push(comparison.value);
34
+ paramIndex.current++;
35
+ }
36
+ else {
37
+ conditions.push(`${qualifiedKey} IS NOT NULL`);
38
+ }
39
+ }
40
+ if (value.in !== undefined && Array.isArray(value.in)) {
41
+ const placeholders = value.in.map(() => `$${paramIndex.current++}`).join(', ');
42
+ conditions.push(`${qualifiedKey} IN (${placeholders})`);
43
+ values.push(...value.in);
44
+ }
45
+ if (value.gte !== undefined) {
46
+ conditions.push(`${qualifiedKey} >= $${paramIndex.current}`);
47
+ values.push(value.gte);
48
+ paramIndex.current++;
49
+ }
50
+ if (value.lte !== undefined) {
51
+ conditions.push(`${qualifiedKey} <= $${paramIndex.current}`);
52
+ values.push(value.lte);
53
+ paramIndex.current++;
54
+ }
55
+ if (value.gt !== undefined) {
56
+ conditions.push(`${qualifiedKey} > $${paramIndex.current}`);
57
+ values.push(value.gt);
58
+ paramIndex.current++;
59
+ }
60
+ if (value.lt !== undefined) {
61
+ conditions.push(`${qualifiedKey} < $${paramIndex.current}`);
62
+ values.push(value.lt);
63
+ paramIndex.current++;
64
+ }
65
+ if (value.contains !== undefined) {
66
+ conditions.push(`LOWER(${qualifiedKey}) LIKE LOWER($${paramIndex.current})`);
67
+ values.push(`%${value.contains}%`);
68
+ paramIndex.current++;
69
+ }
70
+ }
71
+ function qualifyLocalField(localField, mainTableName) {
72
+ const dot = localField.indexOf('.');
73
+ if (dot === -1) {
74
+ return mainTableName ? `${quoteIdent(mainTableName)}.${quoteIdent(localField)}` : quoteIdent(localField);
75
+ }
76
+ const alias = localField.slice(0, dot);
77
+ const column = localField.slice(dot + 1);
78
+ return `${quoteIdent(alias)}.${quoteIdent(column)}`;
79
+ }
80
+ function ancestorChain(target, operations, mainTableName, outerAliases) {
81
+ const chain = [target];
82
+ let current = target;
83
+ const seen = new Set([target.as]);
84
+ while (true) {
85
+ const parentAlias = parentAliasOf(current.localField);
86
+ if (parentAlias === null || parentAlias === mainTableName || outerAliases.has(parentAlias)) {
87
+ break;
88
+ }
89
+ if (seen.has(parentAlias)) {
90
+ break;
91
+ }
92
+ const parent = findJoinOperation(operations, parentAlias);
93
+ if (!parent) {
94
+ break;
95
+ }
96
+ seen.add(parent.as);
97
+ chain.unshift(parent);
98
+ current = parent;
99
+ }
100
+ return chain;
101
+ }
102
+ function existsSql(target, qualifiedColumn, filter, operations, mainTableName, outerAliases, values, paramIndex) {
103
+ const chain = ancestorChain(target, operations, mainTableName, outerAliases);
104
+ const first = chain[0];
105
+ let sql = `EXISTS (SELECT 1 FROM ${quoteIdent(toPostgresStoreName(first.from))} AS ${quoteIdent(first.as)}`;
106
+ for (let index = 1; index < chain.length; index++) {
107
+ const child = chain[index];
108
+ const fromTable = quoteIdent(toPostgresStoreName(child.from));
109
+ const childAlias = quoteIdent(child.as);
110
+ const onLeft = qualifyLocalField(child.localField, mainTableName);
111
+ sql += ` INNER JOIN ${fromTable} AS ${childAlias} ON ${onLeft} = ${childAlias}.${quoteIdent(child.foreignField)}`;
112
+ }
113
+ const conditions = [
114
+ `${qualifyLocalField(first.localField, mainTableName)} = ${quoteIdent(first.as)}.${quoteIdent(first.foreignField)}`,
115
+ ];
116
+ appendFilterConditions(qualifiedColumn, filter, conditions, values, paramIndex);
117
+ sql += ` WHERE ${conditions.join(' AND ')})`;
118
+ return sql;
119
+ }
120
+ export function joinFilterSql(joinFilter, operations, mainTableName, values, paramIndex) {
121
+ const target = findJoinOperation(operations, joinFilter.aliases[joinFilter.aliases.length - 1]);
122
+ if (!target) {
123
+ return null;
124
+ }
125
+ const outerAliases = collectOuterOneToOneAliases(operations, mainTableName);
126
+ const qualified = `${quoteIdent(target.as)}.${quoteIdent(toSqlColumnName(joinFilter.column))}`;
127
+ if (!joinFilter.crossesMany && outerAliases.has(target.as)) {
128
+ const conditions = [];
129
+ appendFilterConditions(qualified, joinFilter.filter, conditions, values, paramIndex);
130
+ if (joinFilter.filter.ne !== undefined) {
131
+ conditions.push(`${quoteIdent(target.as)}."_id" IS NOT NULL`);
132
+ }
133
+ return conditions.join(' AND ');
134
+ }
135
+ return existsSql(target, qualified, joinFilter.filter, operations, mainTableName, outerAliases, values, paramIndex);
136
+ }
@@ -1,5 +1,2 @@
1
1
  import { IQueryOptions } from '@loomcore/common/models';
2
- export interface BuildOrderByClauseOptions {
3
- tablePrefix?: string;
4
- }
5
- export declare function buildOrderByClause(queryObject: IQueryOptions, options?: BuildOrderByClauseOptions): string;
2
+ export declare function buildOrderByClause(queryObject: IQueryOptions, tablePrefix: string): string;
@@ -1,9 +1,8 @@
1
- export function buildOrderByClause(queryObject, options) {
1
+ export function buildOrderByClause(queryObject, tablePrefix) {
2
2
  if (!queryObject.orderBy) {
3
3
  return '';
4
4
  }
5
5
  const orderBy = queryObject.orderBy;
6
6
  const sortDirection = queryObject.sortDirection || 'asc';
7
- const qualified = options?.tablePrefix ? `"${options.tablePrefix}"."${orderBy}"` : orderBy;
8
- return `ORDER BY ${qualified} ${sortDirection}`;
7
+ return `ORDER BY "${tablePrefix}"."${orderBy}" ${sortDirection}`;
9
8
  }
@@ -2,6 +2,7 @@ import { LeftJoin } from '../../operations/left-join.operation.js';
2
2
  import { InnerJoin } from '../../operations/inner-join.operation.js';
3
3
  import { LeftJoinMany } from '../../operations/left-join-many.operation.js';
4
4
  import { toPostgresStoreName } from './convert-keys.util.js';
5
+ import { isJoinOperation } from '../../utils/is-join-operation.util.js';
5
6
  const PK = '_id';
6
7
  export async function getTableColumns(client, tableName) {
7
8
  const storeName = toPostgresStoreName(tableName);
@@ -172,10 +173,7 @@ async function buildJoinDataObject(client, operations, mainTableName) {
172
173
  return jsonbBuildObject(entries);
173
174
  }
174
175
  export async function buildSelectClause(client, mainTableName, operations) {
175
- const leftJoinOperations = operations.filter(op => op instanceof LeftJoin);
176
- const innerJoinOperations = operations.filter(op => op instanceof InnerJoin);
177
- const leftJoinManyOperations = operations.filter(op => op instanceof LeftJoinMany);
178
- const hasJoins = leftJoinOperations.length > 0 || innerJoinOperations.length > 0 || leftJoinManyOperations.length > 0;
176
+ const hasJoins = operations.some(isJoinOperation);
179
177
  if (!hasJoins) {
180
178
  return '*';
181
179
  }
@@ -1,5 +1,6 @@
1
1
  import { IQueryOptions } from '@loomcore/common/models';
2
- export declare function buildWhereClause(queryObject: IQueryOptions, values?: any[], tablePrefix?: string): {
2
+ import type { Operation } from '../../operations/operation.js';
3
+ export declare function buildWhereClause(queryObject: IQueryOptions, tablePrefix: string, values?: any[], operations?: Operation[]): {
3
4
  whereClause: string;
4
5
  values: any[];
5
6
  };
@@ -1,63 +1,24 @@
1
- import { toSnakeCase } from './convert-keys.util.js';
2
- export function buildWhereClause(queryObject, values = [], tablePrefix) {
3
- const filters = queryObject.filters || {};
1
+ import { splitFiltersByType } from '../../utils/resolve-join-filter.js';
2
+ import { appendFilterConditions, joinFilterSql, toSqlColumnName } from './build-join-where.js';
3
+ export function buildWhereClause(queryObject, tablePrefix, values = [], operations) {
4
+ const useJoinFilters = operations !== undefined && operations.length > 0;
5
+ const { rootOptions, joinFilters } = useJoinFilters
6
+ ? splitFiltersByType(queryObject, operations, tablePrefix)
7
+ : { rootOptions: queryObject, joinFilters: [] };
8
+ const filters = rootOptions.filters || {};
4
9
  const conditions = [];
5
- let paramIndex = values.length + 1;
6
- const qualifyColumn = (columnName) => {
7
- return tablePrefix ? `"${tablePrefix}"."${columnName}"` : `"${columnName}"`;
8
- };
10
+ const paramIndex = { current: values.length + 1 };
9
11
  for (const [key, value] of Object.entries(filters)) {
10
12
  if (value) {
11
- let snakeKey;
12
- if (key.startsWith('_')) {
13
- snakeKey = key;
14
- }
15
- else if (key === 'id') {
16
- snakeKey = '_id';
17
- }
18
- else {
19
- snakeKey = toSnakeCase(key);
20
- }
21
- const qualifiedKey = qualifyColumn(snakeKey);
22
- if (value.eq !== undefined) {
23
- conditions.push(`(${qualifiedKey} = $${paramIndex} OR (${qualifiedKey} IS NULL AND $${paramIndex} IS NULL))`);
24
- values.push(value.eq);
25
- paramIndex++;
26
- }
27
- if (value.ne !== undefined) {
28
- conditions.push(`(${qualifiedKey} != $${paramIndex} OR (${qualifiedKey} IS NOT NULL AND $${paramIndex} IS NULL) OR (${qualifiedKey} IS NULL AND $${paramIndex} IS NOT NULL))`);
29
- values.push(value.ne);
30
- paramIndex++;
31
- }
32
- if (value.in !== undefined && Array.isArray(value.in)) {
33
- const placeholders = value.in.map(() => `$${paramIndex++}`).join(', ');
34
- conditions.push(`${qualifiedKey} IN (${placeholders})`);
35
- values.push(...value.in);
36
- }
37
- if (value.gte !== undefined) {
38
- conditions.push(`${qualifiedKey} >= $${paramIndex}`);
39
- values.push(value.gte);
40
- paramIndex++;
41
- }
42
- if (value.lte !== undefined) {
43
- conditions.push(`${qualifiedKey} <= $${paramIndex}`);
44
- values.push(value.lte);
45
- paramIndex++;
46
- }
47
- if (value.gt !== undefined) {
48
- conditions.push(`${qualifiedKey} > $${paramIndex}`);
49
- values.push(value.gt);
50
- paramIndex++;
51
- }
52
- if (value.lt !== undefined) {
53
- conditions.push(`${qualifiedKey} < $${paramIndex}`);
54
- values.push(value.lt);
55
- paramIndex++;
56
- }
57
- if (value.contains !== undefined) {
58
- conditions.push(`LOWER(${qualifiedKey}) LIKE LOWER($${paramIndex})`);
59
- values.push(`%${value.contains}%`);
60
- paramIndex++;
13
+ const qualifiedKey = `"${tablePrefix}"."${toSqlColumnName(key)}"`;
14
+ appendFilterConditions(qualifiedKey, value, conditions, values, paramIndex);
15
+ }
16
+ }
17
+ if (useJoinFilters) {
18
+ for (const joinFilter of joinFilters) {
19
+ const predicate = joinFilterSql(joinFilter, operations, tablePrefix, values, paramIndex);
20
+ if (predicate) {
21
+ conditions.push(predicate);
61
22
  }
62
23
  }
63
24
  }
@@ -0,0 +1,3 @@
1
+ import { type JoinOperation } from "../operations/join-operation.js";
2
+ import { type Operation } from "../operations/operation.js";
3
+ export declare function isJoinOperation(operation: Operation): operation is JoinOperation;
@@ -0,0 +1,6 @@
1
+ import { InnerJoin } from "../operations/inner-join.operation.js";
2
+ import { LeftJoin } from "../operations/left-join.operation.js";
3
+ import { LeftJoinMany } from "../operations/left-join-many.operation.js";
4
+ export function isJoinOperation(operation) {
5
+ return operation instanceof LeftJoin || operation instanceof InnerJoin || operation instanceof LeftJoinMany;
6
+ }
@@ -0,0 +1,9 @@
1
+ export type FilterComparison = {
2
+ kind: 'null';
3
+ } | {
4
+ kind: 'undefined';
5
+ } | {
6
+ kind: 'value';
7
+ value: unknown;
8
+ };
9
+ export declare function readFilterValue(raw: unknown): FilterComparison;
@@ -0,0 +1,15 @@
1
+ export function readFilterValue(raw) {
2
+ if (typeof raw === 'string' && raw.startsWith('"') && raw.endsWith('"')) {
3
+ const inner = raw.slice(1, -1);
4
+ if (inner === 'null' || inner === 'undefined') {
5
+ return { kind: 'value', value: inner };
6
+ }
7
+ }
8
+ if (raw === null || raw === 'null') {
9
+ return { kind: 'null' };
10
+ }
11
+ if (raw === 'undefined') {
12
+ return { kind: 'undefined' };
13
+ }
14
+ return { kind: 'value', value: raw };
15
+ }
@@ -0,0 +1,13 @@
1
+ import type { IQueryOptions } from '@loomcore/common/models';
2
+ import type { ClassifiedFilterKey } from '../models/classified-filter-key.js';
3
+ import type { Operation } from '../operations/operation.js';
4
+ import type { JoinOperation } from '../operations/join-operation.js';
5
+ import { JoinFilter } from '../models/join-filter.interface.js';
6
+ export declare function parentAliasOf(localField: string): string | null;
7
+ export declare function findJoinOperation(operations: Operation[], alias: string): JoinOperation | undefined;
8
+ export declare function logicalParentAlias(operation: JoinOperation, operations: Operation[], mainTableName: string): string | null;
9
+ export declare function classifyFilterKey(key: string, operations: Operation[], mainTableName: string): ClassifiedFilterKey;
10
+ export declare function splitFiltersByType(queryOptions: IQueryOptions, operations: Operation[], mainTableName: string): {
11
+ rootOptions: IQueryOptions;
12
+ joinFilters: JoinFilter[];
13
+ };
@@ -0,0 +1,75 @@
1
+ import { BadRequestError } from '../../errors/index.js';
2
+ import { LeftJoinMany } from '../operations/left-join-many.operation.js';
3
+ import { isJoinOperation } from './is-join-operation.util.js';
4
+ export function parentAliasOf(localField) {
5
+ const dot = localField.indexOf('.');
6
+ if (dot === -1) {
7
+ return null;
8
+ }
9
+ return localField.slice(0, dot);
10
+ }
11
+ export function findJoinOperation(operations, alias) {
12
+ return operations.find((operation) => isJoinOperation(operation) && operation.as === alias);
13
+ }
14
+ export function logicalParentAlias(operation, operations, mainTableName) {
15
+ const rawParent = parentAliasOf(operation.localField);
16
+ if (rawParent === null || rawParent === mainTableName) {
17
+ return null;
18
+ }
19
+ const parent = findJoinOperation(operations, rawParent);
20
+ return parent ? parent.as : null;
21
+ }
22
+ export function classifyFilterKey(key, operations, mainTableName) {
23
+ if (!key.includes('.') || operations.length === 0) {
24
+ return { type: 'root' };
25
+ }
26
+ const parts = key.split('.');
27
+ if (parts.some(part => part.length === 0)) {
28
+ return { type: 'root' };
29
+ }
30
+ if (!findJoinOperation(operations, parts[0])) {
31
+ return { type: 'root' };
32
+ }
33
+ const column = parts[parts.length - 1];
34
+ const aliases = parts.slice(0, -1);
35
+ for (let index = 0; index < aliases.length; index++) {
36
+ const alias = aliases[index];
37
+ const operation = findJoinOperation(operations, alias);
38
+ if (!operation) {
39
+ throw new BadRequestError(`Invalid join filter "${key}": "${alias}" is not a joined table`);
40
+ }
41
+ const expectedParent = index === 0 ? null : aliases[index - 1];
42
+ const actualParent = logicalParentAlias(operation, operations, mainTableName);
43
+ if (actualParent !== expectedParent) {
44
+ const where = actualParent ? `"${actualParent}"` : 'the root collection';
45
+ throw new BadRequestError(`Invalid join filter "${key}": "${alias}" is nested under ${where}`);
46
+ }
47
+ }
48
+ const crossesMany = aliases.some(alias => findJoinOperation(operations, alias) instanceof LeftJoinMany);
49
+ return { type: 'join', aliases, column, crossesMany };
50
+ }
51
+ export function splitFiltersByType(queryOptions, operations, mainTableName) {
52
+ const rootFilters = {};
53
+ const joinFilters = [];
54
+ for (const [key, filter] of Object.entries(queryOptions.filters ?? {})) {
55
+ if (!filter) {
56
+ continue;
57
+ }
58
+ const classified = classifyFilterKey(key, operations, mainTableName);
59
+ if (classified.type === 'root') {
60
+ rootFilters[key] = filter;
61
+ continue;
62
+ }
63
+ joinFilters.push({
64
+ key,
65
+ filter,
66
+ aliases: classified.aliases,
67
+ column: classified.column,
68
+ crossesMany: classified.crossesMany,
69
+ });
70
+ }
71
+ return {
72
+ rootOptions: { ...queryOptions, filters: rootFilters },
73
+ joinFilters,
74
+ };
75
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcore/api",
3
- "version": "0.2.68",
3
+ "version": "0.2.70",
4
4
  "private": false,
5
5
  "description": "Loom Core Api - An opinionated Node.js api using Typescript, Express, and MongoDb or PostgreSQL",
6
6
  "scripts": {