fastify-auto-sqlapi 0.1.14 → 0.2.1
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/AGENTS_BACKEND.md +52 -7
- package/BREAKING_CHANGES.md +86 -5
- package/CHANGELOG.md +57 -0
- package/README.md +110 -3
- package/dist/bin/generate-schema.js +89 -77
- package/dist/bin/generate-tables.js +72 -62
- package/dist/bin/utils.d.ts.map +1 -1
- package/dist/bin/utils.js +47 -33
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/agent/manifest.d.ts +3 -2
- package/dist/lib/agent/manifest.d.ts.map +1 -1
- package/dist/lib/agent/manifest.js +102 -80
- package/dist/lib/cli/mysql-introspect.d.ts +4 -9
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
- package/dist/lib/cli/mysql-introspect.js +8 -7
- package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
- package/dist/lib/cli/pg-introspect.js +0 -1
- package/dist/lib/cli/schema-codegen.d.ts +3 -0
- package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
- package/dist/lib/cli/schema-codegen.js +3 -0
- package/dist/lib/cli/tables-codegen.d.ts +8 -1
- package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
- package/dist/lib/cli/tables-codegen.js +100 -118
- package/dist/lib/condition-methods.d.ts +14 -4
- package/dist/lib/condition-methods.d.ts.map +1 -1
- package/dist/lib/condition-methods.js +18 -4
- package/dist/lib/db.d.ts +7 -2
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +24 -6
- package/dist/lib/dialect.d.ts +2 -1
- package/dist/lib/dialect.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.js +44 -36
- package/dist/lib/engine/rest/delete.d.ts.map +1 -1
- package/dist/lib/engine/rest/get.d.ts.map +1 -1
- package/dist/lib/engine/rest/get.js +5 -4
- package/dist/lib/engine/rest/insert.d.ts.map +1 -1
- package/dist/lib/engine/rest/insert.js +1 -1
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +26 -19
- package/dist/lib/engine/search/aggregations.d.ts +33 -0
- package/dist/lib/engine/search/aggregations.d.ts.map +1 -0
- package/dist/lib/engine/search/aggregations.js +109 -0
- package/dist/lib/engine/search/conditions.d.ts +31 -0
- package/dist/lib/engine/search/conditions.d.ts.map +1 -0
- package/dist/lib/engine/search/conditions.js +131 -0
- package/dist/lib/engine/search/fields.d.ts +49 -0
- package/dist/lib/engine/search/fields.d.ts.map +1 -0
- package/dist/lib/engine/search/fields.js +142 -0
- package/dist/lib/engine/search/join-families.d.ts +35 -0
- package/dist/lib/engine/search/join-families.d.ts.map +1 -0
- package/dist/lib/engine/search/join-families.js +345 -0
- package/dist/lib/engine/search/joins.d.ts +55 -0
- package/dist/lib/engine/search/joins.d.ts.map +1 -0
- package/dist/lib/engine/search/joins.js +81 -0
- package/dist/lib/engine/search/order-by.d.ts +35 -0
- package/dist/lib/engine/search/order-by.d.ts.map +1 -0
- package/dist/lib/engine/search/order-by.js +104 -0
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +106 -791
- package/dist/lib/engine/validate.d.ts +0 -5
- package/dist/lib/engine/validate.d.ts.map +1 -1
- package/dist/lib/engine/write-helpers.d.ts +25 -17
- package/dist/lib/engine/write-helpers.d.ts.map +1 -1
- package/dist/lib/engine/write-helpers.js +86 -44
- package/dist/lib/errors.d.ts +24 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +59 -0
- package/dist/lib/read-access.d.ts +12 -1
- package/dist/lib/read-access.d.ts.map +1 -1
- package/dist/lib/read-access.js +20 -4
- package/dist/lib/schema/helpers.d.ts +0 -15
- package/dist/lib/schema/helpers.d.ts.map +1 -1
- package/dist/lib/schema/helpers.js +7 -4
- package/dist/lib/schema/search.d.ts.map +1 -1
- package/dist/lib/schema/search.js +107 -128
- package/dist/lib/sql-api.d.ts.map +1 -1
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +48 -0
- package/dist/lib/tenant.d.ts +66 -5
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/lib/tenant.js +209 -48
- package/dist/routes/auto/plugin.d.ts.map +1 -1
- package/dist/routes/auto/plugin.js +6 -0
- package/dist/routes/auto/route-helpers.d.ts +0 -7
- package/dist/routes/auto/route-helpers.d.ts.map +1 -1
- package/dist/routes/auto/route-helpers.js +58 -33
- package/dist/routes/auto/search.routes.js +0 -1
- package/dist/types/plugin.d.ts +19 -0
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/table.d.ts +1 -1
- package/dist/types/tenant.d.ts +26 -2
- package/dist/types/tenant.d.ts.map +1 -1
- package/docs/GLOSSARY.md +30 -0
- package/docs/adr/0006-raw-db-errors.md +2 -1
- package/docs/adr/0010-joins-do-not-run-route-guards.md +4 -1
- package/docs/adr/0012-anti-duplication-tooling.md +59 -0
- package/docs/adr/0013-sanitized-db-errors.md +114 -0
- package/docs/adr/README.md +4 -1
- package/package.json +29 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"condition-methods.d.ts","sourceRoot":"","sources":["../../src/lib/condition-methods.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"condition-methods.d.ts","sourceRoot":"","sources":["../../src/lib/condition-methods.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,eAAe,+RAElB,CAAC;AAGX,eAAO,MAAM,gBAAgB,aAAwC,CAAC;AACtE,eAAO,MAAM,WAAW,aAAmC,CAAC;AAC5D,eAAO,MAAM,MAAM,aAA8B,CAAC;AAClD,eAAO,MAAM,QAAQ,aAAgC,CAAC;AACtD,eAAO,MAAM,WAAW,aAAmC,CAAC;AAE5D;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC,yFAAyF;AACzF,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,+FAA+F;AAC/F,eAAO,MAAM,mBAAmB,OAAO,CAAC"}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// Allowed ConditionBuilder methods for the `conditions` search API.
|
|
2
2
|
// Single source of truth for both schema validation (Swagger) and runtime whitelist.
|
|
3
|
-
|
|
3
|
+
const SINGLE_VALUE_METHODS = [
|
|
4
4
|
'isEqual', 'isNotEqual',
|
|
5
5
|
'isGreater', 'isNotGreater', 'isGreaterOrEqual', 'isNotGreaterOrEqual',
|
|
6
6
|
'isLess', 'isNotLess', 'isLessOrEqual', 'isNotLessOrEqual',
|
|
7
7
|
'isLike', 'isNotLike', 'isILike', 'isNotILike',
|
|
8
8
|
];
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const BETWEEN_METHODS = ['isBetween', 'isNotBetween'];
|
|
10
|
+
const IN_METHODS = ['isIn', 'isNotIn'];
|
|
11
|
+
const NULL_METHODS = ['isNull', 'isNotNull'];
|
|
12
12
|
export const ALLOWED_METHODS = [
|
|
13
13
|
...SINGLE_VALUE_METHODS, ...BETWEEN_METHODS, ...IN_METHODS, ...NULL_METHODS,
|
|
14
14
|
];
|
|
@@ -18,3 +18,17 @@ export const BETWEEN_SET = new Set(BETWEEN_METHODS);
|
|
|
18
18
|
export const IN_SET = new Set(IN_METHODS);
|
|
19
19
|
export const NULL_SET = new Set(NULL_METHODS);
|
|
20
20
|
export const ALLOWED_SET = new Set(ALLOWED_METHODS);
|
|
21
|
+
/**
|
|
22
|
+
* Caps on how much work one search request may ask for.
|
|
23
|
+
*
|
|
24
|
+
* Every dotted condition and every 3-part `orderBy` token becomes its own correlated subquery,
|
|
25
|
+
* so an uncapped list turns a single request into arbitrarily many — `maxItemsPerPage` bounds
|
|
26
|
+
* the rows returned, not the work done to find them. They live here, in the data module both
|
|
27
|
+
* the engine and the TypeBox schema builders already share, so the request schema advertises
|
|
28
|
+
* exactly the limit the engine enforces.
|
|
29
|
+
*/
|
|
30
|
+
export const MAX_CONDITIONS = 100;
|
|
31
|
+
/** Token cap the engine applies to `orderBy`, counted over its comma-separated parts. */
|
|
32
|
+
export const MAX_ORDER_BY_PARTS = 20;
|
|
33
|
+
/** Character cap the request schema applies to `orderBy` — generous for MAX_ORDER_BY_PARTS. */
|
|
34
|
+
export const MAX_ORDER_BY_LENGTH = 1024;
|
package/dist/lib/db.d.ts
CHANGED
|
@@ -37,9 +37,14 @@ export declare class QueryClient {
|
|
|
37
37
|
insert(table: string, record: DbRecord, pkCol: string | string[]): Promise<Record<string, unknown>>;
|
|
38
38
|
insertOrUpdate(table: string, record: DbRecord, conflictKeys: string[], pkCol: string | string[]): Promise<Record<string, unknown>>;
|
|
39
39
|
bulkInsert(table: string, records: DbRecord[], pkCol: string | string[], chunkSize?: number): Promise<Record<string, unknown>[]>;
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* `immutableCols` are written when the row is inserted but left untouched when the statement
|
|
42
|
+
* lands on an existing row — like the conflict keys, which are never in the update set either.
|
|
43
|
+
* Used by tenant isolation to keep an upsert from re-assigning a row's owner.
|
|
44
|
+
*/
|
|
45
|
+
bulkInsertOrUpdate(table: string, records: DbRecord[], conflictKeys: string[], pkCol: string | string[], chunkSize?: number, immutableCols?: string[]): Promise<Record<string, unknown>[]>;
|
|
41
46
|
update(table: string, record: DbRecord, where: DbRecord, extraCondition?: ConditionBuilder): Promise<number>;
|
|
42
|
-
delete(table: string, where: DbRecord): Promise<number>;
|
|
47
|
+
delete(table: string, where: DbRecord, extraCondition?: ConditionBuilder): Promise<number>;
|
|
43
48
|
select<T = Record<string, unknown>>({ tableName, columns, where, values, limit, orderBy, joins, distinct, }: SelectOptions): Promise<T[]>;
|
|
44
49
|
}
|
|
45
50
|
export declare function createQueryClient(pool: Queryable, dialectName?: string): QueryClient;
|
package/dist/lib/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAQ/C,sCAAsC;AACtC,eAAO,MAAM,WAAW,GAAI,GAAG,MAAM,WAA0B,CAAC;AAEhE,qBAAa,WAAW;;IACtB,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,KAAK,CAAS;gBAEV,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,UAAU;IAKnD,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAE/B;IAED,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAE9B;IAED,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,iFAAiF;IACjF,IAAI,SAAS,IAAI,UAAU,CAAC,WAAW,CAAC,CAEvC;IAED,mFAAmF;IACnF,IAAI,iBAAiB,IAAI,OAAO,CAE/B;IAED,uEAAuE;IACvE,IAAI,WAAW,IAAI,UAAU,CAAC,aAAa,CAAC,CAE3C;IAED,IAAI,SAAS,IAAI,UAAU,CAAC,WAAW,CAAC,CAEvC;IAED,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAI7B,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU;IAIrC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAQxB;;;;;;;;;OASG;IACG,eAAe,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAmHnE,MAAM,CACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,QAAQ,EAChB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAgB7B,cAAc,CAClB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,QAAQ,EAChB,YAAY,EAAE,MAAM,EAAE,EACtB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAkB7B,UAAU,CACd,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,QAAQ,EAAE,EACnB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,SAAS,SAAqB,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/lib/db.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/db.js
CHANGED
|
@@ -195,11 +195,17 @@ export class QueryClient {
|
|
|
195
195
|
}
|
|
196
196
|
return results;
|
|
197
197
|
}
|
|
198
|
-
|
|
198
|
+
/**
|
|
199
|
+
* `immutableCols` are written when the row is inserted but left untouched when the statement
|
|
200
|
+
* lands on an existing row — like the conflict keys, which are never in the update set either.
|
|
201
|
+
* Used by tenant isolation to keep an upsert from re-assigning a row's owner.
|
|
202
|
+
*/
|
|
203
|
+
async bulkInsertOrUpdate(table, records, conflictKeys, pkCol, chunkSize = DEFAULT_CHUNK_SIZE, immutableCols = []) {
|
|
199
204
|
if (!records.length)
|
|
200
205
|
return [];
|
|
201
206
|
const fields = Object.keys(records[0]);
|
|
202
|
-
const
|
|
207
|
+
const keepAsIs = immutableCols.length ? [...conflictKeys, ...immutableCols] : conflictKeys;
|
|
208
|
+
const upsertClause = this.dialect.upsertSql(this.#q(conflictKeys), this.dialect.upsertUpdateSet(fields, keepAsIs));
|
|
203
209
|
const returning = this.dialect.returningPk(this.#quotedPkCols(pkCol));
|
|
204
210
|
const results = [];
|
|
205
211
|
for (let i = 0; i < records.length; i += chunkSize) {
|
|
@@ -221,16 +227,28 @@ export class QueryClient {
|
|
|
221
227
|
extraWhere = ` AND ${extraCondition.build(values.length + 1, this.dialect.ph)}`;
|
|
222
228
|
values.push(...extraCondition.getValues());
|
|
223
229
|
}
|
|
230
|
+
const setClause = setF.map((f, i) => `${this.dialect.qi(f)} = ${setP[i]}`).join(', ');
|
|
231
|
+
const whereClause = whereF
|
|
232
|
+
.map((f, i) => `${this.dialect.qi(f)} = ${whereP[i]}`)
|
|
233
|
+
.join(' AND ');
|
|
224
234
|
const result = await this.query(`UPDATE ${this.dialect.qi(table)}
|
|
225
|
-
SET ${
|
|
226
|
-
WHERE ${
|
|
235
|
+
SET ${setClause}
|
|
236
|
+
WHERE ${whereClause}${extraWhere}`, values);
|
|
227
237
|
return result.affectedRows;
|
|
228
238
|
}
|
|
229
|
-
async delete(table, where) {
|
|
239
|
+
async delete(table, where, extraCondition) {
|
|
230
240
|
const values = [];
|
|
231
241
|
const { fields, placeholders } = this.#params(where, values);
|
|
242
|
+
let extraWhere = '';
|
|
243
|
+
if (extraCondition) {
|
|
244
|
+
extraWhere = ` AND ${extraCondition.build(values.length + 1, this.dialect.ph)}`;
|
|
245
|
+
values.push(...extraCondition.getValues());
|
|
246
|
+
}
|
|
247
|
+
const whereClause = fields
|
|
248
|
+
.map((f, i) => `${this.dialect.qi(f)} = ${placeholders[i]}`)
|
|
249
|
+
.join(' AND ');
|
|
232
250
|
const result = await this.query(`DELETE FROM ${this.dialect.qi(table)}
|
|
233
|
-
WHERE ${
|
|
251
|
+
WHERE ${whereClause}${extraWhere}`, values);
|
|
234
252
|
return result.affectedRows;
|
|
235
253
|
}
|
|
236
254
|
async select({ tableName, columns = '*', where, values, limit = null, orderBy = '', joins = [], distinct = false, }) {
|
package/dist/lib/dialect.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type DialectName = 'postgres' | 'mysql' | 'mariadb';
|
|
2
|
-
|
|
2
|
+
type TruncateUnit = 'year' | 'quarter' | 'month' | 'day' | 'hour';
|
|
3
3
|
export interface SqlDialect {
|
|
4
4
|
name: DialectName;
|
|
5
5
|
/** Quote identifier: "id" (postgres) or `id` (mysql/mariadb) */
|
|
@@ -31,4 +31,5 @@ export interface SqlDialect {
|
|
|
31
31
|
dateTrunc(unit: TruncateUnit, qualifiedCol: string): string;
|
|
32
32
|
}
|
|
33
33
|
export declare function getDialect(name: DialectName): SqlDialect;
|
|
34
|
+
export {};
|
|
34
35
|
//# sourceMappingURL=dialect.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dialect.d.ts","sourceRoot":"","sources":["../../src/lib/dialect.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;AAE3D,
|
|
1
|
+
{"version":3,"file":"dialect.d.ts","sourceRoot":"","sources":["../../src/lib/dialect.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;AAE3D,KAAK,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAElE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,gEAAgE;IAChE,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,sDAAsD;IACtD,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,oCAAoC;IACpC,SAAS,EAAE,UAAU,GAAG,OAAO,CAAC;IAChC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,4CAA4C;IAC5C,SAAS,CAAC,kBAAkB,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,CAAC;IACvE,8CAA8C;IAC9C,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC;IACzC;;;;;;OAMG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAClE;;;;;;OAMG;IACH,SAAS,CAAC,IAAI,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC;CAC7D;AAqFD,wBAAgB,UAAU,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,CAIxD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-upsert.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-upsert.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAEV,gBAAgB,EAChB,gBAAgB,EAEjB,MAAM,mBAAmB,CAAC;AA+D3B,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA0C5F"}
|
|
@@ -1,10 +1,48 @@
|
|
|
1
1
|
import { camelcaseObject } from '../../naming.js';
|
|
2
2
|
import { processSecondaries, processDeletions, prepareInsertRecord } from '../write-helpers.js';
|
|
3
|
-
import { enforceTenantOnWrites, assertTenantOwnsConflicts } from '../../tenant.js';
|
|
3
|
+
import { enforceTenantOnWrites, assertTenantOwnsConflicts, tenantImmutableColumns, } from '../../tenant.js';
|
|
4
4
|
import { runBulkValidation } from '../validate.js';
|
|
5
5
|
import { primaryAsCols } from '../../../types.js';
|
|
6
|
+
/** One bulk statement for every main row, returning the PKs only. */
|
|
7
|
+
function upsertMains(params, preparedMains, pkCol) {
|
|
8
|
+
const { db, tableConf, tenant } = params;
|
|
9
|
+
const schema = tableConf.Schema;
|
|
10
|
+
const upsertKeys = tableConf.upsertMap?.get(schema);
|
|
11
|
+
if (!upsertKeys) {
|
|
12
|
+
return db.bulkInsert(schema.tableName, preparedMains, pkCol);
|
|
13
|
+
}
|
|
14
|
+
const conflictCols = upsertKeys.map((k) => schema.col(k));
|
|
15
|
+
// Tenant isolation: a conflicting upsert must not overwrite rows owned by another tenant, nor
|
|
16
|
+
// re-assign the owner of a row it legitimately updates.
|
|
17
|
+
return assertTenantOwnsConflicts(db, tenant, schema.tableName, conflictCols, preparedMains)
|
|
18
|
+
.then(() => db.bulkInsertOrUpdate(schema.tableName, preparedMains, conflictCols, pkCol, undefined, tenantImmutableColumns(tenant)));
|
|
19
|
+
}
|
|
20
|
+
/** Secondaries, deletions and the afterInsert hook for one upserted item. */
|
|
21
|
+
async function finalizeUpsertedItem(params, item, inputMain, pkRow) {
|
|
22
|
+
const { db, tableConf, dbTables, request, tenant } = params;
|
|
23
|
+
// pkRow comes from SQL RETURNING or mysql insertId synthesis — keys are DB column names;
|
|
24
|
+
// camelcaseObject maps them back to schema fields (full composite PK).
|
|
25
|
+
const mainPkCamel = camelcaseObject(pkRow, tableConf.Schema);
|
|
26
|
+
const result = { main: mainPkCamel };
|
|
27
|
+
// FK auto-fill: input camelCase + generated PK
|
|
28
|
+
const mainForFK = { ...inputMain, ...mainPkCamel };
|
|
29
|
+
if (item.secondaries && Object.keys(item.secondaries).length > 0) {
|
|
30
|
+
const sec = await processSecondaries({ db, tableConf, dbTables, mainRecord: mainForFK, tenant }, item.secondaries);
|
|
31
|
+
if (Object.keys(sec).length > 0)
|
|
32
|
+
result.secondaries = sec;
|
|
33
|
+
}
|
|
34
|
+
if (item.deletions && Object.keys(item.deletions).length > 0) {
|
|
35
|
+
const del = await processDeletions({ db, tableConf, dbTables, mainRecord: mainForFK, tenant }, item.deletions);
|
|
36
|
+
if (Object.keys(del).length > 0)
|
|
37
|
+
result.deletions = del;
|
|
38
|
+
}
|
|
39
|
+
if (tableConf.afterInsert) {
|
|
40
|
+
await tableConf.afterInsert(db, request, mainForFK, result.secondaries);
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
6
44
|
export async function bulkUpsertEngine(params) {
|
|
7
|
-
const { db, tableConf,
|
|
45
|
+
const { db, tableConf, request, items, tenant } = params;
|
|
8
46
|
if (!items.length)
|
|
9
47
|
return [];
|
|
10
48
|
const schema = tableConf.Schema;
|
|
@@ -22,42 +60,12 @@ export async function bulkUpsertEngine(params) {
|
|
|
22
60
|
// 3. Tenant: inject (direct) or batch-validate FK (indirect) — enforced after user mutations
|
|
23
61
|
await enforceTenantOnWrites(db, tenant, preparedMains);
|
|
24
62
|
// 4. Bulk upsert all mains in one query → returns PK-only
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const conflictCols = upsertKeys.map((k) => schema.col(k));
|
|
29
|
-
// Tenant isolation: a conflicting upsert must not overwrite rows owned by another tenant.
|
|
30
|
-
await assertTenantOwnsConflicts(db, tenant, schema.tableName, conflictCols, preparedMains);
|
|
31
|
-
pkRows = await db.bulkInsertOrUpdate(schema.tableName, preparedMains, conflictCols, pkCol);
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
pkRows = await db.bulkInsert(schema.tableName, preparedMains, pkCol);
|
|
35
|
-
}
|
|
36
|
-
// 5. Process secondaries, deletions, afterInsert per item (all camelCase)
|
|
63
|
+
const pkRows = await upsertMains(params, preparedMains, pkCol);
|
|
64
|
+
// 5-6. Secondaries, deletions and afterInsert per item — sequential on purpose, so hooks
|
|
65
|
+
// observe the same order the caller sent, and errors surface on the first bad item.
|
|
37
66
|
const results = [];
|
|
38
67
|
for (let i = 0; i < items.length; i++) {
|
|
39
|
-
|
|
40
|
-
// pkRows entries come from SQL RETURNING or mysql insertId synthesis — keys are DB
|
|
41
|
-
// column names; camelcaseObject maps them back to schema fields (full composite PK).
|
|
42
|
-
const mainPkCamel = camelcaseObject(pkRows[i], schema);
|
|
43
|
-
const result = { main: mainPkCamel };
|
|
44
|
-
// FK auto-fill: input camelCase + generated PK
|
|
45
|
-
const mainForFK = { ...inputMains[i], ...mainPkCamel };
|
|
46
|
-
if (item.secondaries && Object.keys(item.secondaries).length > 0) {
|
|
47
|
-
const sec = await processSecondaries(db, tableConf, dbTables, mainForFK, item.secondaries);
|
|
48
|
-
if (Object.keys(sec).length > 0)
|
|
49
|
-
result.secondaries = sec;
|
|
50
|
-
}
|
|
51
|
-
if (item.deletions && Object.keys(item.deletions).length > 0) {
|
|
52
|
-
const del = await processDeletions(db, tableConf, mainForFK, item.deletions);
|
|
53
|
-
if (Object.keys(del).length > 0)
|
|
54
|
-
result.deletions = del;
|
|
55
|
-
}
|
|
56
|
-
// 6. afterInsert hook per item (camelCase)
|
|
57
|
-
if (tableConf.afterInsert) {
|
|
58
|
-
await tableConf.afterInsert(db, request, mainForFK, result.secondaries);
|
|
59
|
-
}
|
|
60
|
-
results.push(result);
|
|
68
|
+
results.push(await finalizeUpsertedItem(params, items[i], inputMains[i], pkRows[i]));
|
|
61
69
|
}
|
|
62
70
|
return results;
|
|
63
71
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/delete.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/delete.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEpE,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAuC9E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/get.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/get.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE9D,wBAAsB,SAAS,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAuCrE"}
|
|
@@ -15,10 +15,11 @@ export async function getEngine(params) {
|
|
|
15
15
|
// shared with the through table would be ambiguous.
|
|
16
16
|
cb.isEqual(`${db.qi(tableConf.Schema.tableName)}.${db.qi(pkCol)}`, id);
|
|
17
17
|
const joins = [];
|
|
18
|
-
if (tenant) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
if (tenant?.scope) {
|
|
19
|
+
const scope = tenant.scope;
|
|
20
|
+
cb.append(buildTenantCondition(db, scope, tenant.ids, tableConf.Schema.tableName));
|
|
21
|
+
if ('through' in scope) {
|
|
22
|
+
joins.push(buildTenantJoin(db, scope, tableConf.Schema.tableName));
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
const where = cb.build(1, db.ph);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/insert.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/insert.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAmE9E"}
|
|
@@ -32,7 +32,7 @@ export async function insertEngine(params) {
|
|
|
32
32
|
let secondaryResults;
|
|
33
33
|
if (secondaries && Object.keys(secondaries).length > 0) {
|
|
34
34
|
const mainForFK = { ...inputRecord, ...mainPkCamel };
|
|
35
|
-
secondaryResults = await processSecondaries(tx, tableConf, dbTables, mainForFK, secondaries);
|
|
35
|
+
secondaryResults = await processSecondaries({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant }, secondaries);
|
|
36
36
|
}
|
|
37
37
|
// 5. afterInsert hook (camelCase — input merged with generated PK)
|
|
38
38
|
if (tableConf.afterInsert) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAiC3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA2G9E"}
|
|
@@ -3,6 +3,28 @@ import { processSecondaries, processDeletions } from '../write-helpers.js';
|
|
|
3
3
|
import { stripTenantColumn, assertTenantOwnership, buildTenantUpdateExtra, enforceTenantFKOnUpdate, } from '../../tenant.js';
|
|
4
4
|
import { runValidation } from '../validate.js';
|
|
5
5
|
import { httpError } from '../../errors.js';
|
|
6
|
+
/**
|
|
7
|
+
* Confirm the row exists — and is visible to this tenant — when the request carries no
|
|
8
|
+
* updatable fields. Secondaries and deletions still need the main row to be there, and a
|
|
9
|
+
* missing one must be a 404 rather than a silent success.
|
|
10
|
+
*/
|
|
11
|
+
async function assertRecordExists(tx, tableName, pkCols, pkValues, extraCondition) {
|
|
12
|
+
const whereParts = pkCols.map((c, i) => `${tx.qi(tableName)}.${tx.qi(c)} = ${tx.ph(i + 1)}`);
|
|
13
|
+
let whereSql = whereParts.join(' AND ');
|
|
14
|
+
const whereValues = [...pkValues];
|
|
15
|
+
if (extraCondition) {
|
|
16
|
+
whereSql += ` AND ${extraCondition.build(pkCols.length + 1, tx.ph)}`;
|
|
17
|
+
whereValues.push(...extraCondition.getValues());
|
|
18
|
+
}
|
|
19
|
+
const rows = await tx.select({
|
|
20
|
+
tableName,
|
|
21
|
+
where: whereSql,
|
|
22
|
+
values: whereValues,
|
|
23
|
+
limit: '1',
|
|
24
|
+
});
|
|
25
|
+
if (rows.length === 0)
|
|
26
|
+
throw httpError(404, `Record not found`);
|
|
27
|
+
}
|
|
6
28
|
export async function updateEngine(params) {
|
|
7
29
|
const { db, tableConf, dbTables, request, record, secondaries, deletions, tenant } = params;
|
|
8
30
|
const schema = tableConf.Schema;
|
|
@@ -32,7 +54,7 @@ export async function updateEngine(params) {
|
|
|
32
54
|
// 6. Strip tenant column (user cannot change tenant of an existing record).
|
|
33
55
|
// For indirect scopes the tenant link is the through-FK (localField): it may be changed,
|
|
34
56
|
// but only to another value the caller owns — re-validate the new FK against the tenant.
|
|
35
|
-
if (tenant) {
|
|
57
|
+
if (tenant?.scope) {
|
|
36
58
|
stripTenantColumn(updateFields, tenant.scope);
|
|
37
59
|
await enforceTenantFKOnUpdate(db, tenant, updateFields);
|
|
38
60
|
}
|
|
@@ -50,22 +72,7 @@ export async function updateEngine(params) {
|
|
|
50
72
|
throw httpError(404, `Record not found`);
|
|
51
73
|
}
|
|
52
74
|
else {
|
|
53
|
-
|
|
54
|
-
const whereParts = pkCols.map((c, i) => `${tx.qi(schema.tableName)}.${tx.qi(c)} = ${tx.ph(i + 1)}`);
|
|
55
|
-
let whereSql = whereParts.join(' AND ');
|
|
56
|
-
const whereValues = [...pkValues];
|
|
57
|
-
if (extraCondition) {
|
|
58
|
-
whereSql += ` AND ${extraCondition.build(pkCols.length + 1, tx.ph)}`;
|
|
59
|
-
whereValues.push(...extraCondition.getValues());
|
|
60
|
-
}
|
|
61
|
-
const rows = await tx.select({
|
|
62
|
-
tableName: schema.tableName,
|
|
63
|
-
where: whereSql,
|
|
64
|
-
values: whereValues,
|
|
65
|
-
limit: '1',
|
|
66
|
-
});
|
|
67
|
-
if (rows.length === 0)
|
|
68
|
-
throw httpError(404, `Record not found`);
|
|
75
|
+
await assertRecordExists(tx, schema.tableName, pkCols, pkValues, extraCondition);
|
|
69
76
|
}
|
|
70
77
|
// Build the main response (PK-only — every PK field)
|
|
71
78
|
const mainResult = Object.fromEntries(pkFields.map((f, i) => [f, pkValues[i]]));
|
|
@@ -73,13 +80,13 @@ export async function updateEngine(params) {
|
|
|
73
80
|
let secondaryResults;
|
|
74
81
|
if (secondaries && Object.keys(secondaries).length > 0) {
|
|
75
82
|
const mainForFK = { ...inputRecord };
|
|
76
|
-
secondaryResults = await processSecondaries(tx, tableConf, dbTables, mainForFK, secondaries);
|
|
83
|
+
secondaryResults = await processSecondaries({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant }, secondaries);
|
|
77
84
|
}
|
|
78
85
|
// 9. Deletions (FK auto-fill from main like secondaries)
|
|
79
86
|
let deletionResults;
|
|
80
87
|
if (deletions && Object.keys(deletions).length > 0) {
|
|
81
88
|
const mainForFK = { ...inputRecord };
|
|
82
|
-
deletionResults = await processDeletions(tx, tableConf, mainForFK, deletions);
|
|
89
|
+
deletionResults = await processDeletions({ db: tx, tableConf, dbTables, mainRecord: mainForFK, tenant }, deletions);
|
|
83
90
|
}
|
|
84
91
|
// 10. afterUpdate hook (inside the transaction: throwing rolls everything back)
|
|
85
92
|
if (tableConf.afterUpdate) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correlated aggregate subqueries — the machinery behind `<alias>.<fn>.<field>` in both
|
|
3
|
+
* `orderBy` and `conditions` (HAVING-style).
|
|
4
|
+
*
|
|
5
|
+
* Kept separate from ./conditions.ts on purpose: an aggregate embeds a join-reference
|
|
6
|
+
* condition, while a dotted condition embeds an aggregate. Holding both in one module would
|
|
7
|
+
* be fine, but splitting them the other way round (aggregates with orderBy) would make
|
|
8
|
+
* order-by and conditions import each other.
|
|
9
|
+
*/
|
|
10
|
+
import { Expression } from 'node-condition-builder';
|
|
11
|
+
import type { QueryClient } from '../../db.js';
|
|
12
|
+
import type { QueryParams } from '../query-params.js';
|
|
13
|
+
import type { DbTables, ITable, JoinGroupRequest, SearchCondition, TenantContext } from '../../../types.js';
|
|
14
|
+
export interface AggFn {
|
|
15
|
+
sql: string;
|
|
16
|
+
distinct: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare const AGG_FN: Record<string, AggFn>;
|
|
19
|
+
export declare function aggExpr(fn: AggFn, qualifiedCol: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Correlated aggregate subquery for `<alias>.<fn>.<field>`, as an Expression carrying the
|
|
22
|
+
* values of its optional filter. Markers are `?`, so whoever embeds it — a ConditionBuilder
|
|
23
|
+
* or an ORDER BY list — decides the placeholder positions.
|
|
24
|
+
*
|
|
25
|
+
* The subquery carries the join table's own tenant scope, exactly as `executeJoinGroup` does
|
|
26
|
+
* for the aggregations it returns. Without it, ordering and HAVING-style filtering would count
|
|
27
|
+
* child rows of other tenants, disagreeing with the scoped `result.joinGroup` for the same
|
|
28
|
+
* alias and leaking their existence.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildAggOrderExpr(db: QueryClient, dbTables: DbTables, tableConf: ITable, alias: string, fn: string, field: string, joinGroup: Record<string, JoinGroupRequest> | undefined, tenant?: TenantContext): Expression;
|
|
31
|
+
/** HAVING-style conditions: the dotted `<alias>.<fn>.<field>` entries of `conditions`. */
|
|
32
|
+
export declare function appendAggConditions(params: QueryParams, conditions: SearchCondition[], db: QueryClient, dbTables: DbTables, tableConf: ITable, joinGroup: Record<string, JoinGroupRequest> | undefined, tenant?: TenantContext): string;
|
|
33
|
+
//# sourceMappingURL=aggregations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregations.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/aggregations.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAoB,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACtE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAMtD,OAAO,KAAK,EACV,QAAQ,EACR,MAAM,EACN,gBAAgB,EAChB,eAAe,EACf,aAAa,EACd,MAAM,mBAAmB,CAAC;AAE3B,MAAM,WAAW,KAAK;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE;AAEzD,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAOxC,CAAC;AAEF,wBAAgB,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAAG,SAAS,EACvD,MAAM,CAAC,EAAE,aAAa,GACrB,UAAU,CAgEZ;AAED,0FAA0F;AAC1F,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,eAAe,EAAE,EAC7B,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAAG,SAAS,EACvD,MAAM,CAAC,EAAE,aAAa,GACrB,MAAM,CAyBR"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correlated aggregate subqueries — the machinery behind `<alias>.<fn>.<field>` in both
|
|
3
|
+
* `orderBy` and `conditions` (HAVING-style).
|
|
4
|
+
*
|
|
5
|
+
* Kept separate from ./conditions.ts on purpose: an aggregate embeds a join-reference
|
|
6
|
+
* condition, while a dotted condition embeds an aggregate. Holding both in one module would
|
|
7
|
+
* be fine, but splitting them the other way round (aggregates with orderBy) would make
|
|
8
|
+
* order-by and conditions import each other.
|
|
9
|
+
*/
|
|
10
|
+
import { ConditionBuilder, Expression } from 'node-condition-builder';
|
|
11
|
+
import { ALLOWED_SET } from '../../condition-methods.js';
|
|
12
|
+
import { tenantForTable, buildTenantRowGuard } from '../../tenant.js';
|
|
13
|
+
import { err400, validateSchemaField } from './fields.js';
|
|
14
|
+
import { requireJoin, extractJoinRefs, subqueryAlias } from './joins.js';
|
|
15
|
+
import { buildJoinRefCondition, dispatchConditionMethod } from './conditions.js';
|
|
16
|
+
export const AGG_FN = {
|
|
17
|
+
sum: { sql: 'SUM', distinct: false },
|
|
18
|
+
min: { sql: 'MIN', distinct: false },
|
|
19
|
+
max: { sql: 'MAX', distinct: false },
|
|
20
|
+
avg: { sql: 'AVG', distinct: false },
|
|
21
|
+
count: { sql: 'COUNT', distinct: false },
|
|
22
|
+
distinctCount: { sql: 'COUNT', distinct: true },
|
|
23
|
+
};
|
|
24
|
+
export function aggExpr(fn, qualifiedCol) {
|
|
25
|
+
return fn.distinct ? `COUNT(DISTINCT ${qualifiedCol})` : `${fn.sql}(${qualifiedCol})`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Correlated aggregate subquery for `<alias>.<fn>.<field>`, as an Expression carrying the
|
|
29
|
+
* values of its optional filter. Markers are `?`, so whoever embeds it — a ConditionBuilder
|
|
30
|
+
* or an ORDER BY list — decides the placeholder positions.
|
|
31
|
+
*
|
|
32
|
+
* The subquery carries the join table's own tenant scope, exactly as `executeJoinGroup` does
|
|
33
|
+
* for the aggregations it returns. Without it, ordering and HAVING-style filtering would count
|
|
34
|
+
* child rows of other tenants, disagreeing with the scoped `result.joinGroup` for the same
|
|
35
|
+
* alias and leaking their existence.
|
|
36
|
+
*/
|
|
37
|
+
export function buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup, tenant) {
|
|
38
|
+
const aggFn = AGG_FN[fn];
|
|
39
|
+
if (!aggFn)
|
|
40
|
+
err400(`Invalid aggregation function: ${fn}`);
|
|
41
|
+
const groupReq = joinGroup?.[alias];
|
|
42
|
+
if (!groupReq)
|
|
43
|
+
err400(`orderBy/conditions reference undeclared joinGroup: ${alias}`);
|
|
44
|
+
const declaredFields = groupReq.aggregations[fn];
|
|
45
|
+
if (!Array.isArray(declaredFields) || !declaredFields.includes(field)) {
|
|
46
|
+
err400(`orderBy/conditions reference undeclared aggregation: ${alias}.${fn}.${field}`);
|
|
47
|
+
}
|
|
48
|
+
const joinDef = requireJoin(tableConf, alias, false);
|
|
49
|
+
const { joinSchema, joinField } = joinDef;
|
|
50
|
+
if (groupReq.aggregations.by && groupReq.aggregations.by !== joinField) {
|
|
51
|
+
err400(`Cannot order by aggregation on joinGroup with 'by' clause on non-FK column: ${alias} (grouped by '${groupReq.aggregations.by}', correlation FK is '${joinField}')`);
|
|
52
|
+
}
|
|
53
|
+
// Pass the join table's conf: without it validateSchemaField skips the readExclude
|
|
54
|
+
// check, and a HAVING-style condition on a hidden field becomes a 400-vs-200 oracle
|
|
55
|
+
// (executeJoinGroup's own validation is skipped when the main result set is empty).
|
|
56
|
+
const fieldCol = validateSchemaField(field, joinSchema, dbTables[joinSchema.tableName]);
|
|
57
|
+
const refs = extractJoinRefs(db, tableConf, joinDef);
|
|
58
|
+
// The subquery FROM is aliased so the correlation to the outer table survives a
|
|
59
|
+
// self-referencing relation; everything inside references the alias.
|
|
60
|
+
const subAlias = subqueryAlias(alias, tableConf);
|
|
61
|
+
const subRef = db.qi(subAlias);
|
|
62
|
+
let filterWhere = '';
|
|
63
|
+
let filterVals = [];
|
|
64
|
+
const joinTableConf = dbTables[joinSchema.tableName];
|
|
65
|
+
// Tenant scope first: it is emitted before the request's own filter, so its values must be
|
|
66
|
+
// bound in that order too.
|
|
67
|
+
const guard = buildTenantRowGuard(db, tenantForTable(tenant, joinTableConf), subAlias);
|
|
68
|
+
let tenantWhere = '';
|
|
69
|
+
const tenantVals = [];
|
|
70
|
+
if (guard) {
|
|
71
|
+
const rendered = guard.toExpression();
|
|
72
|
+
tenantWhere = ` AND ${rendered.value}`;
|
|
73
|
+
tenantVals.push(...rendered.values);
|
|
74
|
+
}
|
|
75
|
+
if (groupReq.filters || groupReq.conditions?.length) {
|
|
76
|
+
const cb = buildJoinRefCondition(joinTableConf, joinSchema, { filters: groupReq.filters, conditions: groupReq.conditions }, db, subAlias);
|
|
77
|
+
const fragment = cb.toExpression();
|
|
78
|
+
if (fragment.value) {
|
|
79
|
+
filterWhere = ` AND ${fragment.value}`;
|
|
80
|
+
filterVals = [...fragment.values];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const qualifiedCol = `${subRef}.${db.qi(fieldCol)}`;
|
|
84
|
+
const expr = `COALESCE((SELECT ${aggExpr(aggFn, qualifiedCol)} FROM ${refs.joinTable} AS ${subRef} WHERE ${subRef}.${refs.fkCol} = ${refs.mainTable}.${refs.mainCol}${tenantWhere}${filterWhere}), 0)`;
|
|
85
|
+
return new Expression(expr, [...tenantVals, ...filterVals]);
|
|
86
|
+
}
|
|
87
|
+
/** HAVING-style conditions: the dotted `<alias>.<fn>.<field>` entries of `conditions`. */
|
|
88
|
+
export function appendAggConditions(params, conditions, db, dbTables, tableConf, joinGroup, tenant) {
|
|
89
|
+
let where = '';
|
|
90
|
+
for (const c of conditions) {
|
|
91
|
+
if (!c.field.includes('.'))
|
|
92
|
+
continue;
|
|
93
|
+
if (!ALLOWED_SET.has(c.method)) {
|
|
94
|
+
err400(`Invalid condition method: ${c.method}`);
|
|
95
|
+
}
|
|
96
|
+
const parts = c.field.split('.');
|
|
97
|
+
if (parts.length !== 3) {
|
|
98
|
+
err400(`Invalid dotted field in condition: ${c.field} (expected <alias>.<fn>.<field>)`);
|
|
99
|
+
}
|
|
100
|
+
const [alias, fn, field] = parts;
|
|
101
|
+
// The aggregate carries the values of its own filter; passing it as the left-hand side
|
|
102
|
+
// lets the ConditionBuilder place those and the compared value in one step.
|
|
103
|
+
const expr = buildAggOrderExpr(db, dbTables, tableConf, alias, fn, field, joinGroup, tenant);
|
|
104
|
+
const tmpCb = new ConditionBuilder('AND', db.cbDialect);
|
|
105
|
+
dispatchConditionMethod(tmpCb, c.method, expr, c.params);
|
|
106
|
+
where += ` AND ${params.emitCondition(tmpCb, db)}`;
|
|
107
|
+
}
|
|
108
|
+
return where;
|
|
109
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a request's `filters` and `conditions` into ConditionBuilder clauses.
|
|
3
|
+
*
|
|
4
|
+
* The method vocabulary itself lives in lib/condition-methods.ts, which is a pure data
|
|
5
|
+
* module shared with the TypeBox schema builders — the dispatch below is kept out of it so
|
|
6
|
+
* that module keeps its zero dependencies.
|
|
7
|
+
*/
|
|
8
|
+
import { ConditionBuilder, Expression } from 'node-condition-builder';
|
|
9
|
+
import type { QueryClient } from '../../db.js';
|
|
10
|
+
import type { FilterRecord, ITable, JoinRefFilter, SchemaDefinition, SearchCondition } from '../../../types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Apply one condition method to a builder, after checking the request actually supplied the
|
|
13
|
+
* operands that method needs.
|
|
14
|
+
*
|
|
15
|
+
* `params` is optional in the request schema, so `{field, method}` with no params is a
|
|
16
|
+
* well-formed request that reaches here — reading `params[0]` off it throws a TypeError the
|
|
17
|
+
* error handler can only report as a 500. Arity is checked here rather than at the four call
|
|
18
|
+
* sites so no path can skip it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function dispatchConditionMethod(cb: ConditionBuilder, method: string, colOrExpr: string | Expression, params: unknown[] | undefined): void;
|
|
21
|
+
export declare function buildJoinRefCondition(joinTableConf: ITable | undefined, joinSchema: SchemaDefinition, ref: JoinRefFilter, db: QueryClient, qualifier?: string): ConditionBuilder;
|
|
22
|
+
/**
|
|
23
|
+
* Apply non-dotted conditions to the main `condition` ConditionBuilder when
|
|
24
|
+
* possible. For schema fields → straight dispatch. For computed fields → return
|
|
25
|
+
* side-channel clauses to be appended to WHERE later (with correct placeholder
|
|
26
|
+
* offsets), since ConditionBuilder cannot bind values for an LHS expression.
|
|
27
|
+
*/
|
|
28
|
+
export declare function applyConditions(condition: ConditionBuilder, conditions: SearchCondition[], schema: SchemaDefinition, tableConf: ITable, db: QueryClient): void;
|
|
29
|
+
/** Computed-field equality filters on the main table. */
|
|
30
|
+
export declare function applyComputedFilters(condition: ConditionBuilder, filters: FilterRecord, tableConf: ITable, db: QueryClient): void;
|
|
31
|
+
//# sourceMappingURL=conditions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conditions.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/conditions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACtE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAW/C,OAAO,KAAK,EAEV,YAAY,EACZ,MAAM,EACN,aAAa,EACb,gBAAgB,EAChB,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,gBAAgB,EACpB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAAG,UAAU,EAC9B,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS,GAC5B,IAAI,CA4BN;AAuDD,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,UAAU,EAAE,gBAAgB,EAC5B,GAAG,EAAE,aAAa,EAClB,EAAE,EAAE,WAAW,EACf,SAAS,CAAC,EAAE,MAAM,GACjB,gBAAgB,CAuBlB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,gBAAgB,EAC3B,UAAU,EAAE,eAAe,EAAE,EAC7B,MAAM,EAAE,gBAAgB,EACxB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,WAAW,GACd,IAAI,CAcN;AAED,yDAAyD;AACzD,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE,YAAY,EACrB,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,WAAW,GACd,IAAI,CAIN"}
|