fastify-auto-sqlapi 0.1.14 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/AGENTS_BACKEND.md +52 -7
  2. package/BREAKING_CHANGES.md +86 -5
  3. package/CHANGELOG.md +57 -0
  4. package/README.md +110 -3
  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/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/lib/agent/manifest.d.ts +3 -2
  12. package/dist/lib/agent/manifest.d.ts.map +1 -1
  13. package/dist/lib/agent/manifest.js +102 -80
  14. package/dist/lib/cli/mysql-introspect.d.ts +4 -9
  15. package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
  16. package/dist/lib/cli/mysql-introspect.js +8 -7
  17. package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
  18. package/dist/lib/cli/pg-introspect.js +0 -1
  19. package/dist/lib/cli/schema-codegen.d.ts +3 -0
  20. package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
  21. package/dist/lib/cli/schema-codegen.js +3 -0
  22. package/dist/lib/cli/tables-codegen.d.ts +8 -1
  23. package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
  24. package/dist/lib/cli/tables-codegen.js +100 -118
  25. package/dist/lib/condition-methods.d.ts +14 -4
  26. package/dist/lib/condition-methods.d.ts.map +1 -1
  27. package/dist/lib/condition-methods.js +18 -4
  28. package/dist/lib/db.d.ts +7 -2
  29. package/dist/lib/db.d.ts.map +1 -1
  30. package/dist/lib/db.js +24 -6
  31. package/dist/lib/dialect.d.ts +2 -1
  32. package/dist/lib/dialect.d.ts.map +1 -1
  33. package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
  34. package/dist/lib/engine/bulk/bulk-upsert.js +44 -36
  35. package/dist/lib/engine/rest/delete.d.ts.map +1 -1
  36. package/dist/lib/engine/rest/get.d.ts.map +1 -1
  37. package/dist/lib/engine/rest/get.js +5 -4
  38. package/dist/lib/engine/rest/insert.d.ts.map +1 -1
  39. package/dist/lib/engine/rest/insert.js +1 -1
  40. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  41. package/dist/lib/engine/rest/update.js +26 -19
  42. package/dist/lib/engine/search/aggregations.d.ts +33 -0
  43. package/dist/lib/engine/search/aggregations.d.ts.map +1 -0
  44. package/dist/lib/engine/search/aggregations.js +109 -0
  45. package/dist/lib/engine/search/conditions.d.ts +31 -0
  46. package/dist/lib/engine/search/conditions.d.ts.map +1 -0
  47. package/dist/lib/engine/search/conditions.js +131 -0
  48. package/dist/lib/engine/search/fields.d.ts +49 -0
  49. package/dist/lib/engine/search/fields.d.ts.map +1 -0
  50. package/dist/lib/engine/search/fields.js +142 -0
  51. package/dist/lib/engine/search/join-families.d.ts +35 -0
  52. package/dist/lib/engine/search/join-families.d.ts.map +1 -0
  53. package/dist/lib/engine/search/join-families.js +345 -0
  54. package/dist/lib/engine/search/joins.d.ts +55 -0
  55. package/dist/lib/engine/search/joins.d.ts.map +1 -0
  56. package/dist/lib/engine/search/joins.js +81 -0
  57. package/dist/lib/engine/search/order-by.d.ts +35 -0
  58. package/dist/lib/engine/search/order-by.d.ts.map +1 -0
  59. package/dist/lib/engine/search/order-by.js +104 -0
  60. package/dist/lib/engine/search/search.d.ts.map +1 -1
  61. package/dist/lib/engine/search/search.js +106 -791
  62. package/dist/lib/engine/validate.d.ts +0 -5
  63. package/dist/lib/engine/validate.d.ts.map +1 -1
  64. package/dist/lib/engine/write-helpers.d.ts +25 -17
  65. package/dist/lib/engine/write-helpers.d.ts.map +1 -1
  66. package/dist/lib/engine/write-helpers.js +86 -44
  67. package/dist/lib/errors.d.ts +24 -0
  68. package/dist/lib/errors.d.ts.map +1 -1
  69. package/dist/lib/errors.js +59 -0
  70. package/dist/lib/read-access.d.ts +12 -1
  71. package/dist/lib/read-access.d.ts.map +1 -1
  72. package/dist/lib/read-access.js +20 -4
  73. package/dist/lib/schema/helpers.d.ts +0 -15
  74. package/dist/lib/schema/helpers.d.ts.map +1 -1
  75. package/dist/lib/schema/helpers.js +7 -4
  76. package/dist/lib/schema/search.d.ts.map +1 -1
  77. package/dist/lib/schema/search.js +107 -128
  78. package/dist/lib/sql-api.d.ts.map +1 -1
  79. package/dist/lib/table-helpers.d.ts.map +1 -1
  80. package/dist/lib/table-helpers.js +48 -0
  81. package/dist/lib/tenant.d.ts +66 -5
  82. package/dist/lib/tenant.d.ts.map +1 -1
  83. package/dist/lib/tenant.js +209 -48
  84. package/dist/routes/auto/plugin.d.ts.map +1 -1
  85. package/dist/routes/auto/plugin.js +6 -0
  86. package/dist/routes/auto/route-helpers.d.ts +0 -7
  87. package/dist/routes/auto/route-helpers.d.ts.map +1 -1
  88. package/dist/routes/auto/route-helpers.js +58 -33
  89. package/dist/routes/auto/search.routes.js +0 -1
  90. package/dist/types/plugin.d.ts +19 -0
  91. package/dist/types/plugin.d.ts.map +1 -1
  92. package/dist/types/table.d.ts +1 -1
  93. package/dist/types/tenant.d.ts +26 -2
  94. package/dist/types/tenant.d.ts.map +1 -1
  95. package/docs/GLOSSARY.md +30 -0
  96. package/docs/adr/0006-raw-db-errors.md +2 -1
  97. package/docs/adr/0010-joins-do-not-run-route-guards.md +4 -1
  98. package/docs/adr/0012-anti-duplication-tooling.md +59 -0
  99. package/docs/adr/0013-sanitized-db-errors.md +114 -0
  100. package/docs/adr/README.md +4 -1
  101. 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`. */
@@ -24,6 +24,100 @@ function readableFields(schema, tableConf) {
24
24
  function filtersObject(fields) {
25
25
  return Type.Partial(Type.Object(fields));
26
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
+ }
27
121
  const JoinGroupResultItem = Type.Object({
28
122
  sum: Type.Optional(Type.Record(Type.String(), Type.Any())),
29
123
  min: Type.Optional(Type.Record(Type.String(), Type.Any())),
@@ -42,113 +136,28 @@ export function SearchTableBodyPost(dbTables, tableName) {
42
136
  method: methodEnum,
43
137
  params: Type.Optional(Type.Array(Type.Any())),
44
138
  });
45
- // Build a TypeBox map of computed-field types (declared in defineTable).
46
- const computedTypes = {};
47
- if (tableConf.computedFields) {
48
- for (const [name, fn] of Object.entries(tableConf.computedFields)) {
49
- // Cheap dry-run with a stub context to extract the declared `type`.
50
- // The fn is pure-functional w.r.t. ctx — only `type` is read here.
51
- try {
52
- const stub = fn({
53
- db: { qi: (s) => s, dialectName: 'postgres' },
54
- qiCol: () => '""',
55
- });
56
- if (stub.type)
57
- computedTypes[name] = stub.type;
58
- }
59
- catch {
60
- // ignore — Swagger schema generation is best-effort; runtime validates.
61
- }
62
- }
63
- }
139
+ const computedTypes = computedFieldTypes(tableConf);
64
140
  const filterFields = {
65
141
  ...readableFields(schema, tableConf),
66
142
  ...tableConf.extraFilters,
67
143
  ...computedTypes,
68
144
  };
69
145
  const filtersSchema = Type.Optional(filtersObject(filterFields));
70
- // Per-alias entries split by unique flag:
71
- // - unique:false → joinMustExist / joinMultiple / joinGroup
72
- // - unique:true → joinLeft
73
- const joinMustExistProps = {};
74
- const joinMultipleProps = {};
75
- const joinGroupProps = {};
76
- const joinLeftProps = {};
77
- if (tableConf.allowedReadJoins) {
78
- for (const joinDef of tableConf.allowedReadJoins) {
79
- const { joinSchema, alias, unique } = joinDef;
80
- const joinTableConf = dbTables[joinSchema.tableName];
81
- const joinComputedTypes = {};
82
- if (joinTableConf?.computedFields) {
83
- for (const [name, fn] of Object.entries(joinTableConf.computedFields)) {
84
- try {
85
- const stub = fn({
86
- db: { qi: (s) => s, dialectName: 'postgres' },
87
- qiCol: () => '""',
88
- });
89
- if (stub.type)
90
- joinComputedTypes[name] = stub.type;
91
- }
92
- catch {
93
- // best-effort
94
- }
95
- }
96
- }
97
- const joinSchemaFields = joinTableConf
98
- ? { ...readableFields(joinSchema, joinTableConf), ...joinComputedTypes }
99
- : { ...joinSchema.fields };
100
- // `extraFilters` are only honoured where the engine delegates to the join table's
101
- // own `filters()` — buildJoinRefCondition, i.e. the unique:false paths. joinLeft
102
- // builds its condition inline (buildLeftJoinClauses) and handles schema and computed
103
- // fields only, so advertising extraFilters there would promise a silent no-op.
104
- const joinFilterFields = { ...joinSchemaFields, ...(joinTableConf?.extraFilters ?? {}) };
105
- const joinRefShape = {
106
- filters: Type.Optional(filtersObject(joinFilterFields)),
107
- conditions: Type.Optional(Type.Array(conditionItemSchema)),
108
- };
109
- if (unique) {
110
- joinLeftProps[alias] = Type.Object({
111
- filters: Type.Optional(filtersObject(joinSchemaFields)),
112
- conditions: Type.Optional(Type.Array(conditionItemSchema)),
113
- selection: Type.Optional(Type.String()),
114
- });
115
- }
116
- else {
117
- joinMustExistProps[alias] = Type.Object(joinRefShape);
118
- joinMultipleProps[alias] = Type.Object({
119
- ...joinRefShape,
120
- selection: Type.Optional(Type.String()),
121
- });
122
- joinGroupProps[alias] = Type.Object({
123
- aggregations: Type.Object({
124
- by: Type.Optional(Type.String()),
125
- distinctCount: Type.Optional(Type.Array(Type.String())),
126
- min: Type.Optional(Type.Array(Type.String())),
127
- max: Type.Optional(Type.Array(Type.String())),
128
- sum: Type.Optional(Type.Array(Type.String())),
129
- avg: Type.Optional(Type.Array(Type.String())),
130
- count: Type.Optional(Type.Array(Type.String())),
131
- }),
132
- filters: Type.Optional(filtersObject(joinFilterFields)),
133
- conditions: Type.Optional(Type.Array(conditionItemSchema)),
134
- });
135
- }
136
- }
137
- }
146
+ const joins = buildJoinBodyProps(dbTables, tableConf, conditionItemSchema);
138
147
  const bodyProperties = {
139
148
  filters: filtersSchema,
140
- conditions: Type.Optional(Type.Array(conditionItemSchema)),
149
+ conditions: Type.Optional(Type.Array(conditionItemSchema, { maxItems: MAX_CONDITIONS })),
141
150
  };
142
151
  if (Object.keys(computedTypes).length > 0) {
143
152
  bodyProperties.selectComputed = Type.Optional(Type.Array(Type.String()));
144
153
  }
145
- if (Object.keys(joinMustExistProps).length > 0) {
146
- bodyProperties.joinMustExist = Type.Optional(Type.Partial(Type.Object(joinMustExistProps)));
147
- bodyProperties.joinMultiple = Type.Optional(Type.Partial(Type.Object(joinMultipleProps)));
148
- 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)));
149
158
  }
150
- if (Object.keys(joinLeftProps).length > 0) {
151
- 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)));
152
161
  }
153
162
  return Type.Object(bodyProperties);
154
163
  }
@@ -162,7 +171,7 @@ export function SearchTableBodyPost(dbTables, tableName) {
162
171
  */
163
172
  export function SearchTableQuery(maxItemsPerPage = DEFAULT_MAX_ITEMS_PER_PAGE) {
164
173
  return Type.Object({
165
- orderBy: Type.Optional(Type.String()),
174
+ orderBy: Type.Optional(Type.String({ maxLength: MAX_ORDER_BY_LENGTH })),
166
175
  page: Type.Optional(Type.Integer({ minimum: 1 })),
167
176
  itemsPerPage: Type.Optional(Type.Integer({
168
177
  minimum: 1,
@@ -182,42 +191,12 @@ export function SearchTableResponse(dbTables, tableName) {
182
191
  const tableConf = dbTables[tableName];
183
192
  // Computed fields are present in main rows only when explicitly listed in
184
193
  // request body's selectComputed, so they're optional/Partial here.
185
- const computedTypes = {};
186
- if (tableConf.computedFields) {
187
- for (const [name, fn] of Object.entries(tableConf.computedFields)) {
188
- try {
189
- const stub = fn({
190
- db: { qi: (s) => s, dialectName: 'postgres' },
191
- qiCol: () => '""',
192
- });
193
- if (stub.type)
194
- computedTypes[name] = stub.type;
195
- }
196
- catch {
197
- // best-effort
198
- }
199
- }
200
- }
194
+ const computedTypes = computedFieldTypes(tableConf);
201
195
  const mainReadable = readableFields(tableConf.Schema, tableConf);
202
196
  const mainItem = Object.keys(computedTypes).length > 0
203
197
  ? Type.Partial(Type.Object({ ...mainReadable, ...computedTypes }))
204
198
  : Type.Partial(Type.Object(mainReadable));
205
- const joinMultipleProps = {};
206
- const joinLeftProps = {};
207
- const joinGroupProps = {};
208
- if (tableConf.allowedReadJoins) {
209
- for (const joinDef of tableConf.allowedReadJoins) {
210
- const { joinSchema, alias, unique } = joinDef;
211
- const itemArray = Type.Array(Type.Partial(Type.Object(readableFields(joinSchema, dbTables[joinSchema.tableName]))));
212
- if (unique) {
213
- joinLeftProps[alias] = itemArray;
214
- }
215
- else {
216
- joinMultipleProps[alias] = itemArray;
217
- joinGroupProps[alias] = JoinGroupResultItem;
218
- }
219
- }
220
- }
199
+ const { joinMultipleProps, joinLeftProps, joinGroupProps } = buildJoinResponseProps(dbTables, tableConf);
221
200
  const responseProps = {
222
201
  table: Type.String(),
223
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 +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,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
+ {"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,CAQX;AAoJD,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"}
@@ -50,8 +50,56 @@ export function defineTable(config) {
50
50
  validateWriteJoinsUnrestricted(config.allowedWriteJoins);
51
51
  validateComputedFields(config);
52
52
  validateReadExclude(config);
53
+ validateTenantScope(config);
53
54
  return config;
54
55
  }
56
+ /**
57
+ * `tenantScope` has three forms and they do not mix: `{ column }`, `{ column, through }`, and
58
+ * `{ anyOf }`. A config naming more than one form is ambiguous rather than additive — an `anyOf`
59
+ * entry reached through a parent FK would need its own join per entry — and an empty `anyOf`
60
+ * would fail closed on every row, which looks like a broken deployment rather than a rule.
61
+ *
62
+ * Column *names* are not checked against the schema: a tenant column may legitimately be kept
63
+ * out of the generated schema (see ADR 0009) while still scoping the table.
64
+ */
65
+ function validateTenantScope(config) {
66
+ const scope = config.tenantScope;
67
+ if (!scope)
68
+ return;
69
+ const table = config.Schema.tableName;
70
+ const hasAnyOf = 'anyOf' in scope;
71
+ const hasColumn = 'column' in scope;
72
+ if (hasAnyOf && hasColumn) {
73
+ throw new Error(`defineTable: tenantScope on table '${table}' declares both 'anyOf' and 'column'. ` +
74
+ `Use one form: 'column' (owner column on this table), 'column' + 'through' (owner ` +
75
+ `reached via a FK), or 'anyOf' (several owner columns, visible to any of them).`);
76
+ }
77
+ if (hasAnyOf && 'through' in scope) {
78
+ throw new Error(`defineTable: tenantScope on table '${table}' combines 'anyOf' with 'through'. ` +
79
+ `An 'anyOf' entry reached through a parent is not supported — declare the scope on ` +
80
+ `the parent table instead.`);
81
+ }
82
+ if (hasAnyOf) {
83
+ const { anyOf } = scope;
84
+ if (!Array.isArray(anyOf) || !anyOf.length) {
85
+ throw new Error(`defineTable: tenantScope 'anyOf' on table '${table}' must be a non-empty array of ` +
86
+ `column names. An empty list would hide every row of the table.`);
87
+ }
88
+ if (anyOf.some((c) => typeof c !== 'string' || !c)) {
89
+ throw new Error(`defineTable: tenantScope 'anyOf' on table '${table}' must contain column names as ` +
90
+ `non-empty strings.`);
91
+ }
92
+ if (new Set(anyOf).size !== anyOf.length) {
93
+ throw new Error(`defineTable: tenantScope 'anyOf' on table '${table}' repeats a column. ` +
94
+ `Each owner column must appear once.`);
95
+ }
96
+ return;
97
+ }
98
+ if (!hasColumn || typeof scope.column !== 'string') {
99
+ throw new Error(`defineTable: tenantScope on table '${table}' declares neither 'column' nor 'anyOf'. ` +
100
+ `A scope with no owner column would filter nothing.`);
101
+ }
102
+ }
55
103
  /**
56
104
  * `fields` narrows a relation for reading; it has no meaning on a write join and would be
57
105
  * actively harmful there. Write paths resolve the secondary's upsert rule by looking the
@@ -1,15 +1,62 @@
1
1
  import type { FastifyRequest } from 'fastify';
2
2
  import type { QueryClient } from './db.js';
3
3
  import { ConditionBuilder, type ConditionValue, type ConditionValueOrUndefined } from 'node-condition-builder';
4
- import type { ITable, SqlApiPluginOptions, TenantId, TenantScope, TenantScopeIndirect, TenantContext } from '../types.js';
4
+ import type { ITable, SqlApiPluginOptions, TenantId, TenantScope, TenantScopeDirect, TenantScopeIndirect, TenantScopeAnyOf, TenantContext } from '../types.js';
5
5
  export declare function resolveTenant(options: SqlApiPluginOptions, tableConf: ITable, request: FastifyRequest): Promise<TenantContext | undefined>;
6
+ /**
7
+ * The read predicate for a scope, as a fragment every caller appends to its own WHERE.
8
+ *
9
+ * An `anyOf` scope returns an OR-mode builder — the row is visible when any listed column holds
10
+ * a tenant id — and every caller appends it, so the group keeps its own parentheses. A NULL
11
+ * column simply fails to match, which is the wanted behaviour: the other party still decides.
12
+ */
6
13
  export declare function buildTenantCondition(db: QueryClient, scope: TenantScope, tenantIds: TenantId[], ownTable: string): ConditionBuilder;
14
+ /**
15
+ * Re-target a tenant context onto a related table: the caller's tenant ids paired with that
16
+ * table's OWN scope. A child table declares its own `tenantScope`, which may differ in shape
17
+ * from the main table's, so a write to it must be checked against its own column/through-FK.
18
+ *
19
+ * Returns undefined for the two cases that must stay unscoped — no tenant context (admin) and
20
+ * a table that declares no scope — mirroring `appendJoinTenantScope` on the read side.
21
+ */
22
+ export declare function tenantForTable(tenant: TenantContext | undefined, tableConf: ITable | undefined): TenantContext | undefined;
23
+ /**
24
+ * Tenant guard as a self-contained boolean fragment, for the positions where the scope cannot
25
+ * be expressed as an extra INNER JOIN: a child DELETE, a LEFT JOIN's ON clause, and the
26
+ * correlated subquery of an aggregation. A direct scope compares the table's own column; an
27
+ * indirect one tests the through table with a correlated EXISTS rather than a join.
28
+ *
29
+ * `tableRef` is the name the scoped rows go by in that statement — the table name in a DELETE,
30
+ * the join alias in an ON clause, the subquery alias inside a correlated SELECT. Returns
31
+ * undefined without a tenant context (admin) or when the table carries no scope.
32
+ */
33
+ export declare function buildTenantRowGuard(db: QueryClient, tenant: TenantContext | undefined, tableRef: string): ConditionBuilder | undefined;
7
34
  export declare function buildTenantJoin(db: QueryClient, scope: TenantScopeIndirect, mainTableName: string): string;
8
- export declare function injectTenantValue(record: Record<string, unknown>, scope: TenantScope, tenantIds: TenantId[]): void;
35
+ /**
36
+ * The insert-side rule for an `anyOf` scope: the payload must anchor the row itself.
37
+ *
38
+ * Nothing is auto-injected — with several owner columns the plugin cannot know which party the
39
+ * caller is meant to be, and guessing would either invent a relationship or refuse a legitimate
40
+ * one. So the record must name at least one listed column, and at least one of the named ones
41
+ * must hold a tenant id. The *other* parties are deliberately left alone: a swap request points
42
+ * at a colleague by definition, and requiring every listed column to match would make the shape
43
+ * unusable.
44
+ *
45
+ * @testonly Exported only so unit tests can exercise it directly.
46
+ */
47
+ export declare function assertTenantAnchor(record: Record<string, unknown>, scope: TenantScopeAnyOf, tenantIds: TenantId[]): void;
48
+ /** Writes the tenant value into a record according to its scope.
49
+ * @testonly Exported only so unit tests can exercise it directly.
50
+ */
51
+ export declare function injectTenantValue(record: Record<string, unknown>, scope: TenantScopeDirect | TenantScopeIndirect, tenantIds: TenantId[]): void;
52
+ /** Checks that an indirect-scope FK really belongs to the tenant.
53
+ * @testonly Exported only so unit tests can exercise it directly.
54
+ */
9
55
  export declare function validateTenantFK(db: QueryClient, scope: TenantScopeIndirect, tenantIds: TenantId[], fkValues: unknown[]): Promise<void>;
10
56
  /**
11
57
  * Build WHERE clause for DELETE with tenant filtering.
12
- * Handles both direct (simple AND) and indirect (subquery via JOIN) scopes.
58
+ * Handles both direct (simple AND) and indirect (subquery via JOIN) scopes; a tenant whose
59
+ * addressed table declares no scope yields the plain primary-key clause.
13
60
  */
14
61
  export declare function buildTenantDeleteWhere(db: QueryClient, tableName: string, pkCol: string, pkValue: ConditionValueOrUndefined | ConditionValue[], tenant: TenantContext): {
15
62
  where: string;
@@ -38,8 +85,9 @@ export declare function assertTenantOwnsConflicts(db: QueryClient, tenant: Tenan
38
85
  */
39
86
  export declare function enforceTenantFKOnUpdate(db: QueryClient, tenant: TenantContext | undefined, updateFields: Record<string, unknown>): Promise<void>;
40
87
  /**
41
- * Mutates `fields` in-place: removes the tenant column from a SET payload (direct scopes only).
42
- * No-op for indirect or no scope.
88
+ * Mutates `fields` in-place: removes the tenant columns from a SET payload (scopes whose columns
89
+ * live on the table itself). An `anyOf` scope drops *every* listed column — neither party can be
90
+ * reassigned, the same spirit as "a tenant cannot move its own row". No-op for indirect scopes.
43
91
  */
44
92
  export declare function stripTenantColumn(fields: Record<string, unknown>, scope: TenantScope): void;
45
93
  /**
@@ -49,6 +97,19 @@ export declare function stripTenantColumn(fields: Record<string, unknown>, scope
49
97
  * Throws 400 (ambiguous tenant) or 403 (mismatch) on violation. No-op when `tenant` is undefined.
50
98
  */
51
99
  export declare function enforceTenantOnWrites(db: QueryClient, tenant: TenantContext | undefined, records: Record<string, unknown>[]): Promise<void>;
100
+ /**
101
+ * Columns an UPSERT must leave alone when it lands on an existing row.
102
+ *
103
+ * `stripTenantColumn` does this for a plain UPDATE, but an upsert's payload doubles as the
104
+ * INSERT branch, where the owner columns are exactly what anchors a new row — they cannot simply
105
+ * be dropped. So they are excluded from the `DO UPDATE SET` instead: state which party you are
106
+ * to create the row, and that statement is ignored when the row already exists.
107
+ *
108
+ * Only `anyOf` scopes need this. A direct scope injects the caller's own tenant, so writing it
109
+ * back onto a row the conflict guard already proved theirs is a no-op; an indirect one keeps its
110
+ * owner on another table entirely.
111
+ */
112
+ export declare function tenantImmutableColumns(tenant: TenantContext | undefined): string[];
52
113
  /**
53
114
  * For update: builds the optional `extraCondition` ConditionBuilder for direct tenants.
54
115
  * Returns undefined for indirect or no-tenant scenarios — those are handled separately
@@ -1 +1 @@
1
- {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"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"}