fastify-auto-sqlapi 0.1.13 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/AGENTS_BACKEND.md +32 -5
  2. package/BREAKING_CHANGES.md +80 -5
  3. package/CHANGELOG.md +45 -0
  4. package/README.md +49 -7
  5. package/dist/bin/generate-schema.js +89 -77
  6. package/dist/bin/generate-tables.js +72 -62
  7. package/dist/bin/utils.d.ts.map +1 -1
  8. package/dist/bin/utils.js +47 -33
  9. package/dist/lib/agent/manifest.d.ts +3 -2
  10. package/dist/lib/agent/manifest.d.ts.map +1 -1
  11. package/dist/lib/agent/manifest.js +104 -80
  12. package/dist/lib/cli/mysql-introspect.d.ts +4 -9
  13. package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
  14. package/dist/lib/cli/mysql-introspect.js +8 -7
  15. package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
  16. package/dist/lib/cli/pg-introspect.js +0 -1
  17. package/dist/lib/cli/schema-codegen.d.ts +3 -0
  18. package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
  19. package/dist/lib/cli/schema-codegen.js +3 -0
  20. package/dist/lib/cli/tables-codegen.d.ts +8 -1
  21. package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
  22. package/dist/lib/cli/tables-codegen.js +100 -118
  23. package/dist/lib/condition-methods.d.ts +14 -4
  24. package/dist/lib/condition-methods.d.ts.map +1 -1
  25. package/dist/lib/condition-methods.js +18 -4
  26. package/dist/lib/db.d.ts +1 -1
  27. package/dist/lib/db.d.ts.map +1 -1
  28. package/dist/lib/db.js +16 -4
  29. package/dist/lib/dialect.d.ts +2 -1
  30. package/dist/lib/dialect.d.ts.map +1 -1
  31. package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
  32. package/dist/lib/engine/bulk/bulk-upsert.js +42 -35
  33. package/dist/lib/engine/rest/delete.d.ts.map +1 -1
  34. package/dist/lib/engine/rest/get.d.ts.map +1 -1
  35. package/dist/lib/engine/rest/insert.d.ts.map +1 -1
  36. package/dist/lib/engine/rest/insert.js +1 -1
  37. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  38. package/dist/lib/engine/rest/update.js +25 -18
  39. package/dist/lib/engine/search/aggregations.d.ts +33 -0
  40. package/dist/lib/engine/search/aggregations.d.ts.map +1 -0
  41. package/dist/lib/engine/search/aggregations.js +109 -0
  42. package/dist/lib/engine/search/conditions.d.ts +31 -0
  43. package/dist/lib/engine/search/conditions.d.ts.map +1 -0
  44. package/dist/lib/engine/search/conditions.js +131 -0
  45. package/dist/lib/engine/search/fields.d.ts +49 -0
  46. package/dist/lib/engine/search/fields.d.ts.map +1 -0
  47. package/dist/lib/engine/search/fields.js +142 -0
  48. package/dist/lib/engine/search/join-families.d.ts +35 -0
  49. package/dist/lib/engine/search/join-families.d.ts.map +1 -0
  50. package/dist/lib/engine/search/join-families.js +345 -0
  51. package/dist/lib/engine/search/joins.d.ts +55 -0
  52. package/dist/lib/engine/search/joins.d.ts.map +1 -0
  53. package/dist/lib/engine/search/joins.js +81 -0
  54. package/dist/lib/engine/search/order-by.d.ts +35 -0
  55. package/dist/lib/engine/search/order-by.d.ts.map +1 -0
  56. package/dist/lib/engine/search/order-by.js +104 -0
  57. package/dist/lib/engine/search/search.d.ts.map +1 -1
  58. package/dist/lib/engine/search/search.js +106 -749
  59. package/dist/lib/engine/validate.d.ts +0 -5
  60. package/dist/lib/engine/validate.d.ts.map +1 -1
  61. package/dist/lib/engine/write-helpers.d.ts +25 -17
  62. package/dist/lib/engine/write-helpers.d.ts.map +1 -1
  63. package/dist/lib/engine/write-helpers.js +86 -44
  64. package/dist/lib/read-access.d.ts +37 -1
  65. package/dist/lib/read-access.d.ts.map +1 -1
  66. package/dist/lib/read-access.js +64 -1
  67. package/dist/lib/schema/helpers.d.ts +0 -15
  68. package/dist/lib/schema/helpers.d.ts.map +1 -1
  69. package/dist/lib/schema/helpers.js +7 -4
  70. package/dist/lib/schema/search.d.ts.map +1 -1
  71. package/dist/lib/schema/search.js +120 -121
  72. package/dist/lib/sql-api.d.ts.map +1 -1
  73. package/dist/lib/table-helpers.d.ts +16 -1
  74. package/dist/lib/table-helpers.d.ts.map +1 -1
  75. package/dist/lib/table-helpers.js +55 -1
  76. package/dist/lib/tenant.d.ts +26 -0
  77. package/dist/lib/tenant.d.ts.map +1 -1
  78. package/dist/lib/tenant.js +46 -0
  79. package/dist/routes/auto/route-helpers.d.ts +0 -7
  80. package/dist/routes/auto/route-helpers.d.ts.map +1 -1
  81. package/dist/routes/auto/route-helpers.js +46 -30
  82. package/dist/routes/auto/search.routes.js +0 -1
  83. package/dist/types/join.d.ts +9 -0
  84. package/dist/types/join.d.ts.map +1 -1
  85. package/dist/types/table.d.ts +1 -1
  86. package/docs/GLOSSARY.md +28 -0
  87. package/docs/adr/0010-joins-do-not-run-route-guards.md +100 -0
  88. package/docs/adr/0011-join-fields-allowlist.md +95 -0
  89. package/docs/adr/0012-anti-duplication-tooling.md +59 -0
  90. package/docs/adr/README.md +5 -1
  91. package/package.json +29 -2
@@ -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;
@@ -50,22 +72,7 @@ export async function updateEngine(params) {
50
72
  throw httpError(404, `Record not found`);
51
73
  }
52
74
  else {
53
- // No fields to update: verify the record exists (for secondaries/deletions)
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"}
@@ -0,0 +1,131 @@
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 } from 'node-condition-builder';
9
+ import { assertFiltersReadable, ownField } from '../../read-access.js';
10
+ import { ALLOWED_SET, SINGLE_VALUE_SET, BETWEEN_SET, IN_SET, NULL_SET, } from '../../condition-methods.js';
11
+ import { err400, validateSchemaField, evaluateComputedField, resolveFieldRef, } from './fields.js';
12
+ /**
13
+ * Apply one condition method to a builder, after checking the request actually supplied the
14
+ * operands that method needs.
15
+ *
16
+ * `params` is optional in the request schema, so `{field, method}` with no params is a
17
+ * well-formed request that reaches here — reading `params[0]` off it throws a TypeError the
18
+ * error handler can only report as a 500. Arity is checked here rather than at the four call
19
+ * sites so no path can skip it.
20
+ */
21
+ export function dispatchConditionMethod(cb, method, colOrExpr, params) {
22
+ if (params !== undefined && !Array.isArray(params)) {
23
+ err400(`Condition method '${method}': 'params' must be an array`);
24
+ }
25
+ const args = params ?? [];
26
+ const require = (n) => {
27
+ if (args.length < n) {
28
+ err400(`Condition method '${method}' requires ${n} param(s), got ${args.length}`);
29
+ }
30
+ };
31
+ if (SINGLE_VALUE_SET.has(method)) {
32
+ require(1);
33
+ cb[method](colOrExpr, args[0]);
34
+ }
35
+ else if (BETWEEN_SET.has(method)) {
36
+ require(2);
37
+ cb[method](colOrExpr, args[0], args[1]);
38
+ }
39
+ else if (IN_SET.has(method)) {
40
+ require(1);
41
+ // An explicit `undefined` stays a documented no-op; any other non-array would make the
42
+ // builder iterate a non-iterable.
43
+ if (args[0] !== undefined && !Array.isArray(args[0])) {
44
+ err400(`Condition method '${method}' requires an array as its first param`);
45
+ }
46
+ cb[method](colOrExpr, args[0]);
47
+ }
48
+ else if (NULL_SET.has(method)) {
49
+ cb[method](colOrExpr, true);
50
+ }
51
+ }
52
+ /**
53
+ * Equality filters targeting *computed* fields: `filters.<computedName>` becomes
54
+ * `<expr> = <value>` on the builder `tableConf.filters()` has already populated with the plain
55
+ * column filters. A name that is not a declared computed field is skipped — it was either
56
+ * handled there or rejected up front by `assertKnownFilterKeys`.
57
+ *
58
+ * `qualifier` prefixes the columns the computed expression references: the join's qualifier
59
+ * inside a side query, undefined on the main table (where the schema's own name is used).
60
+ */
61
+ function applyComputedEqualityFilters(cb, filters, computed, schema, db, qualifier) {
62
+ if (!computed)
63
+ return;
64
+ for (const [name, value] of Object.entries(filters)) {
65
+ if (value === null || value === undefined)
66
+ continue;
67
+ const fn = ownField(computed, name);
68
+ if (!fn)
69
+ continue;
70
+ const expr = evaluateComputedField(name, fn, schema, db, qualifier, '', true);
71
+ cb.isEqual(expr, value);
72
+ }
73
+ }
74
+ /** Operator conditions on a join reference, against a schema or a computed field. */
75
+ function applyJoinRefConditions(cb, conditions, joinTableConf, joinSchema, db, colQualifier) {
76
+ const computed = joinTableConf?.computedFields;
77
+ for (const c of conditions) {
78
+ if (!ALLOWED_SET.has(c.method)) {
79
+ err400(`Invalid condition method: ${c.method}`);
80
+ }
81
+ // Computed fields resolve to an Expression carrying their own bound values, which the
82
+ // ConditionBuilder places together with the compared value.
83
+ const fn = ownField(computed, c.field);
84
+ const operand = fn
85
+ ? evaluateComputedField(c.field, fn, joinSchema, db, colQualifier, '', true)
86
+ : `${db.qi(colQualifier)}.${db.qi(validateSchemaField(c.field, joinSchema, joinTableConf))}`;
87
+ dispatchConditionMethod(cb, c.method, operand, c.params ?? []);
88
+ }
89
+ }
90
+ export function buildJoinRefCondition(joinTableConf, joinSchema, ref, db, qualifier) {
91
+ assertFiltersReadable(ref.filters, joinTableConf);
92
+ // Filter keys are checked by assertJoinFilterKeys, up front in searchEngine — this side
93
+ // query may never run, so it is not a place a request can be rejected from.
94
+ // Bare table name for plain side-queries; the subquery alias when the condition lands
95
+ // inside an aliased, correlated FROM (there the table name would resolve to the outer query).
96
+ const colQualifier = qualifier ?? joinSchema.tableName;
97
+ const cb = (ref.filters && joinTableConf)
98
+ ? joinTableConf.filters(ref.filters, db.cbDialect, colQualifier)
99
+ : new ConditionBuilder('AND', db.cbDialect);
100
+ if (ref.filters) {
101
+ applyComputedEqualityFilters(cb, ref.filters, joinTableConf?.computedFields, joinSchema, db, colQualifier);
102
+ }
103
+ if (ref.conditions?.length) {
104
+ applyJoinRefConditions(cb, ref.conditions, joinTableConf, joinSchema, db, colQualifier);
105
+ }
106
+ return cb;
107
+ }
108
+ /**
109
+ * Apply non-dotted conditions to the main `condition` ConditionBuilder when
110
+ * possible. For schema fields → straight dispatch. For computed fields → return
111
+ * side-channel clauses to be appended to WHERE later (with correct placeholder
112
+ * offsets), since ConditionBuilder cannot bind values for an LHS expression.
113
+ */
114
+ export function applyConditions(condition, conditions, schema, tableConf, db) {
115
+ for (const c of conditions) {
116
+ // Skip dot-notation fields — those become aggregation conditions processed later
117
+ if (c.field.includes('.'))
118
+ continue;
119
+ if (!ALLOWED_SET.has(c.method)) {
120
+ err400(`Invalid condition method: ${c.method}`);
121
+ }
122
+ // A computed field resolves to an Expression carrying its own bound values; the
123
+ // ConditionBuilder places them, so no placeholder offset is computed here.
124
+ const ref = resolveFieldRef(c.field, schema, tableConf, db);
125
+ dispatchConditionMethod(condition, c.method, ref.expr, c.params);
126
+ }
127
+ }
128
+ /** Computed-field equality filters on the main table. */
129
+ export function applyComputedFilters(condition, filters, tableConf, db) {
130
+ applyComputedEqualityFilters(condition, filters, tableConf.computedFields, tableConf.Schema, db);
131
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Field resolution for the search engine: turning a name in a request body into a SQL
3
+ * operand, and rows back into API records.
4
+ *
5
+ * This is the leaf of the search modules — it knows about schemas, computed fields and
6
+ * read access, and nothing about joins, ordering or query execution.
7
+ */
8
+ import { Expression } from 'node-condition-builder';
9
+ import type { QueryClient } from '../../db.js';
10
+ import type { ITable, SchemaDefinition, ComputedFieldFn } from '../../../types.js';
11
+ export declare function validateSchemaField(field: string, schema: SchemaDefinition, tableConf?: ITable): string;
12
+ export declare function err400(msg: string): never;
13
+ /**
14
+ * Resolve a field reference in a request body to a SQL operand. A schema column becomes
15
+ * the quoted column name; a computed field becomes an Expression carrying its own bound
16
+ * values, so it can be handed straight to a ConditionBuilder. Throws 400 if neither.
17
+ */
18
+ export interface ResolvedFieldRef {
19
+ expr: string | Expression;
20
+ computed: boolean;
21
+ }
22
+ export declare function resolveFieldRef(field: string, schema: SchemaDefinition, tableConf: ITable | undefined, db: QueryClient, alias?: string): ResolvedFieldRef;
23
+ /**
24
+ * Evaluate a computed field into an Expression carrying its own bound values.
25
+ *
26
+ * Inside a WHERE clause the ConditionBuilder assigns the placeholder indexes, so bound
27
+ * values are always safe there. `allowBoundValues=false` is for the positions that sit
28
+ * *before* the WHERE values in the parameter order (SELECT projections, aggregations),
29
+ * where they cannot be placed correctly; those still reject with 400.
30
+ */
31
+ export declare function evaluateComputedField(name: string, fn: ComputedFieldFn, schema: SchemaDefinition, db: QueryClient, alias: string | undefined, message: string, allowBoundValues?: boolean): Expression;
32
+ /** Bound values carried by a resolved field reference. */
33
+ export declare function refValues(expr: string | Expression): unknown[];
34
+ /** Render a resolved field reference to SQL, binding any values from `startIndex`. */
35
+ export declare function renderRef(expr: string | Expression, startIndex: number, db: QueryClient): string;
36
+ /** Map rows from snake_case columns to camelCase fields, preserving any computed-field columns
37
+ * that are projected by their declared name (skipped by camelcaseObject). */
38
+ export declare function mapRowsToCamelCase(rows: Record<string, unknown>[], schema: SchemaDefinition, computedFieldNames?: Iterable<string>): Record<string, unknown>[];
39
+ /**
40
+ * Build the SELECT columns list for a join fetch: '*' or comma-separated quoted columns.
41
+ * A default '*' selection narrows to the join table's readable columns when it declares
42
+ * `readExclude`; an explicit selection naming an excluded field is rejected with 400.
43
+ *
44
+ * On a relation narrowed by a `fields` allowlist the '*' is always spelled out. Emitting a
45
+ * real `SELECT *` there would fetch the excluded columns and leave them to be dropped by
46
+ * the response serializer — which does not run for a caller of `sqlApi.search()`.
47
+ */
48
+ export declare function buildSelectionColumns(selection: string, joinSchema: SchemaDefinition, db: QueryClient, joinTableConf?: ITable, restricted?: boolean): string;
49
+ //# sourceMappingURL=fields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/fields.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAS/C,OAAO,KAAK,EACV,MAAM,EACN,gBAAgB,EAGhB,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAI3B,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,gBAAgB,EACxB,SAAS,CAAC,EAAE,MAAM,GACjB,MAAM,CAQR;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAIzC;AA6BD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,GAAG,UAAU,CAAC;IAC1B,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,gBAAgB,EACxB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,EAAE,EAAE,WAAW,EACf,KAAK,CAAC,EAAE,MAAM,GACb,gBAAgB,CAiBlB;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,gBAAgB,EACxB,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,OAAO,EAAE,MAAM,EACf,gBAAgB,UAAQ,GACvB,UAAU,CAMZ;AA0BD,0DAA0D;AAC1D,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,EAAE,CAE9D;AAED,sFAAsF;AACtF,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,GAAG,UAAU,EACzB,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,WAAW,GACd,MAAM,CAER;AAED;6EAC6E;AAC7E,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC/B,MAAM,EAAE,gBAAgB,EACxB,kBAAkB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,GACpC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAW3B;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,gBAAgB,EAC5B,EAAE,EAAE,WAAW,EACf,aAAa,CAAC,EAAE,MAAM,EACtB,UAAU,UAAQ,GACjB,MAAM,CAUR"}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Field resolution for the search engine: turning a name in a request body into a SQL
3
+ * operand, and rows back into API records.
4
+ *
5
+ * This is the leaf of the search modules — it knows about schemas, computed fields and
6
+ * read access, and nothing about joins, ordering or query execution.
7
+ */
8
+ import { Expression } from 'node-condition-builder';
9
+ import { camelcaseObject } from '../../naming.js';
10
+ import { assertReadable, readableSelectColumns, explicitSelectColumns, hasOwnField, ownField, } from '../../read-access.js';
11
+ // ─── Schema field validation ────────────────────────────────
12
+ export function validateSchemaField(field, schema, tableConf) {
13
+ if (!hasOwnField(schema.fields, field)) {
14
+ const err = new Error(`Unknown field: ${field}`);
15
+ err.statusCode = 400;
16
+ throw err;
17
+ }
18
+ assertReadable(tableConf, field);
19
+ return schema.col(field);
20
+ }
21
+ export function err400(msg) {
22
+ const e = new Error(msg);
23
+ e.statusCode = 400;
24
+ throw e;
25
+ }
26
+ // ─── Computed fields resolution ─────────────────────────────
27
+ /**
28
+ * Build a ComputedFieldContext bound to a specific table/schema. `alias`
29
+ * (if provided) qualifies the columns produced by `qiCol` — needed when the
30
+ * computed is embedded in a `LEFT JOIN <table> AS <alias>` (joinLeft).
31
+ */
32
+ function buildComputedContext(db, schema, alias) {
33
+ return {
34
+ db,
35
+ qiCol(field, opts) {
36
+ if (!hasOwnField(schema.fields, field)) {
37
+ err400(`Unknown field referenced by computed expression: ${field}`);
38
+ }
39
+ const col = db.qi(schema.col(field));
40
+ // Default to the owning table: the statement may carry joins, and a bare
41
+ // column shared with a joined table would be ambiguous.
42
+ const qualifier = opts?.qualifier ?? alias ?? schema.tableName;
43
+ return qualifier ? `${db.qi(qualifier)}.${col}` : col;
44
+ },
45
+ };
46
+ }
47
+ export function resolveFieldRef(field, schema, tableConf, db, alias) {
48
+ if (hasOwnField(schema.fields, field)) {
49
+ assertReadable(tableConf, field);
50
+ const col = db.qi(schema.col(field));
51
+ // Qualified with the alias or the owning table — never bare, so the reference
52
+ // stays unambiguous when the statement carries joins.
53
+ const expr = `${db.qi(alias ?? schema.tableName)}.${col}`;
54
+ return { expr, computed: false };
55
+ }
56
+ const fn = ownField(tableConf?.computedFields, field);
57
+ if (fn) {
58
+ const ctx = buildComputedContext(db, schema, alias);
59
+ return { expr: computedExpression(field, fn(ctx)), computed: true };
60
+ }
61
+ err400(`Unknown field: ${field}`);
62
+ }
63
+ /**
64
+ * Evaluate a computed field into an Expression carrying its own bound values.
65
+ *
66
+ * Inside a WHERE clause the ConditionBuilder assigns the placeholder indexes, so bound
67
+ * values are always safe there. `allowBoundValues=false` is for the positions that sit
68
+ * *before* the WHERE values in the parameter order (SELECT projections, aggregations),
69
+ * where they cannot be placed correctly; those still reject with 400.
70
+ */
71
+ export function evaluateComputedField(name, fn, schema, db, alias, message, allowBoundValues = false) {
72
+ const ev = fn(buildComputedContext(db, schema, alias));
73
+ if (!allowBoundValues && (ev.values?.length ?? 0) > 0) {
74
+ err400(message);
75
+ }
76
+ return computedExpression(name, ev);
77
+ }
78
+ /**
79
+ * Build the Expression for a computed field, checking that it marks each bound value
80
+ * with a `?`.
81
+ *
82
+ * A mismatch is always a table-configuration bug and would otherwise produce a query that
83
+ * binds values nobody references — silently returning wrong rows. Expressions without
84
+ * bound values are exempt: they are emitted verbatim, so a `?` there is a literal operator
85
+ * (PostgreSQL jsonb) rather than a marker.
86
+ */
87
+ function computedExpression(name, ev) {
88
+ const values = ev.values ?? [];
89
+ if (values.length > 0) {
90
+ const markers = (ev.expr.match(/\\\?|\?/g) ?? []).filter((m) => m === '?').length;
91
+ if (markers !== values.length) {
92
+ throw new Error(`Computed field '${name}' declares ${values.length} bound value(s) but its expression ` +
93
+ `contains ${markers} '?' marker(s). Mark each bound value with '?' (use '\\?' for a ` +
94
+ `literal question mark) so the engine can assign placeholder positions.`);
95
+ }
96
+ }
97
+ return new Expression(ev.expr, values);
98
+ }
99
+ /** Bound values carried by a resolved field reference. */
100
+ export function refValues(expr) {
101
+ return expr instanceof Expression ? [...expr.values] : [];
102
+ }
103
+ /** Render a resolved field reference to SQL, binding any values from `startIndex`. */
104
+ export function renderRef(expr, startIndex, db) {
105
+ return expr instanceof Expression ? expr.render(startIndex, db.ph) : expr;
106
+ }
107
+ /** Map rows from snake_case columns to camelCase fields, preserving any computed-field columns
108
+ * that are projected by their declared name (skipped by camelcaseObject). */
109
+ export function mapRowsToCamelCase(rows, schema, computedFieldNames) {
110
+ const computed = computedFieldNames ? new Set(computedFieldNames) : null;
111
+ return rows.map((r) => {
112
+ const camel = camelcaseObject(r, schema);
113
+ if (computed) {
114
+ for (const name of computed) {
115
+ if (name in r)
116
+ camel[name] = r[name];
117
+ }
118
+ }
119
+ return camel;
120
+ });
121
+ }
122
+ /**
123
+ * Build the SELECT columns list for a join fetch: '*' or comma-separated quoted columns.
124
+ * A default '*' selection narrows to the join table's readable columns when it declares
125
+ * `readExclude`; an explicit selection naming an excluded field is rejected with 400.
126
+ *
127
+ * On a relation narrowed by a `fields` allowlist the '*' is always spelled out. Emitting a
128
+ * real `SELECT *` there would fetch the excluded columns and leave them to be dropped by
129
+ * the response serializer — which does not run for a caller of `sqlApi.search()`.
130
+ */
131
+ export function buildSelectionColumns(selection, joinSchema, db, joinTableConf, restricted = false) {
132
+ if (selection === '*') {
133
+ if (restricted)
134
+ return explicitSelectColumns(joinTableConf, joinSchema, db);
135
+ return readableSelectColumns(joinTableConf, joinSchema, db) ?? '*';
136
+ }
137
+ const table = db.qi(joinSchema.tableName);
138
+ return selection
139
+ .split(',')
140
+ .map((c) => `${table}.${db.qi(validateSchemaField(c.trim(), joinSchema, joinTableConf))}`)
141
+ .join(', ');
142
+ }
@@ -0,0 +1,35 @@
1
+ import type { QueryClient } from '../../db.js';
2
+ import type { QueryParams } from '../query-params.js';
3
+ import type { DbTables, ITable, JoinFetchRequest, JoinGroupRequest, JoinRefFilter, TenantContext } from '../../../types.js';
4
+ interface LeftJoinOnBuild {
5
+ joinClauses: string[];
6
+ values: unknown[];
7
+ }
8
+ interface LeftJoinFilterBuild {
9
+ whereExtras: string[];
10
+ values: unknown[];
11
+ }
12
+ /**
13
+ * The LEFT JOIN clauses themselves, each carrying the joined table's own tenant scope.
14
+ *
15
+ * The scope belongs in the ON clause, not the WHERE. In the WHERE it would discard main rows
16
+ * whose parent belongs to another tenant, silently turning the LEFT JOIN into an INNER JOIN.
17
+ * In the ON clause a foreign parent simply fails to match and reads as NULL, which closes the
18
+ * oracle: the parent rows are already withheld from the response (`executeJoinLeft` scopes its
19
+ * own side query), so an unscoped join would let a caller filter on values it cannot see and
20
+ * recover them one predicate at a time.
21
+ *
22
+ * Emitted before the WHERE fragments because that is where it lands in the statement — MySQL
23
+ * binds `?` positionally.
24
+ */
25
+ export declare function buildLeftJoinOnClauses(db: QueryClient, dbTables: DbTables, tableConf: ITable, aliasesNeedingJoin: Set<string>, startIdx: number, tenant?: TenantContext): LeftJoinOnBuild;
26
+ /** The WHERE fragments a `joinLeft` request's own filters and conditions contribute. */
27
+ export declare function buildLeftJoinFilters(db: QueryClient, dbTables: DbTables, tableConf: ITable, aliasesNeedingJoin: Set<string>, joinLeft: Record<string, JoinFetchRequest> | undefined, startIdx: number): LeftJoinFilterBuild;
28
+ /** joinMultiple: 1:N children, returned as rows under `result.joinMultiple.<alias>`. */
29
+ export declare function executeJoinMultiple(db: QueryClient, dbTables: DbTables, tableConf: ITable, mainResults: Record<string, unknown>[], joinMultiple: Record<string, JoinFetchRequest>, tenant?: TenantContext): Promise<Record<string, Record<string, unknown>[]>>;
30
+ /** joinLeft: N:1 parents, returned as rows under `result.joinLeft.<alias>`. */
31
+ export declare function executeJoinLeft(db: QueryClient, dbTables: DbTables, tableConf: ITable, mainResults: Record<string, unknown>[], joinLeft: Record<string, JoinFetchRequest>, tenant?: TenantContext): Promise<Record<string, Record<string, unknown>[]>>;
32
+ export declare function executeJoinGroup(db: QueryClient, dbTables: DbTables, tableConf: ITable, mainResults: Record<string, unknown>[], joinGroup: Record<string, JoinGroupRequest>, tenant?: TenantContext): Promise<Record<string, Record<string, unknown>>>;
33
+ export declare function buildJoinMustExistClauses(params: QueryParams, db: QueryClient, dbTables: DbTables, tableConf: ITable, joinMustExist: Record<string, JoinRefFilter>, tenant?: TenantContext): string;
34
+ export {};
35
+ //# sourceMappingURL=join-families.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"join-families.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/search/join-families.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAmBtD,OAAO,KAAK,EAEV,QAAQ,EAER,MAAM,EACN,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EAGb,aAAa,EAEd,MAAM,mBAAmB,CAAC;AAI3B,UAAU,eAAe;IACvB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,EAAE,OAAO,EAAE,CAAC;CACnB;AAED,UAAU,mBAAmB;IAC3B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,EAAE,OAAO,EAAE,CAAC;CACnB;AAsED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,kBAAkB,EAAE,GAAG,CAAC,MAAM,CAAC,EAC/B,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,aAAa,GACrB,eAAe,CAgCjB;AAED,wFAAwF;AACxF,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,kBAAkB,EAAE,GAAG,CAAC,MAAM,CAAC,EAC/B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAAG,SAAS,EACtD,QAAQ,EAAE,MAAM,GACf,mBAAmB,CA0CrB;AAmGD,wFAAwF;AACxF,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACtC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EAC9C,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAKpD;AAED,+EAA+E;AAC/E,wBAAgB,eAAe,CAC7B,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACtC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EAC1C,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAKpD;AAsGD,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACtC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EAC3C,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAqClD;AAyBD,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,WAAW,EACnB,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,EAC5C,MAAM,CAAC,EAAE,aAAa,GACrB,MAAM,CA4BR"}