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
@@ -1,5 +1,5 @@
1
1
  import { Type } from '@sinclair/typebox';
2
- import { ALLOWED_METHODS } from '../condition-methods.js';
2
+ import { ALLOWED_METHODS, MAX_CONDITIONS, MAX_ORDER_BY_LENGTH } from '../condition-methods.js';
3
3
  import { readableFieldNames } from '../read-access.js';
4
4
  import { DEFAULT_MAX_ITEMS_PER_PAGE } from '../../types.js';
5
5
  /** Schema fields minus the ones hidden by `readExclude`. */
@@ -12,6 +12,112 @@ function readableFields(schema, tableConf) {
12
12
  }
13
13
  return out;
14
14
  }
15
+ /**
16
+ * A `filters` map.
17
+ *
18
+ * Deliberately NOT `additionalProperties: false`, unlike the write bodies. Fastify runs Ajv
19
+ * with `removeAdditional: true`, so a closed schema would silently *strip* an unknown filter
20
+ * key before the handler ever sees it — reproducing the very bug it looks like it fixes, and
21
+ * leaving the engine's rejection unreachable over HTTP. Unknown keys are rejected by
22
+ * `assertKnownFilterKeys` in the engine instead, which also covers `sqlApi.search()`.
23
+ */
24
+ function filtersObject(fields) {
25
+ return Type.Partial(Type.Object(fields));
26
+ }
27
+ /**
28
+ * TypeBox types declared by a table's computed fields.
29
+ *
30
+ * Each computed is dry-run with a stub context purely to read its declared `type`; the
31
+ * functions are pure with respect to that context. Best-effort by design — a computed that
32
+ * throws is skipped, because this only feeds the Swagger/body schema and the engine
33
+ * validates for real at request time.
34
+ */
35
+ function computedFieldTypes(tableConf) {
36
+ const out = {};
37
+ for (const [name, fn] of Object.entries(tableConf?.computedFields ?? {})) {
38
+ try {
39
+ const stub = fn({
40
+ db: { qi: (s) => s, dialectName: 'postgres' },
41
+ qiCol: () => '""',
42
+ });
43
+ if (stub.type)
44
+ out[name] = stub.type;
45
+ }
46
+ catch {
47
+ // ignore — schema generation is best-effort; runtime validates.
48
+ }
49
+ }
50
+ return out;
51
+ }
52
+ function buildJoinBodyProps(dbTables, tableConf, conditionItemSchema) {
53
+ const props = {
54
+ joinMustExist: {},
55
+ joinMultiple: {},
56
+ joinGroup: {},
57
+ joinLeft: {},
58
+ };
59
+ for (const joinDef of tableConf.allowedReadJoins ?? []) {
60
+ const { joinSchema, alias, unique } = joinDef;
61
+ const joinTableConf = dbTables[joinSchema.tableName];
62
+ const joinSchemaFields = joinTableConf
63
+ ? { ...readableFields(joinSchema, joinTableConf), ...computedFieldTypes(joinTableConf) }
64
+ : { ...joinSchema.fields };
65
+ // `extraFilters` are only honoured where the engine delegates to the join table's
66
+ // own `filters()` — buildJoinRefCondition, i.e. the unique:false paths. joinLeft
67
+ // builds its condition inline (buildLeftJoinClauses) and handles schema and computed
68
+ // fields only, so advertising extraFilters there would promise a silent no-op.
69
+ const joinFilterFields = { ...joinSchemaFields, ...(joinTableConf?.extraFilters ?? {}) };
70
+ const joinRefShape = {
71
+ filters: Type.Optional(filtersObject(joinFilterFields)),
72
+ conditions: Type.Optional(Type.Array(conditionItemSchema, { maxItems: MAX_CONDITIONS })),
73
+ };
74
+ if (unique) {
75
+ props.joinLeft[alias] = Type.Object({
76
+ filters: Type.Optional(filtersObject(joinSchemaFields)),
77
+ conditions: Type.Optional(Type.Array(conditionItemSchema, { maxItems: MAX_CONDITIONS })),
78
+ selection: Type.Optional(Type.String()),
79
+ });
80
+ continue;
81
+ }
82
+ props.joinMustExist[alias] = Type.Object(joinRefShape);
83
+ props.joinMultiple[alias] = Type.Object({
84
+ ...joinRefShape,
85
+ selection: Type.Optional(Type.String()),
86
+ });
87
+ props.joinGroup[alias] = Type.Object({
88
+ aggregations: Type.Object({
89
+ by: Type.Optional(Type.String()),
90
+ distinctCount: Type.Optional(Type.Array(Type.String())),
91
+ min: Type.Optional(Type.Array(Type.String())),
92
+ max: Type.Optional(Type.Array(Type.String())),
93
+ sum: Type.Optional(Type.Array(Type.String())),
94
+ avg: Type.Optional(Type.Array(Type.String())),
95
+ count: Type.Optional(Type.Array(Type.String())),
96
+ }),
97
+ filters: Type.Optional(filtersObject(joinFilterFields)),
98
+ conditions: Type.Optional(Type.Array(conditionItemSchema, { maxItems: MAX_CONDITIONS })),
99
+ });
100
+ }
101
+ return props;
102
+ }
103
+ /** Per-alias *response* shapes: the rows each join family contributes to the result. */
104
+ function buildJoinResponseProps(dbTables, tableConf) {
105
+ const joinMultipleProps = {};
106
+ const joinLeftProps = {};
107
+ const joinGroupProps = {};
108
+ for (const joinDef of tableConf.allowedReadJoins ?? []) {
109
+ const { joinSchema, alias, unique } = joinDef;
110
+ const itemArray = Type.Array(Type.Partial(Type.Object(readableFields(joinSchema, dbTables[joinSchema.tableName]))));
111
+ if (unique) {
112
+ joinLeftProps[alias] = itemArray;
113
+ }
114
+ else {
115
+ joinMultipleProps[alias] = itemArray;
116
+ joinGroupProps[alias] = JoinGroupResultItem;
117
+ }
118
+ }
119
+ return { joinMultipleProps, joinLeftProps, joinGroupProps };
120
+ }
15
121
  const JoinGroupResultItem = Type.Object({
16
122
  sum: Type.Optional(Type.Record(Type.String(), Type.Any())),
17
123
  min: Type.Optional(Type.Record(Type.String(), Type.Any())),
@@ -30,105 +136,28 @@ export function SearchTableBodyPost(dbTables, tableName) {
30
136
  method: methodEnum,
31
137
  params: Type.Optional(Type.Array(Type.Any())),
32
138
  });
33
- // Build a TypeBox map of computed-field types (declared in defineTable).
34
- const computedTypes = {};
35
- if (tableConf.computedFields) {
36
- for (const [name, fn] of Object.entries(tableConf.computedFields)) {
37
- // Cheap dry-run with a stub context to extract the declared `type`.
38
- // The fn is pure-functional w.r.t. ctx — only `type` is read here.
39
- try {
40
- const stub = fn({
41
- db: { qi: (s) => s, dialectName: 'postgres' },
42
- qiCol: () => '""',
43
- });
44
- if (stub.type)
45
- computedTypes[name] = stub.type;
46
- }
47
- catch {
48
- // ignore — Swagger schema generation is best-effort; runtime validates.
49
- }
50
- }
51
- }
139
+ const computedTypes = computedFieldTypes(tableConf);
52
140
  const filterFields = {
53
141
  ...readableFields(schema, tableConf),
54
142
  ...tableConf.extraFilters,
55
143
  ...computedTypes,
56
144
  };
57
- const filtersSchema = Type.Optional(Type.Partial(Type.Object(filterFields)));
58
- // Per-alias entries split by unique flag:
59
- // - unique:false → joinMustExist / joinMultiple / joinGroup
60
- // - unique:true → joinLeft
61
- const joinMustExistProps = {};
62
- const joinMultipleProps = {};
63
- const joinGroupProps = {};
64
- const joinLeftProps = {};
65
- if (tableConf.allowedReadJoins) {
66
- for (const joinDef of tableConf.allowedReadJoins) {
67
- const { joinSchema, alias, unique } = joinDef;
68
- const joinTableConf = dbTables[joinSchema.tableName];
69
- const joinComputedTypes = {};
70
- if (joinTableConf?.computedFields) {
71
- for (const [name, fn] of Object.entries(joinTableConf.computedFields)) {
72
- try {
73
- const stub = fn({
74
- db: { qi: (s) => s, dialectName: 'postgres' },
75
- qiCol: () => '""',
76
- });
77
- if (stub.type)
78
- joinComputedTypes[name] = stub.type;
79
- }
80
- catch {
81
- // best-effort
82
- }
83
- }
84
- }
85
- const joinFilterFields = joinTableConf
86
- ? { ...readableFields(joinSchema, joinTableConf), ...joinTableConf.extraFilters, ...joinComputedTypes }
87
- : { ...joinSchema.fields };
88
- const joinRefShape = {
89
- filters: Type.Optional(Type.Partial(Type.Object(joinFilterFields))),
90
- conditions: Type.Optional(Type.Array(conditionItemSchema)),
91
- };
92
- const joinFetchShape = {
93
- ...joinRefShape,
94
- selection: Type.Optional(Type.String()),
95
- };
96
- if (unique) {
97
- joinLeftProps[alias] = Type.Object(joinFetchShape);
98
- }
99
- else {
100
- joinMustExistProps[alias] = Type.Object(joinRefShape);
101
- joinMultipleProps[alias] = Type.Object(joinFetchShape);
102
- joinGroupProps[alias] = Type.Object({
103
- aggregations: Type.Object({
104
- by: Type.Optional(Type.String()),
105
- distinctCount: Type.Optional(Type.Array(Type.String())),
106
- min: Type.Optional(Type.Array(Type.String())),
107
- max: Type.Optional(Type.Array(Type.String())),
108
- sum: Type.Optional(Type.Array(Type.String())),
109
- avg: Type.Optional(Type.Array(Type.String())),
110
- count: Type.Optional(Type.Array(Type.String())),
111
- }),
112
- filters: Type.Optional(Type.Partial(Type.Object(joinFilterFields))),
113
- conditions: Type.Optional(Type.Array(conditionItemSchema)),
114
- });
115
- }
116
- }
117
- }
145
+ const filtersSchema = Type.Optional(filtersObject(filterFields));
146
+ const joins = buildJoinBodyProps(dbTables, tableConf, conditionItemSchema);
118
147
  const bodyProperties = {
119
148
  filters: filtersSchema,
120
- conditions: Type.Optional(Type.Array(conditionItemSchema)),
149
+ conditions: Type.Optional(Type.Array(conditionItemSchema, { maxItems: MAX_CONDITIONS })),
121
150
  };
122
151
  if (Object.keys(computedTypes).length > 0) {
123
152
  bodyProperties.selectComputed = Type.Optional(Type.Array(Type.String()));
124
153
  }
125
- if (Object.keys(joinMustExistProps).length > 0) {
126
- bodyProperties.joinMustExist = Type.Optional(Type.Partial(Type.Object(joinMustExistProps)));
127
- bodyProperties.joinMultiple = Type.Optional(Type.Partial(Type.Object(joinMultipleProps)));
128
- bodyProperties.joinGroup = Type.Optional(Type.Partial(Type.Object(joinGroupProps)));
154
+ if (Object.keys(joins.joinMustExist).length > 0) {
155
+ bodyProperties.joinMustExist = Type.Optional(Type.Partial(Type.Object(joins.joinMustExist)));
156
+ bodyProperties.joinMultiple = Type.Optional(Type.Partial(Type.Object(joins.joinMultiple)));
157
+ bodyProperties.joinGroup = Type.Optional(Type.Partial(Type.Object(joins.joinGroup)));
129
158
  }
130
- if (Object.keys(joinLeftProps).length > 0) {
131
- bodyProperties.joinLeft = Type.Optional(Type.Partial(Type.Object(joinLeftProps)));
159
+ if (Object.keys(joins.joinLeft).length > 0) {
160
+ bodyProperties.joinLeft = Type.Optional(Type.Partial(Type.Object(joins.joinLeft)));
132
161
  }
133
162
  return Type.Object(bodyProperties);
134
163
  }
@@ -142,7 +171,7 @@ export function SearchTableBodyPost(dbTables, tableName) {
142
171
  */
143
172
  export function SearchTableQuery(maxItemsPerPage = DEFAULT_MAX_ITEMS_PER_PAGE) {
144
173
  return Type.Object({
145
- orderBy: Type.Optional(Type.String()),
174
+ orderBy: Type.Optional(Type.String({ maxLength: MAX_ORDER_BY_LENGTH })),
146
175
  page: Type.Optional(Type.Integer({ minimum: 1 })),
147
176
  itemsPerPage: Type.Optional(Type.Integer({
148
177
  minimum: 1,
@@ -162,42 +191,12 @@ export function SearchTableResponse(dbTables, tableName) {
162
191
  const tableConf = dbTables[tableName];
163
192
  // Computed fields are present in main rows only when explicitly listed in
164
193
  // request body's selectComputed, so they're optional/Partial here.
165
- const computedTypes = {};
166
- if (tableConf.computedFields) {
167
- for (const [name, fn] of Object.entries(tableConf.computedFields)) {
168
- try {
169
- const stub = fn({
170
- db: { qi: (s) => s, dialectName: 'postgres' },
171
- qiCol: () => '""',
172
- });
173
- if (stub.type)
174
- computedTypes[name] = stub.type;
175
- }
176
- catch {
177
- // best-effort
178
- }
179
- }
180
- }
194
+ const computedTypes = computedFieldTypes(tableConf);
181
195
  const mainReadable = readableFields(tableConf.Schema, tableConf);
182
196
  const mainItem = Object.keys(computedTypes).length > 0
183
197
  ? Type.Partial(Type.Object({ ...mainReadable, ...computedTypes }))
184
198
  : Type.Partial(Type.Object(mainReadable));
185
- const joinMultipleProps = {};
186
- const joinLeftProps = {};
187
- const joinGroupProps = {};
188
- if (tableConf.allowedReadJoins) {
189
- for (const joinDef of tableConf.allowedReadJoins) {
190
- const { joinSchema, alias, unique } = joinDef;
191
- const itemArray = Type.Array(Type.Partial(Type.Object(readableFields(joinSchema, dbTables[joinSchema.tableName]))));
192
- if (unique) {
193
- joinLeftProps[alias] = itemArray;
194
- }
195
- else {
196
- joinMultipleProps[alias] = itemArray;
197
- joinGroupProps[alias] = JoinGroupResultItem;
198
- }
199
- }
200
- }
199
+ const { joinMultipleProps, joinLeftProps, joinGroupProps } = buildJoinResponseProps(dbTables, tableConf);
201
200
  const responseProps = {
202
201
  table: Type.String(),
203
202
  main: Type.Array(mainItem),
@@ -1 +1 @@
1
- {"version":3,"file":"sql-api.d.ts","sourceRoot":"","sources":["../../src/lib/sql-api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sql-api.d.ts","sourceRoot":"","sources":["../../src/lib/sql-api.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { ConditionBuilder, type ConditionValueOrUndefined } from 'node-condition-builder';
2
- import type { TSchema, TObject } from '@sinclair/typebox';
2
+ import { type TSchema, type TObject } from '@sinclair/typebox';
3
3
  import type { SchemaDefinition, JoinDefinition, TableFilterFn, ITable } from '../types.js';
4
4
  type ExtraProps<EF> = EF extends TObject<infer P> ? P : EF extends Record<string, TSchema> ? EF : Record<string, never>;
5
5
  export declare function exportTableInfo<F extends Record<string, TSchema>, EF extends TObject | Record<string, TSchema> = Record<never, TSchema>>(Schema: SchemaDefinition<F>, extraFilters?: EF, extendedCondition?: (condition: ConditionBuilder, filters: {
@@ -14,6 +14,21 @@ export interface BuildRelationOptions {
14
14
  alias?: string;
15
15
  selection?: string;
16
16
  unique?: boolean;
17
+ /**
18
+ * Allowlist of the target table's fields reachable through this relation.
19
+ *
20
+ * The relation is declared against a schema narrowed to these fields, so every read
21
+ * surface — `selection`, `filters`, `conditions`, `orderBy`, aggregations, and the
22
+ * generated request/response schemas — rejects anything outside the list with
23
+ * `400 Unknown field`. Use it to expose a table broadly while keeping some of its
24
+ * columns out of one relation, without hiding them from the table's own routes the way
25
+ * `readExclude` would.
26
+ *
27
+ * Fail-closed: a column added to the table later is not reachable through the relation
28
+ * until it is added here. Must include the join field, and is not allowed on a write
29
+ * join.
30
+ */
31
+ fields?: string[];
17
32
  }
18
33
  export declare function buildRelation<M extends Record<string, TSchema>, J extends Record<string, TSchema>>(mainSchema: SchemaDefinition<M>, mainField: string & keyof M | (string & keyof M)[], joinSchema: SchemaDefinition<J>, joinField: string & keyof J, options?: BuildRelationOptions): JoinDefinition;
19
34
  export declare function buildUpsertRule<F extends Record<string, TSchema>>(schema: SchemaDefinition<F>, columns: (string & keyof F)[]): [SchemaDefinition, string[]];
@@ -1 +1 @@
1
- {"version":3,"file":"table-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/table-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,EAAiC,MAAM,wBAAwB,CAAC;AACzH,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,MAAM,EACP,MAAM,aAAa,CAAC;AAarB,KAAK,UAAU,CAAC,EAAE,IAAI,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAExH,wBAAgB,eAAe,CAC7B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,EAAE,SAAS,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,EAErE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,YAAY,GAAE,EAAa,EAC3B,iBAAiB,CAAC,EAAE,CAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,yBAAyB;CAAE,KAC3E,IAAI,GACR;IAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAsChG;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAChB,MAAM,CAAC,CAAC,CAAC,CAMX;AAkED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,aAAa,CAC3B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjC,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,EAClD,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,EAC3B,OAAO,CAAC,EAAE,oBAAoB,GAC7B,cAAc,CAShB;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,GAC5B,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAE9B;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,KAAK,EAAE,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,EAAE,GACvC,GAAG,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAEjC"}
1
+ {"version":3,"file":"table-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/table-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,EAAiC,MAAM,wBAAwB,CAAC;AACzH,OAAO,EAAQ,KAAK,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,MAAM,EACP,MAAM,aAAa,CAAC;AAarB,KAAK,UAAU,CAAC,EAAE,IAAI,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAExH,wBAAgB,eAAe,CAC7B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,EAAE,SAAS,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,EAErE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,YAAY,GAAE,EAAa,EAC3B,iBAAiB,CAAC,EAAE,CAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,yBAAyB;CAAE,KAC3E,IAAI,GACR;IAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAsChG;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAChB,MAAM,CAAC,CAAC,CAAC,CAOX;AAqFD,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAgDD,wBAAgB,aAAa,CAC3B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjC,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,EAClD,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,EAC3B,OAAO,CAAC,EAAE,oBAAoB,GAC7B,cAAc,CAWhB;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,GAC5B,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAE9B;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,KAAK,EAAE,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,EAAE,GACvC,GAAG,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAEjC"}
@@ -1,4 +1,5 @@
1
1
  import { ConditionBuilder } from 'node-condition-builder';
2
+ import { Type } from '@sinclair/typebox';
2
3
  import { getDialect } from './dialect.js';
3
4
  // Quote an identifier using the given ConditionBuilder dialect (or the global
4
5
  // default when not provided). Needed so that DB columns with uppercase letters
@@ -46,10 +47,27 @@ export function exportTableInfo(Schema, extraFilters = {}, extendedCondition) {
46
47
  export function defineTable(config) {
47
48
  validateAliasUniqueness(config.allowedReadJoins, 'allowedReadJoins');
48
49
  validateAliasUniqueness(config.allowedWriteJoins, 'allowedWriteJoins');
50
+ validateWriteJoinsUnrestricted(config.allowedWriteJoins);
49
51
  validateComputedFields(config);
50
52
  validateReadExclude(config);
51
53
  return config;
52
54
  }
55
+ /**
56
+ * `fields` narrows a relation for reading; it has no meaning on a write join and would be
57
+ * actively harmful there. Write paths resolve the secondary's upsert rule by looking the
58
+ * schema object up in `upsertMap` **by identity**, and a narrowed relation carries a copy —
59
+ * the lookup would miss and the upsert would silently degrade to a plain insert. The
60
+ * narrowed schema would also drop columns the caller legitimately sends.
61
+ */
62
+ function validateWriteJoinsUnrestricted(joins) {
63
+ for (const join of joins ?? []) {
64
+ if (join.fields) {
65
+ throw new Error(`defineTable: relation '${join.alias}' in allowedWriteJoins declares 'fields'. ` +
66
+ `The fields allowlist restricts reading only — remove it, and use a separate ` +
67
+ `buildRelation for the read side if that relation is also in allowedReadJoins.`);
68
+ }
69
+ }
70
+ }
53
71
  function validateReadExclude(config) {
54
72
  const excluded = config.readExclude;
55
73
  if (!excluded?.length)
@@ -97,14 +115,50 @@ function validateAliasUniqueness(joins, label) {
97
115
  seen.add(j.alias);
98
116
  }
99
117
  }
118
+ /**
119
+ * Copy of `schema` exposing only `fields`, in schema declaration order.
120
+ *
121
+ * `tableName`, `col` and `colMap` are carried over untouched: the engine resolves the
122
+ * target's configuration through `dbTables[joinSchema.tableName]`, so a narrowed relation
123
+ * must still answer to its real table name, and column mapping must keep working for the
124
+ * fields that survive.
125
+ */
126
+ function narrowSchemaToFields(schema, fields, joinField) {
127
+ const declared = Object.keys(schema.fields);
128
+ for (const field of fields) {
129
+ if (!declared.includes(field)) {
130
+ throw new Error(`buildRelation: 'fields' entry '${field}' is not a field of schema ` +
131
+ `'${schema.tableName}'. Available: ${declared.join(', ')}.`);
132
+ }
133
+ }
134
+ // The join field is what ties the fetched rows back to the main result set. Dropping it
135
+ // from the projection would hand the caller rows it cannot match to anything.
136
+ if (!fields.includes(joinField)) {
137
+ throw new Error(`buildRelation: 'fields' must include the join field '${joinField}' on ` +
138
+ `'${schema.tableName}' — it is what correlates the joined rows with the main ones.`);
139
+ }
140
+ const picked = {};
141
+ for (const field of declared) {
142
+ if (fields.includes(field))
143
+ picked[field] = schema.fields[field];
144
+ }
145
+ return {
146
+ ...schema,
147
+ fields: picked,
148
+ validation: Type.Object(picked),
149
+ partialValidation: Type.Partial(Type.Object(picked)),
150
+ };
151
+ }
100
152
  export function buildRelation(mainSchema, mainField, joinSchema, joinField, options) {
153
+ const fields = options?.fields;
101
154
  return {
102
- joinSchema,
155
+ joinSchema: fields ? narrowSchemaToFields(joinSchema, fields, joinField) : joinSchema,
103
156
  joinField,
104
157
  mainField,
105
158
  alias: options?.alias ?? joinSchema.tableName,
106
159
  selection: options?.selection ?? '*',
107
160
  unique: options?.unique ?? false,
161
+ ...(fields ? { fields: [...fields] } : {}),
108
162
  };
109
163
  }
110
164
  export function buildUpsertRule(schema, columns) {
@@ -4,8 +4,34 @@ import { ConditionBuilder, type ConditionValue, type ConditionValueOrUndefined }
4
4
  import type { ITable, SqlApiPluginOptions, TenantId, TenantScope, TenantScopeIndirect, TenantContext } from '../types.js';
5
5
  export declare function resolveTenant(options: SqlApiPluginOptions, tableConf: ITable, request: FastifyRequest): Promise<TenantContext | undefined>;
6
6
  export declare function buildTenantCondition(db: QueryClient, scope: TenantScope, tenantIds: TenantId[], ownTable: string): ConditionBuilder;
7
+ /**
8
+ * Re-target a tenant context onto a related table: the caller's tenant ids paired with that
9
+ * table's OWN scope. A child table declares its own `tenantScope`, which may differ in shape
10
+ * from the main table's, so a write to it must be checked against its own column/through-FK.
11
+ *
12
+ * Returns undefined for the two cases that must stay unscoped — no tenant context (admin) and
13
+ * a table that declares no scope — mirroring `appendJoinTenantScope` on the read side.
14
+ */
15
+ export declare function tenantForTable(tenant: TenantContext | undefined, tableConf: ITable | undefined): TenantContext | undefined;
16
+ /**
17
+ * Tenant guard as a self-contained boolean fragment, for the positions where the scope cannot
18
+ * be expressed as an extra INNER JOIN: a child DELETE, a LEFT JOIN's ON clause, and the
19
+ * correlated subquery of an aggregation. A direct scope compares the table's own column; an
20
+ * indirect one tests the through table with a correlated EXISTS rather than a join.
21
+ *
22
+ * `tableRef` is the name the scoped rows go by in that statement — the table name in a DELETE,
23
+ * the join alias in an ON clause, the subquery alias inside a correlated SELECT. Returns
24
+ * undefined without a tenant context (admin).
25
+ */
26
+ export declare function buildTenantRowGuard(db: QueryClient, tenant: TenantContext | undefined, tableRef: string): ConditionBuilder | undefined;
7
27
  export declare function buildTenantJoin(db: QueryClient, scope: TenantScopeIndirect, mainTableName: string): string;
28
+ /** Writes the tenant value into a record according to its scope.
29
+ * @testonly Exported only so unit tests can exercise it directly.
30
+ */
8
31
  export declare function injectTenantValue(record: Record<string, unknown>, scope: TenantScope, tenantIds: TenantId[]): void;
32
+ /** Checks that an indirect-scope FK really belongs to the tenant.
33
+ * @testonly Exported only so unit tests can exercise it directly.
34
+ */
9
35
  export declare function validateTenantFK(db: QueryClient, scope: TenantScopeIndirect, tenantIds: TenantId[], fkValues: unknown[]): Promise<void>;
10
36
  /**
11
37
  * Build WHERE clause for DELETE with tenant filtering.
@@ -1 +1 @@
1
- {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,KAAK,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAE/G,OAAO,KAAK,EACV,MAAM,EACN,mBAAmB,EACnB,QAAQ,EACR,WAAW,EACX,mBAAmB,EACnB,aAAa,EACd,MAAM,aAAa,CAAC;AAMrB,wBAAsB,aAAa,CACjC,OAAO,EAAE,mBAAmB,EAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CASpC;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,EACrB,QAAQ,EAAE,MAAM,GACf,gBAAgB,CAgBlB;AAED,wBAAgB,eAAe,CAC7B,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,aAAa,EAAE,MAAM,GACpB,MAAM,CAMR;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,GACpB,IAAI,CAiBN;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,SAAS,EAAE,QAAQ,EAAE,EACrB,QAAQ,EAAE,OAAO,EAAE,GAClB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,WAAW,EACf,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GAAG,cAAc,EAAE,EACrD,MAAM,EAAE,aAAa,GACpB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAsBtC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EAAE,EACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACjC,OAAO,CAAC,IAAI,CAAC,CA2Df;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACpC,OAAO,CAAC,IAAI,CAAC,CAOf;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,WAAW,GACjB,IAAI,CAGN;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACjC,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,GAChC,gBAAgB,GAAG,SAAS,CAM9B;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,OAAO,EAAE,cAAc,GAAG,cAAc,EAAE,GACzC,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,cAAc,EAAE,GACpB,OAAO,CAAC,IAAI,CAAC,CAgBf"}
1
+ {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,KAAK,cAAc,EAAE,KAAK,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAE/G,OAAO,KAAK,EACV,MAAM,EACN,mBAAmB,EACnB,QAAQ,EACR,WAAW,EACX,mBAAmB,EACnB,aAAa,EACd,MAAM,aAAa,CAAC;AAMrB,wBAAsB,aAAa,CACjC,OAAO,EAAE,mBAAmB,EAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CASpC;AAED,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,EACrB,QAAQ,EAAE,MAAM,GACf,gBAAgB,CAgBlB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,aAAa,GAAG,SAAS,CAI3B;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,QAAQ,EAAE,MAAM,GACf,gBAAgB,GAAG,SAAS,CAmB9B;AAED,wBAAgB,eAAe,CAC7B,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,aAAa,EAAE,MAAM,GACpB,MAAM,CAMR;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,QAAQ,EAAE,GACpB,IAAI,CAiBN;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,mBAAmB,EAC1B,SAAS,EAAE,QAAQ,EAAE,EACrB,QAAQ,EAAE,OAAO,EAAE,GAClB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,WAAW,EACf,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GAAG,cAAc,EAAE,EACrD,MAAM,EAAE,aAAa,GACpB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAsBtC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EAAE,EACtB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACjC,OAAO,CAAC,IAAI,CAAC,CA2Df;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACpC,OAAO,CAAC,IAAI,CAAC,CAOf;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,WAAW,GACjB,IAAI,CAGN;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACjC,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,GAChC,gBAAgB,GAAG,SAAS,CAM9B;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,OAAO,EAAE,cAAc,GAAG,cAAc,EAAE,GACzC,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,cAAc,EAAE,GACpB,OAAO,CAAC,IAAI,CAAC,CAgBf"}
@@ -30,12 +30,55 @@ export function buildTenantCondition(db, scope, tenantIds, ownTable) {
30
30
  cb.isIn(qualifier, tenantIds);
31
31
  return cb;
32
32
  }
33
+ /**
34
+ * Re-target a tenant context onto a related table: the caller's tenant ids paired with that
35
+ * table's OWN scope. A child table declares its own `tenantScope`, which may differ in shape
36
+ * from the main table's, so a write to it must be checked against its own column/through-FK.
37
+ *
38
+ * Returns undefined for the two cases that must stay unscoped — no tenant context (admin) and
39
+ * a table that declares no scope — mirroring `appendJoinTenantScope` on the read side.
40
+ */
41
+ export function tenantForTable(tenant, tableConf) {
42
+ const scope = tableConf?.tenantScope;
43
+ if (!tenant || !scope)
44
+ return undefined;
45
+ return { ids: tenant.ids, scope };
46
+ }
47
+ /**
48
+ * Tenant guard as a self-contained boolean fragment, for the positions where the scope cannot
49
+ * be expressed as an extra INNER JOIN: a child DELETE, a LEFT JOIN's ON clause, and the
50
+ * correlated subquery of an aggregation. A direct scope compares the table's own column; an
51
+ * indirect one tests the through table with a correlated EXISTS rather than a join.
52
+ *
53
+ * `tableRef` is the name the scoped rows go by in that statement — the table name in a DELETE,
54
+ * the join alias in an ON clause, the subquery alias inside a correlated SELECT. Returns
55
+ * undefined without a tenant context (admin).
56
+ */
57
+ export function buildTenantRowGuard(db, tenant, tableRef) {
58
+ if (!tenant)
59
+ return undefined;
60
+ const cb = new ConditionBuilder('AND', db.cbDialect);
61
+ if (!isIndirect(tenant.scope)) {
62
+ cb.append(buildTenantCondition(db, tenant.scope, tenant.ids, tableRef));
63
+ return cb;
64
+ }
65
+ const { through, column } = tenant.scope;
66
+ const throughRef = db.qi(through.schema.tableName);
67
+ const markers = tenant.ids.map(() => '?').join(', ');
68
+ cb.raw(`EXISTS (SELECT 1 FROM ${throughRef} WHERE ` +
69
+ `${throughRef}.${db.qi(through.foreignField)} = ${db.qi(tableRef)}.${db.qi(through.localField)} AND ` +
70
+ `${throughRef}.${db.qi(column)} IN (${markers}))`, tenant.ids);
71
+ return cb;
72
+ }
33
73
  export function buildTenantJoin(db, scope, mainTableName) {
34
74
  const throughTable = scope.through.schema.tableName;
35
75
  const localField = scope.through.localField;
36
76
  const foreignField = scope.through.foreignField;
37
77
  return `INNER JOIN ${db.qi(throughTable)} ON ${db.qi(mainTableName)}.${db.qi(localField)} = ${db.qi(throughTable)}.${db.qi(foreignField)}`;
38
78
  }
79
+ /** Writes the tenant value into a record according to its scope.
80
+ * @testonly Exported only so unit tests can exercise it directly.
81
+ */
39
82
  export function injectTenantValue(record, scope, tenantIds) {
40
83
  if (isIndirect(scope))
41
84
  return;
@@ -52,6 +95,9 @@ export function injectTenantValue(record, scope, tenantIds) {
52
95
  }
53
96
  throw httpError(400, 'Ambiguous tenant: specify the tenant value');
54
97
  }
98
+ /** Checks that an indirect-scope FK really belongs to the tenant.
99
+ * @testonly Exported only so unit tests can exercise it directly.
100
+ */
55
101
  export async function validateTenantFK(db, scope, tenantIds, fkValues) {
56
102
  if (!fkValues.length)
57
103
  return;
@@ -1,7 +1,5 @@
1
1
  import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
2
2
  import type { ITable, SqlApiPluginOptions, DbTables, TableOperation } from '../../types.js';
3
- type RequestHook = (request: FastifyRequest, reply: FastifyReply) => Promise<void | FastifyReply>;
4
- export declare function mergeOnRequests(options: SqlApiPluginOptions, tableConf: ITable): RequestHook[];
5
3
  export declare function buildWriteDescription(action: string, tableName: string, tableConf: ITable): string;
6
4
  interface RouteSchema {
7
5
  body?: any;
@@ -31,11 +29,6 @@ export interface AutoRouteSpec {
31
29
  * the registrar will reply.status(successStatus).send(returnValue). */
32
30
  handle: (fastify: FastifyInstance, tableName: string, tableConf: ITable, request: FastifyRequest, reply: FastifyReply) => Promise<unknown>;
33
31
  }
34
- /**
35
- * Iterates over every table in `options.DbTables` and registers one Fastify route for it
36
- * following `spec`. Centralizes onRequest hook merging, tags, schema response wrapping,
37
- * default reply when handler returns a value.
38
- */
39
32
  export declare function registerForAllTables(fastify: FastifyInstance, options: SqlApiPluginOptions, spec: AutoRouteSpec): Promise<void>;
40
33
  export {};
41
34
  //# sourceMappingURL=route-helpers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"route-helpers.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/route-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG7E,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE5F,KAAK,WAAW,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC,CAAC;AAElG,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,EAAE,CAE9F;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAQlG;AAED,UAAU,WAAW;IAEnB,IAAI,CAAC,EAAE,GAAG,CAAC;IAEX,MAAM,CAAC,EAAE,GAAG,CAAC;IAEb,WAAW,CAAC,EAAE,GAAG,CAAC;IAElB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,iGAAiG;IACjG,SAAS,EAAE,cAAc,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC1C,gFAAgF;IAChF,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC,aAAa,EAAE,GAAG,GAAG,GAAG,CAAC;IACzB,wIAAwI;IACxI,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,WAAW,CAAC;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC;IACpF;2EACuE;IACvE,MAAM,EAAE,CACN,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,YAAY,KAChB,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAkDf"}
1
+ {"version":3,"file":"route-helpers.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/route-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG7E,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAQ5F,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAQlG;AAED,UAAU,WAAW;IAEnB,IAAI,CAAC,EAAE,GAAG,CAAC;IAEX,MAAM,CAAC,EAAE,GAAG,CAAC;IAEb,WAAW,CAAC,EAAE,GAAG,CAAC;IAElB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,iGAAiG;IACjG,SAAS,EAAE,cAAc,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC1C,gFAAgF;IAChF,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC,aAAa,EAAE,GAAG,GAAG,GAAG,CAAC;IACzB,wIAAwI;IACxI,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,WAAW,CAAC;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC;IACpF;2EACuE;IACvE,MAAM,EAAE,CACN,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,YAAY,KAChB,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AA6DD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAuBf"}