fastify-auto-sqlapi 0.1.14 → 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 (83) hide show
  1. package/AGENTS_BACKEND.md +4 -4
  2. package/BREAKING_CHANGES.md +5 -5
  3. package/README.md +1 -1
  4. package/dist/bin/generate-schema.js +89 -77
  5. package/dist/bin/generate-tables.js +72 -62
  6. package/dist/bin/utils.d.ts.map +1 -1
  7. package/dist/bin/utils.js +47 -33
  8. package/dist/lib/agent/manifest.d.ts +3 -2
  9. package/dist/lib/agent/manifest.d.ts.map +1 -1
  10. package/dist/lib/agent/manifest.js +102 -80
  11. package/dist/lib/cli/mysql-introspect.d.ts +4 -9
  12. package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
  13. package/dist/lib/cli/mysql-introspect.js +8 -7
  14. package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
  15. package/dist/lib/cli/pg-introspect.js +0 -1
  16. package/dist/lib/cli/schema-codegen.d.ts +3 -0
  17. package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
  18. package/dist/lib/cli/schema-codegen.js +3 -0
  19. package/dist/lib/cli/tables-codegen.d.ts +8 -1
  20. package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
  21. package/dist/lib/cli/tables-codegen.js +100 -118
  22. package/dist/lib/condition-methods.d.ts +14 -4
  23. package/dist/lib/condition-methods.d.ts.map +1 -1
  24. package/dist/lib/condition-methods.js +18 -4
  25. package/dist/lib/db.d.ts +1 -1
  26. package/dist/lib/db.d.ts.map +1 -1
  27. package/dist/lib/db.js +16 -4
  28. package/dist/lib/dialect.d.ts +2 -1
  29. package/dist/lib/dialect.d.ts.map +1 -1
  30. package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
  31. package/dist/lib/engine/bulk/bulk-upsert.js +42 -35
  32. package/dist/lib/engine/rest/delete.d.ts.map +1 -1
  33. package/dist/lib/engine/rest/get.d.ts.map +1 -1
  34. package/dist/lib/engine/rest/insert.d.ts.map +1 -1
  35. package/dist/lib/engine/rest/insert.js +1 -1
  36. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  37. package/dist/lib/engine/rest/update.js +25 -18
  38. package/dist/lib/engine/search/aggregations.d.ts +33 -0
  39. package/dist/lib/engine/search/aggregations.d.ts.map +1 -0
  40. package/dist/lib/engine/search/aggregations.js +109 -0
  41. package/dist/lib/engine/search/conditions.d.ts +31 -0
  42. package/dist/lib/engine/search/conditions.d.ts.map +1 -0
  43. package/dist/lib/engine/search/conditions.js +131 -0
  44. package/dist/lib/engine/search/fields.d.ts +49 -0
  45. package/dist/lib/engine/search/fields.d.ts.map +1 -0
  46. package/dist/lib/engine/search/fields.js +142 -0
  47. package/dist/lib/engine/search/join-families.d.ts +35 -0
  48. package/dist/lib/engine/search/join-families.d.ts.map +1 -0
  49. package/dist/lib/engine/search/join-families.js +345 -0
  50. package/dist/lib/engine/search/joins.d.ts +55 -0
  51. package/dist/lib/engine/search/joins.d.ts.map +1 -0
  52. package/dist/lib/engine/search/joins.js +81 -0
  53. package/dist/lib/engine/search/order-by.d.ts +35 -0
  54. package/dist/lib/engine/search/order-by.d.ts.map +1 -0
  55. package/dist/lib/engine/search/order-by.js +104 -0
  56. package/dist/lib/engine/search/search.d.ts.map +1 -1
  57. package/dist/lib/engine/search/search.js +101 -787
  58. package/dist/lib/engine/validate.d.ts +0 -5
  59. package/dist/lib/engine/validate.d.ts.map +1 -1
  60. package/dist/lib/engine/write-helpers.d.ts +25 -17
  61. package/dist/lib/engine/write-helpers.d.ts.map +1 -1
  62. package/dist/lib/engine/write-helpers.js +86 -44
  63. package/dist/lib/read-access.d.ts +12 -1
  64. package/dist/lib/read-access.d.ts.map +1 -1
  65. package/dist/lib/read-access.js +20 -4
  66. package/dist/lib/schema/helpers.d.ts +0 -15
  67. package/dist/lib/schema/helpers.d.ts.map +1 -1
  68. package/dist/lib/schema/helpers.js +7 -4
  69. package/dist/lib/schema/search.d.ts.map +1 -1
  70. package/dist/lib/schema/search.js +107 -128
  71. package/dist/lib/sql-api.d.ts.map +1 -1
  72. package/dist/lib/tenant.d.ts +26 -0
  73. package/dist/lib/tenant.d.ts.map +1 -1
  74. package/dist/lib/tenant.js +46 -0
  75. package/dist/routes/auto/route-helpers.d.ts +0 -7
  76. package/dist/routes/auto/route-helpers.d.ts.map +1 -1
  77. package/dist/routes/auto/route-helpers.js +46 -30
  78. package/dist/routes/auto/search.routes.js +0 -1
  79. package/dist/types/table.d.ts +1 -1
  80. package/docs/GLOSSARY.md +28 -0
  81. package/docs/adr/0012-anti-duplication-tooling.md +59 -0
  82. package/docs/adr/README.md +3 -1
  83. package/package.json +29 -2
@@ -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"}