@loomcore/api 0.2.69 → 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.
- package/dist/databases/models/classified-filter-key.d.ts +8 -0
- package/dist/databases/models/classified-filter-key.js +1 -0
- package/dist/databases/models/index.d.ts +2 -0
- package/dist/databases/models/index.js +2 -0
- package/dist/databases/models/join-filter.interface.d.ts +8 -0
- package/dist/databases/models/join-filter.interface.js +1 -0
- package/dist/databases/mongo-db/queries/mongo-get-by-id.query.js +11 -4
- package/dist/databases/mongo-db/queries/mongo-get.query.js +11 -3
- package/dist/databases/mongo-db/utils/build-join-match.util.d.ts +4 -0
- package/dist/databases/mongo-db/utils/build-join-match.util.js +192 -0
- package/dist/databases/mongo-db/utils/convert-operations-to-pipeline.util.js +15 -5
- package/dist/databases/operations/index.d.ts +1 -0
- package/dist/databases/operations/index.js +1 -0
- package/dist/databases/operations/join-operation.d.ts +4 -0
- package/dist/databases/operations/join-operation.js +1 -0
- package/dist/databases/postgres/commands/postgres-batch-update.command.js +2 -3
- package/dist/databases/postgres/commands/postgres-delete-many.command.js +1 -1
- package/dist/databases/postgres/commands/postgres-full-update-by-id.command.js +1 -3
- package/dist/databases/postgres/commands/postgres-partial-update-by-id.command.js +1 -3
- package/dist/databases/postgres/commands/postgres-update.command.js +4 -7
- package/dist/databases/postgres/queries/postgres-find-one.query.js +2 -2
- package/dist/databases/postgres/queries/postgres-find.query.js +2 -2
- package/dist/databases/postgres/queries/postgres-get-all.query.js +1 -3
- package/dist/databases/postgres/queries/postgres-get-by-id.query.js +2 -5
- package/dist/databases/postgres/queries/postgres-get.query.js +6 -11
- package/dist/databases/postgres/utils/build-count-query.d.ts +6 -1
- package/dist/databases/postgres/utils/build-count-query.js +11 -3
- package/dist/databases/postgres/utils/build-join-clauses.d.ts +2 -4
- package/dist/databases/postgres/utils/build-join-clauses.js +28 -35
- package/dist/databases/postgres/utils/build-join-where.d.ts +9 -0
- package/dist/databases/postgres/utils/build-join-where.js +136 -0
- package/dist/databases/postgres/utils/build-order-by-clause.d.ts +1 -4
- package/dist/databases/postgres/utils/build-order-by-clause.js +2 -3
- package/dist/databases/postgres/utils/build-select-clause.js +2 -4
- package/dist/databases/postgres/utils/build-where-clause.d.ts +2 -1
- package/dist/databases/postgres/utils/build-where-clause.js +18 -70
- package/dist/databases/utils/is-join-operation.util.d.ts +3 -0
- package/dist/databases/utils/is-join-operation.util.js +6 -0
- package/dist/databases/utils/resolve-join-filter.d.ts +13 -0
- package/dist/databases/utils/resolve-join-filter.js +75 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
export * from './classified-filter-key.js';
|
|
1
2
|
export * from './constants.js';
|
|
2
3
|
export * from './database.interface.js';
|
|
3
4
|
export * from './database.js';
|
|
4
5
|
export * from './delete-result.js';
|
|
5
6
|
export * from './update-result.js';
|
|
7
|
+
export * from './join-filter.interface.js';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
export * from './classified-filter-key.js';
|
|
1
2
|
export * from './constants.js';
|
|
2
3
|
export * from './database.interface.js';
|
|
3
4
|
export * from './database.js';
|
|
4
5
|
export * from './delete-result.js';
|
|
5
6
|
export * from './update-result.js';
|
|
7
|
+
export * from './join-filter.interface.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { entityUtils } from '@loomcore/common/utils';
|
|
2
2
|
import { BadRequestError } from '../../../errors/index.js';
|
|
3
|
+
import { splitFiltersByType } from '../../utils/resolve-join-filter.js';
|
|
3
4
|
import NoSqlPipeline from '../models/no-sql-pipeline.js';
|
|
5
|
+
import { buildJoinMatchStage } from '../utils/build-join-match.util.js';
|
|
4
6
|
export async function getById(db, operations, queryObject, id, pluralResourceName) {
|
|
5
7
|
if (!entityUtils.isValidObjectId(id)) {
|
|
6
8
|
throw new BadRequestError('id is not a valid ObjectId');
|
|
@@ -8,10 +10,15 @@ export async function getById(db, operations, queryObject, id, pluralResourceNam
|
|
|
8
10
|
const collection = db.collection(pluralResourceName);
|
|
9
11
|
queryObject.filters || (queryObject.filters = {});
|
|
10
12
|
queryObject.filters._id = { eq: id };
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
.
|
|
14
|
-
.
|
|
13
|
+
const { rootOptions, joinFilters } = splitFiltersByType(queryObject, operations, pluralResourceName);
|
|
14
|
+
const pipelineBuilder = new NoSqlPipeline()
|
|
15
|
+
.addMatch(rootOptions)
|
|
16
|
+
.addOperations(operations);
|
|
17
|
+
const joinMatch = buildJoinMatchStage(joinFilters, operations);
|
|
18
|
+
if (joinMatch) {
|
|
19
|
+
pipelineBuilder.addStage(joinMatch);
|
|
20
|
+
}
|
|
21
|
+
const pipeline = pipelineBuilder.build();
|
|
15
22
|
const entity = await collection.aggregate(pipeline).next();
|
|
16
23
|
return entity;
|
|
17
24
|
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
import { splitFiltersByType } from '../../utils/resolve-join-filter.js';
|
|
1
2
|
import NoSqlPipeline from '../models/no-sql-pipeline.js';
|
|
3
|
+
import { buildJoinMatchStage } from '../utils/build-join-match.util.js';
|
|
2
4
|
import { apiUtils } from '../../../utils/api.utils.js';
|
|
3
5
|
export async function get(db, operations, queryOptions, modelSpec, pluralResourceName) {
|
|
4
6
|
const collection = db.collection(pluralResourceName);
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
.
|
|
7
|
+
const { rootOptions, joinFilters } = splitFiltersByType(queryOptions, operations, pluralResourceName);
|
|
8
|
+
const pipelineBuilder = new NoSqlPipeline()
|
|
9
|
+
.addMatch(rootOptions, modelSpec)
|
|
10
|
+
.addOperations(operations);
|
|
11
|
+
const joinMatch = buildJoinMatchStage(joinFilters, operations);
|
|
12
|
+
if (joinMatch) {
|
|
13
|
+
pipelineBuilder.addStage(joinMatch);
|
|
14
|
+
}
|
|
15
|
+
const pipeline = pipelineBuilder
|
|
8
16
|
.addQueryOptions(queryOptions, true)
|
|
9
17
|
.build();
|
|
10
18
|
const cursor = collection.aggregate(pipeline);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Document } from 'mongodb';
|
|
2
|
+
import type { Operation } from '../../operations/operation.js';
|
|
3
|
+
import { JoinFilter } from '../../models/join-filter.interface.js';
|
|
4
|
+
export declare function buildJoinMatchStage(joinFilters: JoinFilter[], operations: Operation[]): Document | null;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { entityUtils } from '@loomcore/common/utils';
|
|
2
|
+
import { ObjectId } from 'mongodb';
|
|
3
|
+
import { LeftJoinMany } from '../../operations/left-join-many.operation.js';
|
|
4
|
+
import { PROPERTIES_THAT_ARE_NOT_OBJECT_IDS } from '../../models/constants.js';
|
|
5
|
+
import { findJoinOperation } from '../../utils/resolve-join-filter.js';
|
|
6
|
+
function shouldConvertEqToObjectId(column) {
|
|
7
|
+
if (PROPERTIES_THAT_ARE_NOT_OBJECT_IDS.includes(column)) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
return column === '_id' || column.endsWith('Id');
|
|
11
|
+
}
|
|
12
|
+
function shouldConvertInToObjectIds(column) {
|
|
13
|
+
if (PROPERTIES_THAT_ARE_NOT_OBJECT_IDS.includes(column)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return column === '_id' || column.endsWith('Id') || column.endsWith('Ids');
|
|
17
|
+
}
|
|
18
|
+
function convertEqValue(column, value) {
|
|
19
|
+
if (shouldConvertEqToObjectId(column) && typeof value === 'string' && entityUtils.isValidObjectId(value)) {
|
|
20
|
+
return new ObjectId(value);
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === 'string' && !isNaN(Number(value))) {
|
|
23
|
+
return Number(value);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function convertInValues(column, values) {
|
|
28
|
+
if (shouldConvertInToObjectIds(column)) {
|
|
29
|
+
const objectIds = values
|
|
30
|
+
.filter(val => typeof val === 'string' && entityUtils.isValidObjectId(val))
|
|
31
|
+
.map(val => new ObjectId(val));
|
|
32
|
+
if (objectIds.length === 0) {
|
|
33
|
+
return { values: [], dropped: true };
|
|
34
|
+
}
|
|
35
|
+
return { values: objectIds, dropped: false };
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
values: values.map(val => typeof val === 'string' && !isNaN(Number(val)) ? Number(val) : val),
|
|
39
|
+
dropped: false,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function compareExpression(fieldExpr, column, filter) {
|
|
43
|
+
const parts = [];
|
|
44
|
+
if (filter.eq !== undefined) {
|
|
45
|
+
parts.push({ $eq: [fieldExpr, convertEqValue(column, filter.eq)] });
|
|
46
|
+
}
|
|
47
|
+
if (filter.ne !== undefined) {
|
|
48
|
+
parts.push({ $ne: [fieldExpr, convertEqValue(column, filter.ne)] });
|
|
49
|
+
}
|
|
50
|
+
if (filter.in !== undefined && Array.isArray(filter.in)) {
|
|
51
|
+
const converted = convertInValues(column, filter.in);
|
|
52
|
+
if (!converted.dropped) {
|
|
53
|
+
parts.push({ $in: [fieldExpr, converted.values] });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (filter.gte !== undefined) {
|
|
57
|
+
parts.push({ $gte: [fieldExpr, filter.gte] });
|
|
58
|
+
}
|
|
59
|
+
if (filter.lte !== undefined) {
|
|
60
|
+
parts.push({ $lte: [fieldExpr, filter.lte] });
|
|
61
|
+
}
|
|
62
|
+
if (filter.gt !== undefined) {
|
|
63
|
+
parts.push({ $gt: [fieldExpr, filter.gt] });
|
|
64
|
+
}
|
|
65
|
+
if (filter.lt !== undefined) {
|
|
66
|
+
parts.push({ $lt: [fieldExpr, filter.lt] });
|
|
67
|
+
}
|
|
68
|
+
if (filter.contains !== undefined) {
|
|
69
|
+
parts.push({
|
|
70
|
+
$cond: [
|
|
71
|
+
{ $eq: [{ $type: fieldExpr }, 'string'] },
|
|
72
|
+
{ $regexMatch: { input: fieldExpr, regex: filter.contains, options: 'i' } },
|
|
73
|
+
false,
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (parts.length === 0) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (parts.length === 1) {
|
|
81
|
+
return parts[0];
|
|
82
|
+
}
|
|
83
|
+
return { $and: parts };
|
|
84
|
+
}
|
|
85
|
+
function scalarAssignment(column, filter) {
|
|
86
|
+
const ops = {};
|
|
87
|
+
if (filter.eq !== undefined) {
|
|
88
|
+
ops.$eq = convertEqValue(column, filter.eq);
|
|
89
|
+
}
|
|
90
|
+
if (filter.ne !== undefined) {
|
|
91
|
+
ops.$ne = convertEqValue(column, filter.ne);
|
|
92
|
+
}
|
|
93
|
+
if (filter.in !== undefined && Array.isArray(filter.in)) {
|
|
94
|
+
const converted = convertInValues(column, filter.in);
|
|
95
|
+
if (!converted.dropped) {
|
|
96
|
+
ops.$in = converted.values;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (filter.gte !== undefined) {
|
|
100
|
+
ops.$gte = filter.gte;
|
|
101
|
+
}
|
|
102
|
+
if (filter.lte !== undefined) {
|
|
103
|
+
ops.$lte = filter.lte;
|
|
104
|
+
}
|
|
105
|
+
if (filter.gt !== undefined) {
|
|
106
|
+
ops.$gt = filter.gt;
|
|
107
|
+
}
|
|
108
|
+
if (filter.lt !== undefined) {
|
|
109
|
+
ops.$lt = filter.lt;
|
|
110
|
+
}
|
|
111
|
+
if (filter.contains !== undefined) {
|
|
112
|
+
ops.$regex = filter.contains;
|
|
113
|
+
ops.$options = 'i';
|
|
114
|
+
}
|
|
115
|
+
const keys = Object.keys(ops);
|
|
116
|
+
if (keys.length === 0) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
if (keys.length === 1 && keys[0] === '$eq') {
|
|
120
|
+
return ops.$eq;
|
|
121
|
+
}
|
|
122
|
+
return ops;
|
|
123
|
+
}
|
|
124
|
+
function anyMatchExpression(joinFilter, operations) {
|
|
125
|
+
const walk = (scope, index) => {
|
|
126
|
+
const alias = joinFilter.aliases[index];
|
|
127
|
+
const operation = findJoinOperation(operations, alias);
|
|
128
|
+
if (!operation) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const here = scope ? `${scope}.${alias}` : `$_joinData.${alias}`;
|
|
132
|
+
const isLast = index === joinFilter.aliases.length - 1;
|
|
133
|
+
if (operation instanceof LeftJoinMany) {
|
|
134
|
+
const varName = `j${index}`;
|
|
135
|
+
const cond = isLast
|
|
136
|
+
? compareExpression(`$$${varName}.${joinFilter.column}`, joinFilter.column, joinFilter.filter)
|
|
137
|
+
: walk(`$$${varName}`, index + 1);
|
|
138
|
+
if (!cond) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
$gt: [
|
|
143
|
+
{
|
|
144
|
+
$size: {
|
|
145
|
+
$filter: {
|
|
146
|
+
input: { $ifNull: [here, []] },
|
|
147
|
+
as: varName,
|
|
148
|
+
cond,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
0,
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (isLast) {
|
|
157
|
+
return compareExpression(`${here}.${joinFilter.column}`, joinFilter.column, joinFilter.filter);
|
|
158
|
+
}
|
|
159
|
+
return walk(here, index + 1);
|
|
160
|
+
};
|
|
161
|
+
return walk('', 0);
|
|
162
|
+
}
|
|
163
|
+
export function buildJoinMatchStage(joinFilters, operations) {
|
|
164
|
+
const match = {};
|
|
165
|
+
const expressions = [];
|
|
166
|
+
for (const joinFilter of joinFilters) {
|
|
167
|
+
if (joinFilter.crossesMany) {
|
|
168
|
+
const expression = anyMatchExpression(joinFilter, operations);
|
|
169
|
+
if (expression) {
|
|
170
|
+
expressions.push(expression);
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const assignment = scalarAssignment(joinFilter.column, joinFilter.filter);
|
|
175
|
+
if (assignment !== undefined) {
|
|
176
|
+
match[`_joinData.${joinFilter.aliases.join('.')}.${joinFilter.column}`] = assignment;
|
|
177
|
+
}
|
|
178
|
+
if (joinFilter.filter.ne !== undefined) {
|
|
179
|
+
match[`_joinData.${joinFilter.aliases.join('.')}`] = { $ne: null };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (expressions.length === 1) {
|
|
183
|
+
match.$expr = expressions[0];
|
|
184
|
+
}
|
|
185
|
+
else if (expressions.length > 1) {
|
|
186
|
+
match.$expr = { $and: expressions };
|
|
187
|
+
}
|
|
188
|
+
if (Object.keys(match).length === 0) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return { $match: match };
|
|
192
|
+
}
|
|
@@ -13,17 +13,27 @@ function resolveLocalFieldPath(localField, processedOperations) {
|
|
|
13
13
|
}
|
|
14
14
|
return localField;
|
|
15
15
|
}
|
|
16
|
+
function innerJoinDropsDocument(operation, processedOperations) {
|
|
17
|
+
if (!(operation instanceof InnerJoin)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const dot = operation.localField.indexOf('.');
|
|
21
|
+
if (dot === -1) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
const parentAlias = operation.localField.slice(0, dot);
|
|
25
|
+
const parent = processedOperations.find(op => (op instanceof LeftJoin || op instanceof InnerJoin || op instanceof LeftJoinMany) && op.as === parentAlias);
|
|
26
|
+
return !(parent instanceof LeftJoinMany);
|
|
27
|
+
}
|
|
16
28
|
export function convertOperationsToPipeline(operations) {
|
|
17
29
|
let pipeline = [];
|
|
18
30
|
const processedOperations = [];
|
|
19
|
-
const joinAliases = operations
|
|
20
|
-
.filter(op => op instanceof LeftJoin || op instanceof InnerJoin || op instanceof LeftJoinMany)
|
|
21
|
-
.map(op => op.as);
|
|
22
31
|
operations.forEach(operation => {
|
|
23
32
|
if (operation instanceof LeftJoin || operation instanceof InnerJoin) {
|
|
24
33
|
const needsObjectIdConversion = operation.foreignField === '_id';
|
|
25
34
|
const isNestedField = operation.localField.includes('.');
|
|
26
35
|
const resolvedLocalField = resolveLocalFieldPath(operation.localField, processedOperations);
|
|
36
|
+
const preserveMissingMatch = !innerJoinDropsDocument(operation, processedOperations);
|
|
27
37
|
if (needsObjectIdConversion) {
|
|
28
38
|
pipeline.push({
|
|
29
39
|
$lookup: {
|
|
@@ -51,7 +61,7 @@ export function convertOperationsToPipeline(operations) {
|
|
|
51
61
|
}, {
|
|
52
62
|
$unwind: {
|
|
53
63
|
path: `$${operation.as}Arr`,
|
|
54
|
-
preserveNullAndEmptyArrays:
|
|
64
|
+
preserveNullAndEmptyArrays: preserveMissingMatch
|
|
55
65
|
}
|
|
56
66
|
}, {
|
|
57
67
|
$addFields: {
|
|
@@ -79,7 +89,7 @@ export function convertOperationsToPipeline(operations) {
|
|
|
79
89
|
}, {
|
|
80
90
|
$unwind: {
|
|
81
91
|
path: `$${operation.as}Arr`,
|
|
82
|
-
preserveNullAndEmptyArrays:
|
|
92
|
+
preserveNullAndEmptyArrays: preserveMissingMatch
|
|
83
93
|
}
|
|
84
94
|
}, {
|
|
85
95
|
$addFields: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -39,7 +39,7 @@ export async function batchUpdate(connection, entities, operations, queryObject,
|
|
|
39
39
|
const { columns, values } = columnsAndValuesFromEntity(updateData);
|
|
40
40
|
const setClause = columns.map((col, index) => `${col} = $${index + 1}`).join(', ');
|
|
41
41
|
queryObject.filters._id = { eq: _id };
|
|
42
|
-
const { whereClause } = buildWhereClause(queryObject, values);
|
|
42
|
+
const { whereClause } = buildWhereClause(queryObject, pluralResourceName, values);
|
|
43
43
|
const query = `
|
|
44
44
|
UPDATE "${pluralResourceName}"
|
|
45
45
|
SET ${setClause}
|
|
@@ -50,9 +50,8 @@ export async function batchUpdate(connection, entities, operations, queryObject,
|
|
|
50
50
|
await session.query('COMMIT');
|
|
51
51
|
const joinClauses = buildJoinClauses(operations, pluralResourceName);
|
|
52
52
|
const hasJoins = operations.some(op => op instanceof LeftJoin || op instanceof InnerJoin || op instanceof LeftJoinMany);
|
|
53
|
-
const tablePrefix = hasJoins ? pluralResourceName : undefined;
|
|
54
53
|
queryObject.filters._id = { in: entityIds };
|
|
55
|
-
const { whereClause, values } = buildWhereClause(queryObject,
|
|
54
|
+
const { whereClause, values } = buildWhereClause(queryObject, pluralResourceName);
|
|
56
55
|
const selectClause = hasJoins
|
|
57
56
|
? await buildSelectClause(session, pluralResourceName, operations)
|
|
58
57
|
: '*';
|
|
@@ -2,7 +2,7 @@ import { buildWhereClause } from '../utils/build-where-clause.js';
|
|
|
2
2
|
import { DeleteResult } from '../../models/delete-result.js';
|
|
3
3
|
export async function deleteMany(client, queryObject, pluralResourceName) {
|
|
4
4
|
try {
|
|
5
|
-
const { whereClause, values } = buildWhereClause(queryObject);
|
|
5
|
+
const { whereClause, values } = buildWhereClause(queryObject, pluralResourceName);
|
|
6
6
|
const query = `DELETE FROM "${pluralResourceName}" ${whereClause}`;
|
|
7
7
|
const result = await client.query(query, values);
|
|
8
8
|
return new DeleteResult(true, result.rowCount || 0);
|
|
@@ -54,9 +54,7 @@ export async function fullUpdateById(client, operations, id, entity, pluralResou
|
|
|
54
54
|
throw new IdNotFoundError();
|
|
55
55
|
}
|
|
56
56
|
const hasJoins = operations.some(op => op instanceof LeftJoin || op instanceof InnerJoin || op instanceof LeftJoinMany);
|
|
57
|
-
const joinClauses =
|
|
58
|
-
? buildJoinClauses(operations, pluralResourceName, { oneToOneOnly: true })
|
|
59
|
-
: buildJoinClauses(operations, pluralResourceName);
|
|
57
|
+
const joinClauses = buildJoinClauses(operations, pluralResourceName);
|
|
60
58
|
const selectClause = hasJoins
|
|
61
59
|
? await buildSelectClause(client, pluralResourceName, operations)
|
|
62
60
|
: '*';
|
|
@@ -24,9 +24,7 @@ export async function partialUpdateById(client, operations, id, entity, pluralRe
|
|
|
24
24
|
throw new IdNotFoundError();
|
|
25
25
|
}
|
|
26
26
|
const hasJoins = operations.some(op => op instanceof LeftJoin || op instanceof InnerJoin || op instanceof LeftJoinMany);
|
|
27
|
-
const joinClauses =
|
|
28
|
-
? buildJoinClauses(operations, pluralResourceName, { oneToOneOnly: true })
|
|
29
|
-
: buildJoinClauses(operations, pluralResourceName);
|
|
27
|
+
const joinClauses = buildJoinClauses(operations, pluralResourceName);
|
|
30
28
|
const selectClause = hasJoins
|
|
31
29
|
? await buildSelectClause(client, pluralResourceName, operations)
|
|
32
30
|
: '*';
|
|
@@ -9,7 +9,7 @@ import { buildSelectClause } from '../utils/build-select-clause.js';
|
|
|
9
9
|
import { columnsAndValuesFromEntity } from '../utils/columns-and-values-from-entity.js';
|
|
10
10
|
export async function update(client, queryObject, entity, operations, pluralResourceName) {
|
|
11
11
|
try {
|
|
12
|
-
const { whereClause, values: whereValues } = buildWhereClause(queryObject);
|
|
12
|
+
const { whereClause, values: whereValues } = buildWhereClause(queryObject, pluralResourceName);
|
|
13
13
|
const { columns, values: entityValues } = columnsAndValuesFromEntity(entity);
|
|
14
14
|
const updateColumns = columns.filter(col => col !== '"_id"');
|
|
15
15
|
const updateValues = entityValues.filter((_, index) => columns[index] !== '"_id"');
|
|
@@ -32,12 +32,9 @@ export async function update(client, queryObject, entity, operations, pluralReso
|
|
|
32
32
|
throw new NotFoundError('No records found matching update query');
|
|
33
33
|
}
|
|
34
34
|
const hasJoins = operations.some(op => op instanceof LeftJoin || op instanceof InnerJoin || op instanceof LeftJoinMany);
|
|
35
|
-
const joinClauses =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const orderByClause = buildOrderByClause(queryObject, hasJoins ? { tablePrefix: pluralResourceName } : undefined);
|
|
39
|
-
const tablePrefix = hasJoins ? pluralResourceName : undefined;
|
|
40
|
-
const { whereClause: selectWhereClause, values: selectWhereValues } = buildWhereClause(queryObject, [], tablePrefix);
|
|
35
|
+
const joinClauses = buildJoinClauses(operations, pluralResourceName);
|
|
36
|
+
const orderByClause = buildOrderByClause(queryObject, pluralResourceName);
|
|
37
|
+
const { whereClause: selectWhereClause, values: selectWhereValues } = buildWhereClause(queryObject, pluralResourceName);
|
|
41
38
|
const selectClause = hasJoins
|
|
42
39
|
? await buildSelectClause(client, pluralResourceName, operations)
|
|
43
40
|
: '*';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { buildWhereClause } from '../utils/build-where-clause.js';
|
|
2
2
|
import { buildOrderByClause } from '../utils/build-order-by-clause.js';
|
|
3
3
|
export async function findOne(client, queryObject, pluralResourceName) {
|
|
4
|
-
const { whereClause, values } = buildWhereClause(queryObject);
|
|
5
|
-
const orderByClause = buildOrderByClause(queryObject);
|
|
4
|
+
const { whereClause, values } = buildWhereClause(queryObject, pluralResourceName);
|
|
5
|
+
const orderByClause = buildOrderByClause(queryObject, pluralResourceName);
|
|
6
6
|
const query = `SELECT * FROM "${pluralResourceName}" ${whereClause} ${orderByClause} LIMIT 1`;
|
|
7
7
|
const result = await client.query(query, values);
|
|
8
8
|
if (result.rows.length === 0) {
|
|
@@ -2,8 +2,8 @@ import { buildWhereClause } from '../utils/build-where-clause.js';
|
|
|
2
2
|
import { buildOrderByClause } from '../utils/build-order-by-clause.js';
|
|
3
3
|
import { buildPaginationClause } from '../utils/build-pagination-clause.js';
|
|
4
4
|
export async function find(client, queryObject, pluralResourceName) {
|
|
5
|
-
const { whereClause, values } = buildWhereClause(queryObject);
|
|
6
|
-
const orderByClause = buildOrderByClause(queryObject);
|
|
5
|
+
const { whereClause, values } = buildWhereClause(queryObject, pluralResourceName);
|
|
6
|
+
const orderByClause = buildOrderByClause(queryObject, pluralResourceName);
|
|
7
7
|
const paginationClause = buildPaginationClause(queryObject);
|
|
8
8
|
const query = `SELECT * FROM "${pluralResourceName}" ${whereClause} ${orderByClause} ${paginationClause}`.trim();
|
|
9
9
|
const result = await client.query(query, values);
|
|
@@ -5,9 +5,7 @@ import { buildJoinClauses } from '../utils/build-join-clauses.js';
|
|
|
5
5
|
import { buildSelectClause } from '../utils/build-select-clause.js';
|
|
6
6
|
export async function getAll(client, operations, pluralResourceName) {
|
|
7
7
|
const hasJoins = operations.some(op => op instanceof LeftJoin || op instanceof InnerJoin || op instanceof LeftJoinMany);
|
|
8
|
-
const joinClauses =
|
|
9
|
-
? buildJoinClauses(operations, pluralResourceName, { oneToOneOnly: true })
|
|
10
|
-
: buildJoinClauses(operations, pluralResourceName);
|
|
8
|
+
const joinClauses = buildJoinClauses(operations, pluralResourceName);
|
|
11
9
|
const selectClause = hasJoins
|
|
12
10
|
? await buildSelectClause(client, pluralResourceName, operations)
|
|
13
11
|
: '*';
|
|
@@ -9,16 +9,13 @@ export async function getById(client, operations, queryObject, id, pluralResourc
|
|
|
9
9
|
if (!id)
|
|
10
10
|
throw new BadRequestError('id is required');
|
|
11
11
|
const hasJoins = operations.some(op => op instanceof LeftJoin || op instanceof InnerJoin || op instanceof LeftJoinMany);
|
|
12
|
-
const joinClauses =
|
|
13
|
-
? buildJoinClauses(operations, pluralResourceName, { oneToOneOnly: true })
|
|
14
|
-
: buildJoinClauses(operations, pluralResourceName);
|
|
12
|
+
const joinClauses = buildJoinClauses(operations, pluralResourceName);
|
|
15
13
|
const selectClause = hasJoins
|
|
16
14
|
? await buildSelectClause(client, pluralResourceName, operations)
|
|
17
15
|
: '*';
|
|
18
16
|
queryObject.filters || (queryObject.filters = {});
|
|
19
17
|
queryObject.filters._id = { eq: id };
|
|
20
|
-
const
|
|
21
|
-
const { whereClause, values } = buildWhereClause(queryObject, [], tablePrefix);
|
|
18
|
+
const { whereClause, values } = buildWhereClause(queryObject, pluralResourceName, [], operations);
|
|
22
19
|
const query = `SELECT ${selectClause} FROM "${pluralResourceName}" ${joinClauses} ${whereClause} LIMIT 1`;
|
|
23
20
|
const result = await client.query(query, values);
|
|
24
21
|
if (result.rows.length === 0) {
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { LeftJoin } from '../../operations/left-join.operation.js';
|
|
2
|
-
import { InnerJoin } from '../../operations/inner-join.operation.js';
|
|
3
|
-
import { LeftJoinMany } from '../../operations/left-join-many.operation.js';
|
|
4
1
|
import { buildWhereClause } from '../utils/build-where-clause.js';
|
|
5
2
|
import { buildOrderByClause } from '../utils/build-order-by-clause.js';
|
|
6
3
|
import { buildJoinClauses } from '../utils/build-join-clauses.js';
|
|
@@ -8,20 +5,18 @@ import { buildSelectClause } from '../utils/build-select-clause.js';
|
|
|
8
5
|
import { executeCountQuery } from '../utils/build-count-query.js';
|
|
9
6
|
import { apiUtils } from '../../../utils/api.utils.js';
|
|
10
7
|
import { buildPaginationClause } from '../utils/build-pagination-clause.js';
|
|
8
|
+
import { isJoinOperation } from '../../utils/is-join-operation.util.js';
|
|
11
9
|
export async function get(client, operations, queryOptions, pluralResourceName) {
|
|
12
|
-
const hasJoins = operations.some(
|
|
13
|
-
const joinClauses =
|
|
14
|
-
|
|
15
|
-
: buildJoinClauses(operations, pluralResourceName);
|
|
16
|
-
const orderByClause = buildOrderByClause(queryOptions, hasJoins ? { tablePrefix: pluralResourceName } : undefined);
|
|
10
|
+
const hasJoins = operations.some(isJoinOperation);
|
|
11
|
+
const joinClauses = buildJoinClauses(operations, pluralResourceName);
|
|
12
|
+
const orderByClause = buildOrderByClause(queryOptions, pluralResourceName);
|
|
17
13
|
const paginationClause = buildPaginationClause(queryOptions);
|
|
18
14
|
const selectClause = hasJoins
|
|
19
15
|
? await buildSelectClause(client, pluralResourceName, operations)
|
|
20
16
|
: '*';
|
|
21
|
-
const
|
|
22
|
-
const { whereClause, values } = buildWhereClause(queryOptions, [], tablePrefix);
|
|
17
|
+
const { whereClause, values } = buildWhereClause(queryOptions, pluralResourceName, [], operations);
|
|
23
18
|
const baseQuery = `SELECT ${selectClause} FROM "${pluralResourceName}" ${joinClauses} ${whereClause}`;
|
|
24
|
-
const total = await executeCountQuery(client, pluralResourceName, queryOptions);
|
|
19
|
+
const total = await executeCountQuery(client, pluralResourceName, queryOptions, operations);
|
|
25
20
|
const dataQuery = `${baseQuery} ${orderByClause} ${paginationClause}`.trim();
|
|
26
21
|
const dataResult = await client.query(dataQuery, values);
|
|
27
22
|
const entities = hasJoins
|
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
import type { PostgresConnection } from '../postgres-connection.js';
|
|
2
2
|
import { IQueryOptions } from '@loomcore/common/models';
|
|
3
|
-
|
|
3
|
+
import type { Operation } from '../../operations/operation.js';
|
|
4
|
+
export declare function buildCountQuerySql(pluralResourceName: string, queryOptions?: IQueryOptions, operations?: Operation[]): {
|
|
5
|
+
countQuery: string;
|
|
6
|
+
values: any[];
|
|
7
|
+
};
|
|
8
|
+
export declare function executeCountQuery(client: PostgresConnection, pluralResourceName: string, queryOptions?: IQueryOptions, operations?: Operation[]): Promise<number>;
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
import { buildJoinClauses } from './build-join-clauses.js';
|
|
1
2
|
import { buildWhereClause } from './build-where-clause.js';
|
|
2
|
-
export
|
|
3
|
-
const
|
|
4
|
-
const
|
|
3
|
+
export function buildCountQuerySql(pluralResourceName, queryOptions = {}, operations = []) {
|
|
4
|
+
const joinClauses = buildJoinClauses(operations, pluralResourceName);
|
|
5
|
+
const { whereClause, values } = buildWhereClause(queryOptions, pluralResourceName, [], operations);
|
|
6
|
+
const countQuery = ['SELECT COUNT(*) as total', `FROM "${pluralResourceName}"`, joinClauses, whereClause]
|
|
7
|
+
.filter(part => part.trim().length > 0)
|
|
8
|
+
.join(' ');
|
|
9
|
+
return { countQuery, values };
|
|
10
|
+
}
|
|
11
|
+
export async function executeCountQuery(client, pluralResourceName, queryOptions = {}, operations = []) {
|
|
12
|
+
const { countQuery, values } = buildCountQuerySql(pluralResourceName, queryOptions, operations);
|
|
5
13
|
const countResult = await client.query(countQuery, values);
|
|
6
14
|
return parseInt(countResult.rows[0].total, 10);
|
|
7
15
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
import { Operation } from '../../operations/operation.js';
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
export declare function buildJoinClauses(operations: Operation[], mainTableName?: string, options?: BuildJoinClausesOptions): string;
|
|
2
|
+
export declare function collectOuterOneToOneAliases(operations: Operation[], mainTableName: string): Set<string>;
|
|
3
|
+
export declare function buildJoinClauses(operations: Operation[], mainTableName: string): string;
|
|
@@ -1,54 +1,47 @@
|
|
|
1
1
|
import { LeftJoin } from '../../operations/left-join.operation.js';
|
|
2
2
|
import { InnerJoin } from '../../operations/inner-join.operation.js';
|
|
3
|
-
import { LeftJoinMany } from '../../operations/left-join-many.operation.js';
|
|
4
3
|
import { toPostgresStoreName } from './convert-keys.util.js';
|
|
5
4
|
function getParentAlias(localField) {
|
|
6
5
|
if (!localField.includes('.'))
|
|
7
6
|
return null;
|
|
8
7
|
return localField.split('.')[0];
|
|
9
8
|
}
|
|
10
|
-
function
|
|
11
|
-
const
|
|
12
|
-
for (const
|
|
13
|
-
if (
|
|
14
|
-
|
|
9
|
+
export function collectOuterOneToOneAliases(operations, mainTableName) {
|
|
10
|
+
const aliases = new Set();
|
|
11
|
+
for (const operation of operations) {
|
|
12
|
+
if (!(operation instanceof LeftJoin) && !(operation instanceof InnerJoin)) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const parent = getParentAlias(operation.localField);
|
|
16
|
+
const parentInScope = parent === null || parent === mainTableName || aliases.has(parent);
|
|
17
|
+
if (!parentInScope) {
|
|
18
|
+
continue;
|
|
15
19
|
}
|
|
20
|
+
aliases.add(operation.as);
|
|
16
21
|
}
|
|
17
|
-
return
|
|
22
|
+
return aliases;
|
|
18
23
|
}
|
|
19
|
-
export function buildJoinClauses(operations, mainTableName
|
|
20
|
-
const oneToOneOnly = options?.oneToOneOnly ?? false;
|
|
21
|
-
const throughTableAliases = oneToOneOnly ? getThroughTableAliases(operations) : new Set();
|
|
24
|
+
export function buildJoinClauses(operations, mainTableName) {
|
|
22
25
|
const joinClauses = [];
|
|
23
|
-
const oneToOneAliasesInScope =
|
|
26
|
+
const oneToOneAliasesInScope = collectOuterOneToOneAliases(operations, mainTableName);
|
|
24
27
|
for (const operation of operations) {
|
|
25
|
-
if (operation instanceof
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
if (!(operation instanceof LeftJoin) && !(operation instanceof InnerJoin)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (!oneToOneAliasesInScope.has(operation.as)) {
|
|
32
|
+
continue;
|
|
28
33
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
continue;
|
|
35
|
-
oneToOneAliasesInScope.add(operation.as);
|
|
36
|
-
}
|
|
34
|
+
const joinType = operation instanceof LeftJoin ? 'LEFT JOIN' : 'INNER JOIN';
|
|
35
|
+
let leftSide;
|
|
36
|
+
if (operation.localField.includes('.')) {
|
|
37
|
+
const [alias, column] = operation.localField.split('.');
|
|
38
|
+
leftSide = `"${alias}"."${column}"`;
|
|
37
39
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const joinType = useLeftJoin || operation instanceof LeftJoin || operation instanceof LeftJoinMany ? 'LEFT JOIN' : 'INNER JOIN';
|
|
41
|
-
let leftSide;
|
|
42
|
-
if (operation.localField.includes('.')) {
|
|
43
|
-
const [alias, column] = operation.localField.split('.');
|
|
44
|
-
leftSide = `"${alias}"."${column}"`;
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
leftSide = mainTableName ? `"${mainTableName}"."${operation.localField}"` : `"${operation.localField}"`;
|
|
48
|
-
}
|
|
49
|
-
const fromTable = toPostgresStoreName(operation.from);
|
|
50
|
-
joinClauses.push(`${joinType} "${fromTable}" AS "${operation.as}" ON ${leftSide} = "${operation.as}"."${operation.foreignField}"`);
|
|
40
|
+
else {
|
|
41
|
+
leftSide = `"${mainTableName}"."${operation.localField}"`;
|
|
51
42
|
}
|
|
43
|
+
const fromTable = toPostgresStoreName(operation.from);
|
|
44
|
+
joinClauses.push(`${joinType} "${fromTable}" AS "${operation.as}" ON ${leftSide} = "${operation.as}"."${operation.foreignField}"`);
|
|
52
45
|
}
|
|
53
46
|
return joinClauses.join(' ');
|
|
54
47
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Filter } from '@loomcore/common/models';
|
|
2
|
+
import type { Operation } from '../../operations/operation.js';
|
|
3
|
+
import { JoinFilter } from '../../models/join-filter.interface.js';
|
|
4
|
+
export interface SqlParamIndex {
|
|
5
|
+
current: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function toSqlColumnName(column: string): string;
|
|
8
|
+
export declare function appendFilterConditions(qualifiedKey: string, value: Filter, conditions: string[], values: any[], paramIndex: SqlParamIndex): void;
|
|
9
|
+
export declare function joinFilterSql(joinFilter: JoinFilter, operations: Operation[], mainTableName: string, values: any[], paramIndex: SqlParamIndex): string | null;
|
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,76 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
export function buildWhereClause(queryObject, values = [],
|
|
4
|
-
const
|
|
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 || {};
|
|
5
9
|
const conditions = [];
|
|
6
|
-
|
|
7
|
-
const qualifyColumn = (columnName) => {
|
|
8
|
-
return tablePrefix ? `"${tablePrefix}"."${columnName}"` : `"${columnName}"`;
|
|
9
|
-
};
|
|
10
|
+
const paramIndex = { current: values.length + 1 };
|
|
10
11
|
for (const [key, value] of Object.entries(filters)) {
|
|
11
12
|
if (value) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
const qualifiedKey = qualifyColumn(snakeKey);
|
|
23
|
-
if (value.eq !== undefined) {
|
|
24
|
-
const comparison = readFilterValue(value.eq);
|
|
25
|
-
if (comparison.kind === 'value') {
|
|
26
|
-
conditions.push(`(${qualifiedKey} = $${paramIndex} OR (${qualifiedKey} IS NULL AND $${paramIndex} IS NULL))`);
|
|
27
|
-
values.push(comparison.value);
|
|
28
|
-
paramIndex++;
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
conditions.push(`${qualifiedKey} IS NULL`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
if (value.ne !== undefined) {
|
|
35
|
-
const comparison = readFilterValue(value.ne);
|
|
36
|
-
if (comparison.kind === 'value') {
|
|
37
|
-
conditions.push(`(${qualifiedKey} != $${paramIndex} OR (${qualifiedKey} IS NOT NULL AND $${paramIndex} IS NULL) OR (${qualifiedKey} IS NULL AND $${paramIndex} IS NOT NULL))`);
|
|
38
|
-
values.push(comparison.value);
|
|
39
|
-
paramIndex++;
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
conditions.push(`${qualifiedKey} IS NOT NULL`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (value.in !== undefined && Array.isArray(value.in)) {
|
|
46
|
-
const placeholders = value.in.map(() => `$${paramIndex++}`).join(', ');
|
|
47
|
-
conditions.push(`${qualifiedKey} IN (${placeholders})`);
|
|
48
|
-
values.push(...value.in);
|
|
49
|
-
}
|
|
50
|
-
if (value.gte !== undefined) {
|
|
51
|
-
conditions.push(`${qualifiedKey} >= $${paramIndex}`);
|
|
52
|
-
values.push(value.gte);
|
|
53
|
-
paramIndex++;
|
|
54
|
-
}
|
|
55
|
-
if (value.lte !== undefined) {
|
|
56
|
-
conditions.push(`${qualifiedKey} <= $${paramIndex}`);
|
|
57
|
-
values.push(value.lte);
|
|
58
|
-
paramIndex++;
|
|
59
|
-
}
|
|
60
|
-
if (value.gt !== undefined) {
|
|
61
|
-
conditions.push(`${qualifiedKey} > $${paramIndex}`);
|
|
62
|
-
values.push(value.gt);
|
|
63
|
-
paramIndex++;
|
|
64
|
-
}
|
|
65
|
-
if (value.lt !== undefined) {
|
|
66
|
-
conditions.push(`${qualifiedKey} < $${paramIndex}`);
|
|
67
|
-
values.push(value.lt);
|
|
68
|
-
paramIndex++;
|
|
69
|
-
}
|
|
70
|
-
if (value.contains !== undefined) {
|
|
71
|
-
conditions.push(`LOWER(${qualifiedKey}) LIKE LOWER($${paramIndex})`);
|
|
72
|
-
values.push(`%${value.contains}%`);
|
|
73
|
-
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);
|
|
74
22
|
}
|
|
75
23
|
}
|
|
76
24
|
}
|
|
@@ -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,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
|
+
}
|