@loomcore/api 0.1.97 → 0.1.99
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/LICENSE +201 -201
- package/README.md +77 -77
- package/dist/__tests__/common-test.utils.js +3 -5
- package/dist/__tests__/postgres-test-migrations/postgres-test-schema.js +266 -266
- package/dist/__tests__/postgres.test-database.js +8 -8
- package/dist/databases/migrations/migration-runner.js +21 -21
- package/dist/databases/mongo-db/utils/convert-operations-to-pipeline.util.js +65 -473
- package/dist/databases/operations/index.d.ts +3 -4
- package/dist/databases/operations/index.js +3 -4
- package/dist/databases/operations/{join.operation.d.ts → inner-join.operation.d.ts} +1 -1
- package/dist/databases/operations/{join.operation.js → inner-join.operation.js} +2 -2
- package/dist/databases/operations/left-join-many.operation.d.ts +7 -0
- package/dist/databases/operations/left-join-many.operation.js +15 -0
- package/dist/databases/operations/{join-many.operation.d.ts → left-join.operation.d.ts} +1 -1
- package/dist/databases/operations/{join-many.operation.js → left-join.operation.js} +2 -2
- package/dist/databases/operations/operation.d.ts +4 -5
- package/dist/databases/postgres/commands/postgres-batch-update.command.js +11 -12
- package/dist/databases/postgres/commands/postgres-create-many.command.js +4 -4
- package/dist/databases/postgres/commands/postgres-create.command.js +4 -4
- package/dist/databases/postgres/commands/postgres-full-update-by-id.command.js +14 -14
- package/dist/databases/postgres/commands/postgres-partial-update-by-id.command.js +8 -8
- package/dist/databases/postgres/commands/postgres-update.command.js +8 -8
- package/dist/databases/postgres/migrations/postgres-initial-schema.js +224 -224
- package/dist/databases/postgres/postgres.database.js +17 -17
- package/dist/databases/postgres/queries/postgres-get-all.query.js +4 -5
- package/dist/databases/postgres/queries/postgres-get-by-id.query.js +4 -5
- package/dist/databases/postgres/queries/postgres-get.query.js +4 -5
- package/dist/databases/postgres/utils/build-join-clauses.d.ts +1 -1
- package/dist/databases/postgres/utils/build-join-clauses.js +26 -360
- package/dist/databases/postgres/utils/build-select-clause.js +15 -27
- package/dist/databases/postgres/utils/does-table-exist.util.js +4 -4
- package/dist/databases/postgres/utils/transform-join-results.js +19 -120
- package/package.json +92 -92
- package/dist/databases/operations/join-through-many.operation.d.ts +0 -10
- package/dist/databases/operations/join-through-many.operation.js +0 -21
- package/dist/databases/operations/join-through.operation.d.ts +0 -10
- package/dist/databases/operations/join-through.operation.js +0 -21
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { JoinThroughMany } from "../../operations/join-through-many.operation.js";
|
|
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";
|
|
5
4
|
import { buildJoinClauses } from "../utils/build-join-clauses.js";
|
|
6
5
|
import { buildSelectClause } from "../utils/build-select-clause.js";
|
|
7
6
|
import { transformJoinResults } from "../utils/transform-join-results.js";
|
|
@@ -11,7 +10,7 @@ export async function getById(client, operations, queryObject, id, pluralResourc
|
|
|
11
10
|
if (!id)
|
|
12
11
|
throw new BadRequestError('id is required');
|
|
13
12
|
const joinClauses = buildJoinClauses(operations, pluralResourceName);
|
|
14
|
-
const hasJoins = operations.some(op => op instanceof
|
|
13
|
+
const hasJoins = operations.some(op => op instanceof LeftJoin || op instanceof InnerJoin || op instanceof LeftJoinMany);
|
|
15
14
|
const selectClause = hasJoins
|
|
16
15
|
? await buildSelectClause(client, pluralResourceName, pluralResourceName, operations)
|
|
17
16
|
: '*';
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { JoinThroughMany } from "../../operations/join-through-many.operation.js";
|
|
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";
|
|
5
4
|
import { buildWhereClause } from "../utils/build-where-clause.js";
|
|
6
5
|
import { buildOrderByClause } from "../utils/build-order-by-clause.js";
|
|
7
6
|
import { buildJoinClauses } from "../utils/build-join-clauses.js";
|
|
@@ -14,7 +13,7 @@ export async function get(client, operations, queryOptions, pluralResourceName)
|
|
|
14
13
|
const joinClauses = buildJoinClauses(operations, pluralResourceName);
|
|
15
14
|
const orderByClause = buildOrderByClause(queryOptions);
|
|
16
15
|
const paginationClause = buildPaginationClause(queryOptions);
|
|
17
|
-
const hasJoins = operations.some(op => op instanceof
|
|
16
|
+
const hasJoins = operations.some(op => op instanceof LeftJoin || op instanceof InnerJoin || op instanceof LeftJoinMany);
|
|
18
17
|
const selectClause = hasJoins
|
|
19
18
|
? await buildSelectClause(client, pluralResourceName, pluralResourceName, operations)
|
|
20
19
|
: '*';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { Operation } from "../../operations/operation.js";
|
|
2
|
-
export declare function buildJoinClauses(operations: Operation[], mainTableName
|
|
2
|
+
export declare function buildJoinClauses(operations: Operation[], mainTableName: string): string;
|
|
@@ -1,374 +1,40 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { JoinThroughMany } from "../../operations/join-through-many.operation.js";
|
|
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";
|
|
5
4
|
import { toSnakeCase } from "./convert-keys.util.js";
|
|
6
5
|
function convertFieldToSnakeCase(field) {
|
|
7
|
-
if (field.startsWith(
|
|
6
|
+
if (field.startsWith("_")) {
|
|
8
7
|
return field;
|
|
9
8
|
}
|
|
10
|
-
|
|
11
|
-
return '_id';
|
|
12
|
-
}
|
|
13
|
-
else {
|
|
14
|
-
return toSnakeCase(field);
|
|
15
|
-
}
|
|
9
|
+
return toSnakeCase(field);
|
|
16
10
|
}
|
|
17
|
-
function resolveLocalField(localField, mainTableName
|
|
18
|
-
if (!localField.includes(
|
|
19
|
-
const
|
|
20
|
-
return mainTableName
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const objectJoin = operations.find(op => op instanceof JoinThrough && op.as === alias);
|
|
26
|
-
if (objectJoin) {
|
|
27
|
-
return `(${alias}.aggregated->>'${field}')::integer`;
|
|
28
|
-
}
|
|
29
|
-
const arrayJoin = operations.find(op => (op instanceof JoinMany || op instanceof JoinThroughMany) && op.as === alias);
|
|
30
|
-
if (arrayJoin) {
|
|
31
|
-
return `(${alias}.aggregated->>'${field}')::integer`;
|
|
32
|
-
}
|
|
33
|
-
const snakeField = convertFieldToSnakeCase(field);
|
|
34
|
-
return `${alias}."${snakeField}"`;
|
|
35
|
-
}
|
|
36
|
-
function findEnrichmentTarget(operation, operations) {
|
|
37
|
-
if (!operation.localField.includes('.')) {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
const [alias, field] = operation.localField.split('.');
|
|
41
|
-
const target = operations.find(op => (op instanceof JoinMany || op instanceof JoinThroughMany) && op.as === alias);
|
|
42
|
-
if (target && operations.indexOf(target) < operations.indexOf(operation)) {
|
|
43
|
-
return { target, field };
|
|
44
|
-
}
|
|
45
|
-
return null;
|
|
11
|
+
function resolveLocalField(localField, mainTableName) {
|
|
12
|
+
if (!localField.includes(".")) {
|
|
13
|
+
const snake = convertFieldToSnakeCase(localField);
|
|
14
|
+
return `"${mainTableName}"."${snake}"`;
|
|
15
|
+
}
|
|
16
|
+
const [alias, field] = localField.split(".");
|
|
17
|
+
const snake = convertFieldToSnakeCase(field);
|
|
18
|
+
return `${alias}."${snake}"`;
|
|
46
19
|
}
|
|
47
20
|
export function buildJoinClauses(operations, mainTableName) {
|
|
48
|
-
let
|
|
49
|
-
const processedAliases = new Set();
|
|
50
|
-
const enrichedAliases = new Set();
|
|
51
|
-
const joinOperations = operations.filter(op => op instanceof Join);
|
|
52
|
-
const targetsToEnrich = new Set();
|
|
53
|
-
const enrichmentsByTarget = new Map();
|
|
21
|
+
let joinClause = "";
|
|
54
22
|
for (const operation of operations) {
|
|
55
|
-
if (operation instanceof
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
for (const [targetAlias, enrichments] of enrichmentsByTarget.entries()) {
|
|
68
|
-
enrichments.sort((a, b) => operations.indexOf(a.operation) - operations.indexOf(b.operation));
|
|
69
|
-
}
|
|
70
|
-
for (const operation of operations) {
|
|
71
|
-
if (processedAliases.has(operation.as)) {
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
const isEnrichmentOp = (operation instanceof JoinMany || operation instanceof JoinThroughMany) &&
|
|
75
|
-
findEnrichmentTarget(operation, operations) !== null;
|
|
76
|
-
if (targetsToEnrich.has(operation.as) && !isEnrichmentOp) {
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (operation instanceof Join) {
|
|
80
|
-
const localFieldRef = resolveLocalField(operation.localField, mainTableName, operations);
|
|
81
|
-
joinClauses += ` LEFT JOIN "${operation.from}" AS ${operation.as} ON ${localFieldRef} = "${operation.as}"."${operation.foreignField}"`;
|
|
82
|
-
processedAliases.add(operation.as);
|
|
83
|
-
}
|
|
84
|
-
else if (operation instanceof JoinMany) {
|
|
85
|
-
const enrichment = findEnrichmentTarget(operation, operations);
|
|
86
|
-
if (enrichment) {
|
|
87
|
-
const enrichments = enrichmentsByTarget.get(enrichment.target.as);
|
|
88
|
-
const isFirstEnrichment = enrichments[0].operation === operation;
|
|
89
|
-
if (isFirstEnrichment && !enrichedAliases.has(enrichment.target.as)) {
|
|
90
|
-
const { target, field } = enrichment;
|
|
91
|
-
const targetLocalFieldRef = target.localField.includes('.')
|
|
92
|
-
? resolveLocalField(target.localField, mainTableName, operations)
|
|
93
|
-
: (() => {
|
|
94
|
-
const snakeField = convertFieldToSnakeCase(target.localField);
|
|
95
|
-
return mainTableName ? `"${mainTableName}"."${snakeField}"` : `"${snakeField}"`;
|
|
96
|
-
})();
|
|
97
|
-
let enrichmentJoins = '';
|
|
98
|
-
let jsonBuildObjects = '';
|
|
99
|
-
for (const enrich of enrichments) {
|
|
100
|
-
const enrichField = enrich.field;
|
|
101
|
-
const enrichOp = enrich.operation;
|
|
102
|
-
const enrichAlias = `enrich_${enrichOp.as}`;
|
|
103
|
-
const nestedJoin = joinOperations.find((j) => {
|
|
104
|
-
if (!j.localField.includes('.'))
|
|
105
|
-
return false;
|
|
106
|
-
const [referencedAlias] = j.localField.split('.');
|
|
107
|
-
const referencedJoin = operations.find(op => (op instanceof Join || op instanceof JoinMany || op instanceof JoinThroughMany) &&
|
|
108
|
-
op.as === referencedAlias);
|
|
109
|
-
if (referencedJoin instanceof Join && referencedJoin.from === enrichOp.from) {
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
if (enrichOp instanceof JoinThroughMany && referencedJoin instanceof Join && referencedJoin.from === enrichOp.from) {
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
return false;
|
|
116
|
-
});
|
|
117
|
-
if (nestedJoin) {
|
|
118
|
-
const nestedField = nestedJoin.localField.split('.')[1];
|
|
119
|
-
enrichmentJoins += ` LEFT JOIN LATERAL (
|
|
120
|
-
SELECT COALESCE(
|
|
121
|
-
JSON_AGG(
|
|
122
|
-
enrich_elem.value || jsonb_build_object('${nestedJoin.as}', nested_join_data.${nestedJoin.as})
|
|
123
|
-
),
|
|
124
|
-
'[]'::json
|
|
125
|
-
) AS enriched
|
|
126
|
-
FROM (
|
|
127
|
-
SELECT COALESCE(JSON_AGG(row_to_json(enrich_table)), '[]'::json) AS enriched
|
|
128
|
-
FROM "${enrichOp.from}" AS enrich_table
|
|
129
|
-
WHERE enrich_table."${enrichOp.foreignField}" = (elem.value->>'${enrichField}')::integer
|
|
130
|
-
AND enrich_table."_deleted" IS NULL
|
|
131
|
-
) AS enrich_data
|
|
132
|
-
CROSS JOIN LATERAL jsonb_array_elements(enrich_data.enriched::jsonb) AS enrich_elem
|
|
133
|
-
LEFT JOIN LATERAL (
|
|
134
|
-
SELECT row_to_json(nested_table) AS ${nestedJoin.as}
|
|
135
|
-
FROM "${nestedJoin.from}" AS nested_table
|
|
136
|
-
WHERE nested_table."${nestedJoin.foreignField}" = (enrich_elem.value->>'${nestedField}')::integer
|
|
137
|
-
AND nested_table."_deleted" IS NULL
|
|
138
|
-
LIMIT 1
|
|
139
|
-
) AS nested_join_data ON true
|
|
140
|
-
) AS ${enrichAlias} ON true`;
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
enrichmentJoins += ` LEFT JOIN LATERAL (
|
|
144
|
-
SELECT COALESCE(JSON_AGG(row_to_json(enrich_table)), '[]'::json) AS enriched
|
|
145
|
-
FROM "${enrichOp.from}" AS enrich_table
|
|
146
|
-
WHERE enrich_table."${enrichOp.foreignField}" = (elem.value->>'${enrichField}')::integer
|
|
147
|
-
AND enrich_table."_deleted" IS NULL
|
|
148
|
-
) AS ${enrichAlias} ON true`;
|
|
149
|
-
}
|
|
150
|
-
jsonBuildObjects += ` || jsonb_build_object('${enrichOp.as}', COALESCE(${enrichAlias}.enriched, '[]'::json))`;
|
|
151
|
-
}
|
|
152
|
-
joinClauses += ` LEFT JOIN LATERAL (
|
|
153
|
-
SELECT COALESCE(
|
|
154
|
-
JSON_AGG(
|
|
155
|
-
elem.value${jsonBuildObjects}
|
|
156
|
-
),
|
|
157
|
-
'[]'::json
|
|
158
|
-
) AS aggregated
|
|
159
|
-
FROM (
|
|
160
|
-
SELECT COALESCE(JSON_AGG(row_to_json(target_table)), '[]'::json) AS aggregated
|
|
161
|
-
FROM "${target.from}" AS target_table
|
|
162
|
-
WHERE target_table."${target.foreignField}" = ${targetLocalFieldRef}
|
|
163
|
-
AND target_table."_deleted" IS NULL
|
|
164
|
-
) AS original_data
|
|
165
|
-
CROSS JOIN LATERAL jsonb_array_elements(original_data.aggregated::jsonb) AS elem${enrichmentJoins}
|
|
166
|
-
) AS ${target.as} ON true`;
|
|
167
|
-
enrichedAliases.add(target.as);
|
|
168
|
-
processedAliases.add(target.as);
|
|
169
|
-
for (const enrich of enrichments) {
|
|
170
|
-
processedAliases.add(enrich.operation.as);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
const localFieldRef = resolveLocalField(operation.localField, mainTableName, operations);
|
|
176
|
-
joinClauses += ` LEFT JOIN LATERAL (
|
|
177
|
-
SELECT COALESCE(JSON_AGG(row_to_json("${operation.from}")), '[]'::json) AS aggregated
|
|
178
|
-
FROM "${operation.from}"
|
|
179
|
-
WHERE "${operation.from}"."${operation.foreignField}" = ${localFieldRef}
|
|
180
|
-
AND "${operation.from}"."_deleted" IS NULL
|
|
181
|
-
) AS ${operation.as} ON true`;
|
|
182
|
-
processedAliases.add(operation.as);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
else if (operation instanceof JoinThrough) {
|
|
186
|
-
const localFieldRef = resolveLocalField(operation.localField, mainTableName, operations);
|
|
187
|
-
joinClauses += ` LEFT JOIN LATERAL (
|
|
188
|
-
SELECT row_to_json(${operation.as}) AS aggregated
|
|
189
|
-
FROM "${operation.through}"
|
|
190
|
-
INNER JOIN "${operation.from}" AS ${operation.as}
|
|
191
|
-
ON ${operation.as}."${operation.foreignField}" = "${operation.through}"."${operation.throughForeignField}"
|
|
192
|
-
WHERE "${operation.through}"."${operation.throughLocalField}" = ${localFieldRef}
|
|
193
|
-
AND "${operation.through}"."_deleted" IS NULL
|
|
194
|
-
AND ${operation.as}."_deleted" IS NULL
|
|
195
|
-
LIMIT 1
|
|
196
|
-
) AS ${operation.as} ON true`;
|
|
197
|
-
processedAliases.add(operation.as);
|
|
198
|
-
}
|
|
199
|
-
else if (operation instanceof JoinThroughMany) {
|
|
200
|
-
const enrichment = findEnrichmentTarget(operation, operations);
|
|
201
|
-
if (enrichment) {
|
|
202
|
-
const enrichments = enrichmentsByTarget.get(enrichment.target.as);
|
|
203
|
-
const isFirstEnrichment = enrichments[0].operation === operation;
|
|
204
|
-
if (isFirstEnrichment && !enrichedAliases.has(enrichment.target.as)) {
|
|
205
|
-
const { target, field } = enrichment;
|
|
206
|
-
const targetLocalFieldRef = target.localField.includes('.')
|
|
207
|
-
? resolveLocalField(target.localField, mainTableName, operations)
|
|
208
|
-
: (() => {
|
|
209
|
-
const snakeField = convertFieldToSnakeCase(target.localField);
|
|
210
|
-
return mainTableName ? `"${mainTableName}"."${snakeField}"` : `"${snakeField}"`;
|
|
211
|
-
})();
|
|
212
|
-
const isTargetJoinMany = target instanceof JoinMany;
|
|
213
|
-
let enrichmentJoins = '';
|
|
214
|
-
let jsonBuildObjects = '';
|
|
215
|
-
for (const enrich of enrichments) {
|
|
216
|
-
const enrichField = enrich.field;
|
|
217
|
-
const enrichOp = enrich.operation;
|
|
218
|
-
const enrichAlias = `enrich_${enrichOp.as}`;
|
|
219
|
-
if (enrichOp instanceof JoinMany) {
|
|
220
|
-
const nestedJoin = joinOperations.find(j => j.localField.includes('.') &&
|
|
221
|
-
j.localField.startsWith(`${enrichOp.as}.`));
|
|
222
|
-
if (nestedJoin) {
|
|
223
|
-
const nestedField = nestedJoin.localField.split('.')[1];
|
|
224
|
-
enrichmentJoins += ` LEFT JOIN LATERAL (
|
|
225
|
-
SELECT COALESCE(
|
|
226
|
-
JSON_AGG(
|
|
227
|
-
enrich_elem.value || jsonb_build_object('${nestedJoin.as}', nested_join_data.${nestedJoin.as})
|
|
228
|
-
),
|
|
229
|
-
'[]'::json
|
|
230
|
-
) AS enriched
|
|
231
|
-
FROM (
|
|
232
|
-
SELECT COALESCE(JSON_AGG(row_to_json(enrich_table)), '[]'::json) AS enriched
|
|
233
|
-
FROM "${enrichOp.from}" AS enrich_table
|
|
234
|
-
WHERE enrich_table."${enrichOp.foreignField}" = (elem.value->>'${enrichField}')::integer
|
|
235
|
-
AND enrich_table."_deleted" IS NULL
|
|
236
|
-
) AS enrich_data
|
|
237
|
-
CROSS JOIN LATERAL jsonb_array_elements(enrich_data.enriched::jsonb) AS enrich_elem
|
|
238
|
-
LEFT JOIN LATERAL (
|
|
239
|
-
SELECT row_to_json(nested_table) AS ${nestedJoin.as}
|
|
240
|
-
FROM "${nestedJoin.from}" AS nested_table
|
|
241
|
-
WHERE nested_table."${nestedJoin.foreignField}" = (enrich_elem.value->>'${nestedField}')::integer
|
|
242
|
-
AND nested_table."_deleted" IS NULL
|
|
243
|
-
LIMIT 1
|
|
244
|
-
) AS nested_join_data ON true
|
|
245
|
-
) AS ${enrichAlias} ON true`;
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
enrichmentJoins += ` LEFT JOIN LATERAL (
|
|
249
|
-
SELECT COALESCE(JSON_AGG(row_to_json(enrich_table)), '[]'::json) AS enriched
|
|
250
|
-
FROM "${enrichOp.from}" AS enrich_table
|
|
251
|
-
WHERE enrich_table."${enrichOp.foreignField}" = (elem.value->>'${enrichField}')::integer
|
|
252
|
-
AND enrich_table."_deleted" IS NULL
|
|
253
|
-
) AS ${enrichAlias} ON true`;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
const nestedJoin = joinOperations.find((j) => {
|
|
258
|
-
if (!j.localField.includes('.'))
|
|
259
|
-
return false;
|
|
260
|
-
const [referencedAlias] = j.localField.split('.');
|
|
261
|
-
const referencedJoin = operations.find(op => (op instanceof Join || op instanceof JoinMany || op instanceof JoinThroughMany) &&
|
|
262
|
-
op.as === referencedAlias);
|
|
263
|
-
if (referencedJoin instanceof Join && referencedJoin.from === enrichOp.from) {
|
|
264
|
-
return true;
|
|
265
|
-
}
|
|
266
|
-
if (enrichOp instanceof JoinThroughMany && referencedJoin instanceof Join && referencedJoin.from === enrichOp.from) {
|
|
267
|
-
return true;
|
|
268
|
-
}
|
|
269
|
-
return false;
|
|
270
|
-
});
|
|
271
|
-
if (nestedJoin) {
|
|
272
|
-
const nestedField = nestedJoin.localField.split('.')[1];
|
|
273
|
-
enrichmentJoins += ` LEFT JOIN LATERAL (
|
|
274
|
-
SELECT COALESCE(
|
|
275
|
-
JSON_AGG(
|
|
276
|
-
enrich_elem.value || jsonb_build_object('${nestedJoin.as}', nested_join_data.${nestedJoin.as})
|
|
277
|
-
),
|
|
278
|
-
'[]'::json
|
|
279
|
-
) AS enriched
|
|
280
|
-
FROM (
|
|
281
|
-
SELECT COALESCE(JSON_AGG(row_to_json(${enrichAlias}_table)), '[]'::json) AS enriched
|
|
282
|
-
FROM "${enrichOp.through}"
|
|
283
|
-
INNER JOIN "${enrichOp.from}" AS ${enrichAlias}_table
|
|
284
|
-
ON ${enrichAlias}_table."${enrichOp.foreignField}" = "${enrichOp.through}"."${enrichOp.throughForeignField}"
|
|
285
|
-
WHERE "${enrichOp.through}"."${enrichOp.throughLocalField}" = (elem.value->>'${enrichField}')::integer
|
|
286
|
-
AND "${enrichOp.through}"."_deleted" IS NULL
|
|
287
|
-
AND ${enrichAlias}_table."_deleted" IS NULL
|
|
288
|
-
) AS enrich_data
|
|
289
|
-
CROSS JOIN LATERAL jsonb_array_elements(enrich_data.enriched::jsonb) AS enrich_elem
|
|
290
|
-
LEFT JOIN LATERAL (
|
|
291
|
-
SELECT row_to_json(nested_table) AS ${nestedJoin.as}
|
|
292
|
-
FROM "${nestedJoin.from}" AS nested_table
|
|
293
|
-
WHERE nested_table."${nestedJoin.foreignField}" = (enrich_elem.value->>'${nestedField}')::integer
|
|
294
|
-
AND nested_table."_deleted" IS NULL
|
|
295
|
-
LIMIT 1
|
|
296
|
-
) AS nested_join_data ON true
|
|
297
|
-
) AS ${enrichAlias} ON true`;
|
|
298
|
-
}
|
|
299
|
-
else {
|
|
300
|
-
enrichmentJoins += ` LEFT JOIN LATERAL (
|
|
301
|
-
SELECT COALESCE(JSON_AGG(row_to_json(${enrichAlias}_table)), '[]'::json) AS enriched
|
|
302
|
-
FROM "${enrichOp.through}"
|
|
303
|
-
INNER JOIN "${enrichOp.from}" AS ${enrichAlias}_table
|
|
304
|
-
ON ${enrichAlias}_table."${enrichOp.foreignField}" = "${enrichOp.through}"."${enrichOp.throughForeignField}"
|
|
305
|
-
WHERE "${enrichOp.through}"."${enrichOp.throughLocalField}" = (elem.value->>'${enrichField}')::integer
|
|
306
|
-
AND "${enrichOp.through}"."_deleted" IS NULL
|
|
307
|
-
AND ${enrichAlias}_table."_deleted" IS NULL
|
|
308
|
-
) AS ${enrichAlias} ON true`;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
jsonBuildObjects += ` || jsonb_build_object('${enrichOp.as}', COALESCE(${enrichAlias}.enriched, '[]'::json))`;
|
|
312
|
-
}
|
|
313
|
-
if (isTargetJoinMany) {
|
|
314
|
-
joinClauses += ` LEFT JOIN LATERAL (
|
|
315
|
-
SELECT COALESCE(
|
|
316
|
-
JSON_AGG(
|
|
317
|
-
elem.value${jsonBuildObjects}
|
|
318
|
-
),
|
|
319
|
-
'[]'::json
|
|
320
|
-
) AS aggregated
|
|
321
|
-
FROM (
|
|
322
|
-
SELECT COALESCE(JSON_AGG(row_to_json(target_table)), '[]'::json) AS aggregated
|
|
323
|
-
FROM "${target.from}" AS target_table
|
|
324
|
-
WHERE target_table."${target.foreignField}" = ${targetLocalFieldRef}
|
|
325
|
-
AND target_table."_deleted" IS NULL
|
|
326
|
-
) AS original_data
|
|
327
|
-
CROSS JOIN LATERAL jsonb_array_elements(original_data.aggregated::jsonb) AS elem${enrichmentJoins}
|
|
328
|
-
) AS ${target.as} ON true`;
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
const targetThrough = target;
|
|
332
|
-
joinClauses += ` LEFT JOIN LATERAL (
|
|
333
|
-
SELECT COALESCE(
|
|
334
|
-
JSON_AGG(
|
|
335
|
-
elem.value${jsonBuildObjects}
|
|
336
|
-
),
|
|
337
|
-
'[]'::json
|
|
338
|
-
) AS aggregated
|
|
339
|
-
FROM (
|
|
340
|
-
SELECT COALESCE(JSON_AGG(row_to_json(target_table)), '[]'::json) AS aggregated
|
|
341
|
-
FROM "${targetThrough.through}"
|
|
342
|
-
INNER JOIN "${targetThrough.from}" AS target_table
|
|
343
|
-
ON target_table."${targetThrough.foreignField}" = "${targetThrough.through}"."${targetThrough.throughForeignField}"
|
|
344
|
-
WHERE "${targetThrough.through}"."${targetThrough.throughLocalField}" = ${targetLocalFieldRef}
|
|
345
|
-
AND "${targetThrough.through}"."_deleted" IS NULL
|
|
346
|
-
AND target_table."_deleted" IS NULL
|
|
347
|
-
) AS original_data
|
|
348
|
-
CROSS JOIN LATERAL jsonb_array_elements(original_data.aggregated::jsonb) AS elem${enrichmentJoins}
|
|
349
|
-
) AS ${target.as} ON true`;
|
|
350
|
-
}
|
|
351
|
-
enrichedAliases.add(target.as);
|
|
352
|
-
processedAliases.add(target.as);
|
|
353
|
-
for (const enrich of enrichments) {
|
|
354
|
-
processedAliases.add(enrich.operation.as);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
23
|
+
if (operation instanceof LeftJoin || operation instanceof InnerJoin || operation instanceof LeftJoinMany) {
|
|
24
|
+
const localRef = resolveLocalField(operation.localField, mainTableName);
|
|
25
|
+
const foreignSnake = convertFieldToSnakeCase(operation.foreignField);
|
|
26
|
+
const joinType = operation instanceof InnerJoin ? "INNER JOIN" : "LEFT JOIN";
|
|
27
|
+
if (operation instanceof LeftJoinMany) {
|
|
28
|
+
joinClause += ` ${joinType} (
|
|
29
|
+
SELECT "${foreignSnake}", json_agg(row_to_json(_many.*)) AS aggregated
|
|
30
|
+
FROM "${operation.from}" _many
|
|
31
|
+
GROUP BY "${foreignSnake}"
|
|
32
|
+
) AS ${operation.as} ON ${localRef} = ${operation.as}."${foreignSnake}"`;
|
|
357
33
|
}
|
|
358
34
|
else {
|
|
359
|
-
|
|
360
|
-
joinClauses += ` LEFT JOIN LATERAL (
|
|
361
|
-
SELECT COALESCE(JSON_AGG(row_to_json(${operation.as})), '[]'::json) AS aggregated
|
|
362
|
-
FROM "${operation.through}"
|
|
363
|
-
INNER JOIN "${operation.from}" AS ${operation.as}
|
|
364
|
-
ON ${operation.as}."${operation.foreignField}" = "${operation.through}"."${operation.throughForeignField}"
|
|
365
|
-
WHERE "${operation.through}"."${operation.throughLocalField}" = ${localFieldRef}
|
|
366
|
-
AND "${operation.through}"."_deleted" IS NULL
|
|
367
|
-
AND ${operation.as}."_deleted" IS NULL
|
|
368
|
-
) AS ${operation.as} ON true`;
|
|
369
|
-
processedAliases.add(operation.as);
|
|
35
|
+
joinClause += ` ${joinType} "${operation.from}" AS ${operation.as} ON ${localRef} = ${operation.as}."${foreignSnake}"`;
|
|
370
36
|
}
|
|
371
37
|
}
|
|
372
38
|
}
|
|
373
|
-
return
|
|
39
|
+
return joinClause;
|
|
374
40
|
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { JoinThroughMany } from '../../operations/join-through-many.operation.js';
|
|
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';
|
|
5
4
|
async function getTableColumns(client, tableName) {
|
|
6
|
-
const result = await client.query(`
|
|
7
|
-
SELECT column_name
|
|
8
|
-
FROM information_schema.columns
|
|
9
|
-
WHERE table_schema = current_schema()
|
|
10
|
-
AND table_name = $1
|
|
11
|
-
ORDER BY ordinal_position
|
|
5
|
+
const result = await client.query(`
|
|
6
|
+
SELECT column_name
|
|
7
|
+
FROM information_schema.columns
|
|
8
|
+
WHERE table_schema = current_schema()
|
|
9
|
+
AND table_name = $1
|
|
10
|
+
ORDER BY ordinal_position
|
|
12
11
|
`, [tableName]);
|
|
13
12
|
return result.rows.map(row => row.column_name);
|
|
14
13
|
}
|
|
@@ -17,43 +16,32 @@ function findEnrichmentTarget(operation, operations) {
|
|
|
17
16
|
return null;
|
|
18
17
|
}
|
|
19
18
|
const [alias] = operation.localField.split('.');
|
|
20
|
-
const target = operations.find(op =>
|
|
19
|
+
const target = operations.find(op => op instanceof LeftJoinMany && op.as === alias);
|
|
21
20
|
if (target && operations.indexOf(target) < operations.indexOf(operation)) {
|
|
22
21
|
return { target, field: operation.localField.split('.')[1] };
|
|
23
22
|
}
|
|
24
23
|
return null;
|
|
25
24
|
}
|
|
26
25
|
export async function buildSelectClause(client, mainTableName, mainTableAlias, operations) {
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const joinThroughManyOperations = operations.filter(op => op instanceof JoinThroughMany);
|
|
26
|
+
const leftJoinOperations = operations.filter(op => op instanceof LeftJoin);
|
|
27
|
+
const innerJoinOperations = operations.filter(op => op instanceof InnerJoin);
|
|
28
|
+
const leftJoinManyOperations = operations.filter(op => op instanceof LeftJoinMany);
|
|
31
29
|
const mainTableColumns = await getTableColumns(client, mainTableName);
|
|
32
30
|
const mainSelects = mainTableColumns.map(col => `"${mainTableName}"."${col}" AS "${col}"`);
|
|
33
31
|
const joinSelects = [];
|
|
34
|
-
for (const join of
|
|
32
|
+
for (const join of [...leftJoinOperations, ...innerJoinOperations]) {
|
|
35
33
|
const joinColumns = await getTableColumns(client, join.from);
|
|
36
34
|
for (const col of joinColumns) {
|
|
37
35
|
joinSelects.push(`${join.as}."${col}" AS "${join.as}__${col}"`);
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
|
-
for (const
|
|
41
|
-
joinSelects.push(`${joinThrough.as}.aggregated AS "${joinThrough.as}"`);
|
|
42
|
-
}
|
|
43
|
-
for (const joinMany of joinManyOperations) {
|
|
38
|
+
for (const joinMany of leftJoinManyOperations) {
|
|
44
39
|
const enrichment = findEnrichmentTarget(joinMany, operations);
|
|
45
40
|
if (enrichment) {
|
|
46
41
|
continue;
|
|
47
42
|
}
|
|
48
43
|
joinSelects.push(`${joinMany.as}.aggregated AS "${joinMany.as}"`);
|
|
49
44
|
}
|
|
50
|
-
for (const joinThroughMany of joinThroughManyOperations) {
|
|
51
|
-
const enrichment = findEnrichmentTarget(joinThroughMany, operations);
|
|
52
|
-
if (enrichment) {
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
joinSelects.push(`${joinThroughMany.as}.aggregated AS "${joinThroughMany.as}"`);
|
|
56
|
-
}
|
|
57
45
|
const allSelects = [...mainSelects, ...joinSelects];
|
|
58
46
|
return allSelects.join(', ');
|
|
59
47
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export async function doesTableExist(client, tableName) {
|
|
2
|
-
const result = await client.query(`
|
|
3
|
-
SELECT EXISTS (
|
|
4
|
-
SELECT 1 FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = $1
|
|
5
|
-
)
|
|
2
|
+
const result = await client.query(`
|
|
3
|
+
SELECT EXISTS (
|
|
4
|
+
SELECT 1 FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = $1
|
|
5
|
+
)
|
|
6
6
|
`, [tableName]);
|
|
7
7
|
return result.rows[0].exists;
|
|
8
8
|
}
|