fastify-auto-sqlapi 0.0.4 → 0.1.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 (127) hide show
  1. package/AGENTS.md +15 -2
  2. package/AGENTS_BACKEND.md +160 -32
  3. package/AGENTS_FRONTEND.md +220 -49
  4. package/README.md +147 -27
  5. package/dist/bin/generate-schema.js +12 -29
  6. package/dist/bin/generate-tables.js +10 -33
  7. package/dist/bin/utils.d.ts +29 -1
  8. package/dist/bin/utils.d.ts.map +1 -1
  9. package/dist/bin/utils.js +71 -2
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/lib/cli/load-dependency.d.ts +8 -0
  13. package/dist/lib/cli/load-dependency.d.ts.map +1 -0
  14. package/dist/lib/cli/load-dependency.js +21 -0
  15. package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
  16. package/dist/lib/cli/mysql-introspect.js +2 -11
  17. package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
  18. package/dist/lib/cli/pg-introspect.js +2 -17
  19. package/dist/lib/cli/schema-codegen.d.ts +1 -1
  20. package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
  21. package/dist/lib/cli/schema-codegen.js +11 -4
  22. package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
  23. package/dist/lib/cli/tables-codegen.js +4 -3
  24. package/dist/lib/db.d.ts +1 -0
  25. package/dist/lib/db.d.ts.map +1 -1
  26. package/dist/lib/db.js +33 -49
  27. package/dist/lib/dialect.d.ts +17 -0
  28. package/dist/lib/dialect.d.ts.map +1 -1
  29. package/dist/lib/dialect.js +32 -0
  30. package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
  31. package/dist/lib/engine/bulk/bulk-upsert.js +26 -50
  32. package/dist/lib/engine/rest/delete.d.ts.map +1 -1
  33. package/dist/lib/engine/rest/delete.js +5 -10
  34. package/dist/lib/engine/rest/get.d.ts.map +1 -1
  35. package/dist/lib/engine/rest/get.js +4 -6
  36. package/dist/lib/engine/rest/insert.d.ts.map +1 -1
  37. package/dist/lib/engine/rest/insert.js +20 -39
  38. package/dist/lib/engine/rest/update.d.ts.map +1 -1
  39. package/dist/lib/engine/rest/update.js +35 -67
  40. package/dist/lib/engine/search/search.d.ts.map +1 -1
  41. package/dist/lib/engine/search/search.js +650 -130
  42. package/dist/lib/engine/write-helpers.d.ts +39 -2
  43. package/dist/lib/engine/write-helpers.d.ts.map +1 -1
  44. package/dist/lib/engine/write-helpers.js +58 -15
  45. package/dist/lib/errors.d.ts +11 -0
  46. package/dist/lib/errors.d.ts.map +1 -0
  47. package/dist/lib/errors.js +12 -0
  48. package/dist/lib/naming.d.ts +11 -2
  49. package/dist/lib/naming.d.ts.map +1 -1
  50. package/dist/lib/naming.js +33 -5
  51. package/dist/lib/schema/bulk-upsert.d.ts.map +1 -1
  52. package/dist/lib/schema/bulk-upsert.js +9 -25
  53. package/dist/lib/schema/helpers.d.ts +28 -1
  54. package/dist/lib/schema/helpers.d.ts.map +1 -1
  55. package/dist/lib/schema/helpers.js +38 -0
  56. package/dist/lib/schema/insert.d.ts.map +1 -1
  57. package/dist/lib/schema/insert.js +9 -19
  58. package/dist/lib/schema/search.d.ts.map +1 -1
  59. package/dist/lib/schema/search.js +147 -53
  60. package/dist/lib/schema/update.d.ts.map +1 -1
  61. package/dist/lib/schema/update.js +9 -25
  62. package/dist/lib/sql-api.d.ts +6 -6
  63. package/dist/lib/sql-api.d.ts.map +1 -1
  64. package/dist/lib/sql-api.js +8 -8
  65. package/dist/lib/table-helpers.d.ts +6 -1
  66. package/dist/lib/table-helpers.d.ts.map +1 -1
  67. package/dist/lib/table-helpers.js +51 -3
  68. package/dist/lib/tenant.d.ts +23 -0
  69. package/dist/lib/tenant.d.ts.map +1 -1
  70. package/dist/lib/tenant.js +57 -9
  71. package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
  72. package/dist/routes/auto/bulk-delete.routes.js +17 -23
  73. package/dist/routes/auto/bulk-upsert.routes.d.ts.map +1 -1
  74. package/dist/routes/auto/bulk-upsert.routes.js +16 -22
  75. package/dist/routes/auto/delete.routes.d.ts.map +1 -1
  76. package/dist/routes/auto/delete.routes.js +18 -24
  77. package/dist/routes/auto/get.routes.d.ts.map +1 -1
  78. package/dist/routes/auto/get.routes.js +16 -23
  79. package/dist/routes/auto/insert.routes.d.ts.map +1 -1
  80. package/dist/routes/auto/insert.routes.js +19 -26
  81. package/dist/routes/auto/route-helpers.d.ts +27 -2
  82. package/dist/routes/auto/route-helpers.d.ts.map +1 -1
  83. package/dist/routes/auto/route-helpers.js +39 -1
  84. package/dist/routes/auto/search.routes.d.ts.map +1 -1
  85. package/dist/routes/auto/search.routes.js +47 -47
  86. package/dist/routes/auto/update.routes.d.ts.map +1 -1
  87. package/dist/routes/auto/update.routes.js +20 -27
  88. package/dist/types/cli.d.ts +22 -0
  89. package/dist/types/cli.d.ts.map +1 -0
  90. package/dist/types/cli.js +1 -0
  91. package/dist/types/computed.d.ts +29 -0
  92. package/dist/types/computed.d.ts.map +1 -0
  93. package/dist/types/computed.js +1 -0
  94. package/dist/types/conditions.d.ts +16 -0
  95. package/dist/types/conditions.d.ts.map +1 -0
  96. package/dist/types/conditions.js +1 -0
  97. package/dist/types/db.d.ts +22 -0
  98. package/dist/types/db.d.ts.map +1 -0
  99. package/dist/types/db.js +1 -0
  100. package/dist/types/dml.d.ts +64 -0
  101. package/dist/types/dml.d.ts.map +1 -0
  102. package/dist/types/dml.js +1 -0
  103. package/dist/types/join.d.ts +44 -0
  104. package/dist/types/join.d.ts.map +1 -0
  105. package/dist/types/join.js +1 -0
  106. package/dist/types/plugin.d.ts +20 -0
  107. package/dist/types/plugin.d.ts.map +1 -0
  108. package/dist/types/plugin.js +1 -0
  109. package/dist/types/schema.d.ts +10 -0
  110. package/dist/types/schema.d.ts.map +1 -0
  111. package/dist/types/schema.js +1 -0
  112. package/dist/types/search.d.ts +45 -0
  113. package/dist/types/search.d.ts.map +1 -0
  114. package/dist/types/search.js +1 -0
  115. package/dist/types/table.d.ts +52 -0
  116. package/dist/types/table.d.ts.map +1 -0
  117. package/dist/types/table.js +7 -0
  118. package/dist/types/tenant.d.ts +19 -0
  119. package/dist/types/tenant.d.ts.map +1 -0
  120. package/dist/types/tenant.js +1 -0
  121. package/dist/types/validation.d.ts +21 -0
  122. package/dist/types/validation.d.ts.map +1 -0
  123. package/dist/types/validation.js +1 -0
  124. package/dist/types.d.ts +12 -258
  125. package/dist/types.d.ts.map +1 -1
  126. package/dist/types.js +12 -7
  127. package/package.json +8 -5
@@ -4,96 +4,190 @@ const JoinGroupResultItem = Type.Object({
4
4
  sum: Type.Optional(Type.Record(Type.String(), Type.Any())),
5
5
  min: Type.Optional(Type.Record(Type.String(), Type.Any())),
6
6
  max: Type.Optional(Type.Record(Type.String(), Type.Any())),
7
+ avg: Type.Optional(Type.Record(Type.String(), Type.Any())),
8
+ count: Type.Optional(Type.Record(Type.String(), Type.Any())),
7
9
  distinctCount: Type.Optional(Type.Record(Type.String(), Type.Any())),
8
10
  rows: Type.Optional(Type.Array(Type.Any())),
9
11
  });
10
12
  export function SearchTableBodyPost(dbTables, tableName) {
11
13
  const tableConf = dbTables[tableName];
12
14
  const schema = tableConf.Schema;
13
- // Filters: schema fields + extraFilters
15
+ const methodEnum = Type.Union(ALLOWED_METHODS.map((m) => Type.Literal(m)));
16
+ const conditionItemSchema = Type.Object({
17
+ field: Type.String(),
18
+ method: methodEnum,
19
+ params: Type.Optional(Type.Array(Type.Any())),
20
+ });
21
+ // Build a TypeBox map of computed-field types (declared in defineTable).
22
+ const computedTypes = {};
23
+ if (tableConf.computedFields) {
24
+ for (const [name, fn] of Object.entries(tableConf.computedFields)) {
25
+ // Cheap dry-run with a stub context to extract the declared `type`.
26
+ // The fn is pure-functional w.r.t. ctx — only `type` is read here.
27
+ try {
28
+ const stub = fn({
29
+ db: { qi: (s) => s, dialectName: 'postgres' },
30
+ qiCol: () => '""',
31
+ });
32
+ if (stub.type)
33
+ computedTypes[name] = stub.type;
34
+ }
35
+ catch {
36
+ // ignore — Swagger schema generation is best-effort; runtime validates.
37
+ }
38
+ }
39
+ }
14
40
  const filterFields = {
15
41
  ...schema.fields,
16
42
  ...tableConf.extraFilters,
43
+ ...computedTypes,
17
44
  };
18
45
  const filtersSchema = Type.Optional(Type.Partial(Type.Object(filterFields)));
19
- // Joins & JoinGroups from allowedReadJoins
20
- const joinProperties = {};
21
- const joinFilterProperties = {};
22
- const joinGroupProperties = {};
46
+ // Per-alias entries split by unique flag:
47
+ // - unique:false → joinMustExist / joinMultiple / joinGroup
48
+ // - unique:true → joinLeft
49
+ const joinMustExistProps = {};
50
+ const joinMultipleProps = {};
51
+ const joinGroupProps = {};
52
+ const joinLeftProps = {};
23
53
  if (tableConf.allowedReadJoins) {
24
- for (const [joinSchema] of tableConf.allowedReadJoins) {
25
- const joinTableName = joinSchema.tableName;
26
- const joinTableConf = dbTables[joinTableName];
54
+ for (const joinDef of tableConf.allowedReadJoins) {
55
+ const { joinSchema, alias, unique } = joinDef;
56
+ const joinTableConf = dbTables[joinSchema.tableName];
57
+ const joinComputedTypes = {};
58
+ if (joinTableConf?.computedFields) {
59
+ for (const [name, fn] of Object.entries(joinTableConf.computedFields)) {
60
+ try {
61
+ const stub = fn({
62
+ db: { qi: (s) => s, dialectName: 'postgres' },
63
+ qiCol: () => '""',
64
+ });
65
+ if (stub.type)
66
+ joinComputedTypes[name] = stub.type;
67
+ }
68
+ catch {
69
+ // best-effort
70
+ }
71
+ }
72
+ }
27
73
  const joinFilterFields = joinTableConf
28
- ? { ...joinSchema.fields, ...joinTableConf.extraFilters }
74
+ ? { ...joinSchema.fields, ...joinTableConf.extraFilters, ...joinComputedTypes }
29
75
  : { ...joinSchema.fields };
30
- joinProperties[joinTableName] = Type.Object({
31
- filters: Type.Optional(Type.Partial(Type.Object(joinFilterFields))),
32
- });
33
- joinFilterProperties[joinTableName] = Type.Partial(Type.Object(joinFilterFields));
34
- joinGroupProperties[joinTableName] = Type.Object({
35
- aggregations: Type.Object({
36
- by: Type.Optional(Type.String()),
37
- distinctCount: Type.Optional(Type.Array(Type.String())),
38
- min: Type.Optional(Type.Array(Type.String())),
39
- max: Type.Optional(Type.Array(Type.String())),
40
- sum: Type.Optional(Type.Array(Type.String())),
41
- }),
76
+ const joinRefShape = {
42
77
  filters: Type.Optional(Type.Partial(Type.Object(joinFilterFields))),
43
- });
78
+ conditions: Type.Optional(Type.Array(conditionItemSchema)),
79
+ };
80
+ const joinFetchShape = {
81
+ ...joinRefShape,
82
+ selection: Type.Optional(Type.String()),
83
+ };
84
+ if (unique) {
85
+ joinLeftProps[alias] = Type.Object(joinFetchShape);
86
+ }
87
+ else {
88
+ joinMustExistProps[alias] = Type.Object(joinRefShape);
89
+ joinMultipleProps[alias] = Type.Object(joinFetchShape);
90
+ joinGroupProps[alias] = Type.Object({
91
+ aggregations: Type.Object({
92
+ by: Type.Optional(Type.String()),
93
+ distinctCount: Type.Optional(Type.Array(Type.String())),
94
+ min: Type.Optional(Type.Array(Type.String())),
95
+ max: Type.Optional(Type.Array(Type.String())),
96
+ sum: Type.Optional(Type.Array(Type.String())),
97
+ avg: Type.Optional(Type.Array(Type.String())),
98
+ count: Type.Optional(Type.Array(Type.String())),
99
+ }),
100
+ filters: Type.Optional(Type.Partial(Type.Object(joinFilterFields))),
101
+ conditions: Type.Optional(Type.Array(conditionItemSchema)),
102
+ });
103
+ }
44
104
  }
45
105
  }
46
- // Conditions: advanced filters with ConditionBuilder methods
47
- const methodEnum = Type.Union(ALLOWED_METHODS.map((m) => Type.Literal(m)));
48
- const conditionItemSchema = Type.Object({
49
- field: Type.String(),
50
- method: methodEnum,
51
- params: Type.Optional(Type.Array(Type.Any())),
52
- });
53
106
  const bodyProperties = {
54
107
  filters: filtersSchema,
55
108
  conditions: Type.Optional(Type.Array(conditionItemSchema)),
56
109
  };
57
- if (Object.keys(joinProperties).length > 0) {
58
- bodyProperties.joinFilters = Type.Optional(Type.Partial(Type.Object(joinFilterProperties)));
59
- bodyProperties.joins = Type.Optional(Type.Partial(Type.Object(joinProperties)));
60
- bodyProperties.joinGroups = Type.Optional(Type.Partial(Type.Object(joinGroupProperties)));
110
+ if (Object.keys(computedTypes).length > 0) {
111
+ bodyProperties.selectComputed = Type.Optional(Type.Array(Type.String()));
112
+ }
113
+ if (Object.keys(joinMustExistProps).length > 0) {
114
+ bodyProperties.joinMustExist = Type.Optional(Type.Partial(Type.Object(joinMustExistProps)));
115
+ bodyProperties.joinMultiple = Type.Optional(Type.Partial(Type.Object(joinMultipleProps)));
116
+ bodyProperties.joinGroup = Type.Optional(Type.Partial(Type.Object(joinGroupProps)));
117
+ }
118
+ if (Object.keys(joinLeftProps).length > 0) {
119
+ bodyProperties.joinLeft = Type.Optional(Type.Partial(Type.Object(joinLeftProps)));
61
120
  }
62
121
  return Type.Object(bodyProperties);
63
122
  }
64
123
  export const SearchTableQueryString = Type.Object({
65
124
  orderBy: Type.Optional(Type.String()),
66
125
  page: Type.Optional(Type.Integer({ minimum: 1 })),
67
- itemsPerPage: Type.Optional(Type.Integer({ minimum: 1, maximum: 5000, default: 500 })),
126
+ itemsPerPage: Type.Optional(Type.Integer({ minimum: 1, default: 500 })),
68
127
  computeMin: Type.Optional(Type.String()),
69
128
  computeMax: Type.Optional(Type.String()),
70
129
  computeSum: Type.Optional(Type.String()),
71
130
  computeAvg: Type.Optional(Type.String()),
131
+ // selectComputed list goes in the body (POST), not querystring — see SearchTableBodyPost.
72
132
  });
73
133
  export function SearchTableResponse(dbTables, tableName) {
74
134
  const tableConf = dbTables[tableName];
75
- const mainItem = Type.Partial(Type.Object(tableConf.Schema.fields));
76
- const joinResponseProperties = {};
77
- const joinGroupResponseProperties = {};
135
+ // Computed fields are present in main rows only when explicitly listed in
136
+ // request body's selectComputed, so they're optional/Partial here.
137
+ const computedTypes = {};
138
+ if (tableConf.computedFields) {
139
+ for (const [name, fn] of Object.entries(tableConf.computedFields)) {
140
+ try {
141
+ const stub = fn({
142
+ db: { qi: (s) => s, dialectName: 'postgres' },
143
+ qiCol: () => '""',
144
+ });
145
+ if (stub.type)
146
+ computedTypes[name] = stub.type;
147
+ }
148
+ catch {
149
+ // best-effort
150
+ }
151
+ }
152
+ }
153
+ const mainItem = Object.keys(computedTypes).length > 0
154
+ ? Type.Partial(Type.Object({ ...tableConf.Schema.fields, ...computedTypes }))
155
+ : Type.Partial(Type.Object(tableConf.Schema.fields));
156
+ const joinMultipleProps = {};
157
+ const joinLeftProps = {};
158
+ const joinGroupProps = {};
78
159
  if (tableConf.allowedReadJoins) {
79
- for (const [joinSchema] of tableConf.allowedReadJoins) {
80
- joinResponseProperties[joinSchema.tableName] = Type.Array(Type.Partial(Type.Object(joinSchema.fields)));
81
- joinGroupResponseProperties[joinSchema.tableName] = JoinGroupResultItem;
160
+ for (const joinDef of tableConf.allowedReadJoins) {
161
+ const { joinSchema, alias, unique } = joinDef;
162
+ const itemArray = Type.Array(Type.Partial(Type.Object(joinSchema.fields)));
163
+ if (unique) {
164
+ joinLeftProps[alias] = itemArray;
165
+ }
166
+ else {
167
+ joinMultipleProps[alias] = itemArray;
168
+ joinGroupProps[alias] = JoinGroupResultItem;
169
+ }
82
170
  }
83
171
  }
84
- return Type.Object({
172
+ const responseProps = {
85
173
  table: Type.String(),
86
174
  main: Type.Array(mainItem),
87
- joins: Type.Optional(Type.Partial(Type.Object(joinResponseProperties))),
88
- joinGroups: Type.Optional(Type.Partial(Type.Object(joinGroupResponseProperties))),
89
- pagination: Type.Optional(Type.Object({
90
- total: Type.Integer(),
91
- pages: Type.Integer(),
92
- computed: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
93
- paginator: Type.Object({
94
- page: Type.Integer(),
95
- itemsPerPage: Type.Integer(),
96
- }),
97
- })),
98
- });
175
+ };
176
+ if (Object.keys(joinMultipleProps).length > 0) {
177
+ responseProps.joinMultiple = Type.Optional(Type.Partial(Type.Object(joinMultipleProps)));
178
+ responseProps.joinGroup = Type.Optional(Type.Partial(Type.Object(joinGroupProps)));
179
+ }
180
+ if (Object.keys(joinLeftProps).length > 0) {
181
+ responseProps.joinLeft = Type.Optional(Type.Partial(Type.Object(joinLeftProps)));
182
+ }
183
+ responseProps.pagination = Type.Optional(Type.Object({
184
+ total: Type.Integer(),
185
+ pages: Type.Integer(),
186
+ computed: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
187
+ paginator: Type.Object({
188
+ page: Type.Integer(),
189
+ itemsPerPage: Type.Integer(),
190
+ }),
191
+ }));
192
+ return Type.Object(responseProps);
99
193
  }
@@ -1 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAI/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAsC9E;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CA8BlF"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAgB,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAS/C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAqB9E;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAalF"}
@@ -1,7 +1,6 @@
1
1
  import { Type } from '@sinclair/typebox';
2
2
  import { primaryAsString } from '../../types.js';
3
- import { findSecondaryTableConf } from '../engine/write-helpers.js';
4
- import { pkSchema, buildSecondaryFields, applySchemaOverrides } from './helpers.js';
3
+ import { pkSchema, applySchemaOverrides, attachWriteJoinSections, writeJoinBodyFields, writeJoinResponseFields, } from './helpers.js';
5
4
  export function UpdateTableBody(dbTables, tableName) {
6
5
  const tableConf = dbTables[tableName];
7
6
  const schema = tableConf.Schema;
@@ -14,18 +13,10 @@ export function UpdateTableBody(dbTables, tableName) {
14
13
  const bodyProperties = {
15
14
  main: Type.Object(mainFields),
16
15
  };
17
- if (tableConf.allowedWriteJoins?.length) {
18
- const secondaryProperties = {};
19
- const deletionProperties = {};
20
- for (const [joinSchema, joinField] of tableConf.allowedWriteJoins) {
21
- const secondaryTableConf = findSecondaryTableConf(dbTables, joinSchema.tableName);
22
- const joinFields = buildSecondaryFields(joinSchema, joinField, secondaryTableConf);
23
- secondaryProperties[joinSchema.tableName] = Type.Array(Type.Object(joinFields));
24
- deletionProperties[joinSchema.tableName] = Type.Array(Type.Partial(Type.Object(joinSchema.fields)));
25
- }
26
- bodyProperties.secondaries = Type.Optional(Type.Partial(Type.Object(secondaryProperties)));
27
- bodyProperties.deletions = Type.Optional(Type.Partial(Type.Object(deletionProperties)));
28
- }
16
+ attachWriteJoinSections(bodyProperties, tableConf, dbTables, {
17
+ withDeletions: true,
18
+ secondaryFields: writeJoinBodyFields,
19
+ });
29
20
  return Type.Object(bodyProperties);
30
21
  }
31
22
  export function UpdateTableResponse(dbTables, tableName) {
@@ -33,16 +24,9 @@ export function UpdateTableResponse(dbTables, tableName) {
33
24
  const responseProperties = {
34
25
  main: Type.Object(pkSchema(tableConf, tableConf.Schema, primaryAsString(tableConf.primary))),
35
26
  };
36
- if (tableConf.allowedWriteJoins?.length) {
37
- const secondaryProperties = {};
38
- const deletionProperties = {};
39
- for (const [joinSchema, joinField] of tableConf.allowedWriteJoins) {
40
- const secondaryTableConf = findSecondaryTableConf(dbTables, joinSchema.tableName);
41
- secondaryProperties[joinSchema.tableName] = Type.Array(Type.Object(pkSchema(secondaryTableConf, joinSchema, joinField)));
42
- deletionProperties[joinSchema.tableName] = Type.Array(Type.Partial(Type.Object(joinSchema.fields)));
43
- }
44
- responseProperties.secondaries = Type.Optional(Type.Partial(Type.Object(secondaryProperties)));
45
- responseProperties.deletions = Type.Optional(Type.Partial(Type.Object(deletionProperties)));
46
- }
27
+ attachWriteJoinSections(responseProperties, tableConf, dbTables, {
28
+ withDeletions: true,
29
+ secondaryFields: writeJoinResponseFields,
30
+ });
47
31
  return Type.Object(responseProperties);
48
32
  }
@@ -1,21 +1,21 @@
1
1
  import type { FastifyRequest } from 'fastify';
2
2
  import { QueryClient } from './db.js';
3
3
  import { type DialectName } from './dialect.js';
4
- import type { DbTables, Queryable, FilterRecord, SearchCondition, Paginator, JoinGroupRequest, SearchResult, GetResult, InsertResult, UpdateResult, DeleteResult, BulkUpsertItem, BulkUpsertResult, BulkDeleteResult, TenantId } from '../types.js';
4
+ import type { DbTables, Queryable, FilterRecord, SearchCondition, Paginator, JoinGroupRequest, JoinRefFilter, JoinFetchRequest, SearchResult, GetResult, InsertResult, UpdateResult, DeleteResult, BulkUpsertItem, BulkUpsertResult, BulkDeleteResult, TenantId } from '../types.js';
5
5
  export interface SqlApiSearchParams {
6
6
  filters?: FilterRecord;
7
7
  conditions?: SearchCondition[];
8
- joinFilters?: Record<string, FilterRecord>;
9
- joins?: Record<string, {
10
- filters?: FilterRecord;
11
- }>;
12
- joinGroups?: Record<string, JoinGroupRequest>;
8
+ joinMustExist?: Record<string, JoinRefFilter>;
9
+ joinMultiple?: Record<string, JoinFetchRequest>;
10
+ joinGroup?: Record<string, JoinGroupRequest>;
11
+ joinLeft?: Record<string, JoinFetchRequest>;
13
12
  orderBy?: string;
14
13
  paginator?: Paginator;
15
14
  computeMin?: string;
16
15
  computeMax?: string;
17
16
  computeSum?: string;
18
17
  computeAvg?: string;
18
+ selectComputed?: string[];
19
19
  }
20
20
  export interface SqlApiInsertParams {
21
21
  record: Record<string, unknown>;
@@ -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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"}
@@ -2,6 +2,7 @@ import { ConditionBuilder } from 'node-condition-builder';
2
2
  import { createQueryClient, QueryClient } from './db.js';
3
3
  import { getDialect } from './dialect.js';
4
4
  import { resolveTenant } from './tenant.js';
5
+ import { httpError } from './errors.js';
5
6
  import { searchEngine } from './engine/search/search.js';
6
7
  import { getEngine } from './engine/rest/get.js';
7
8
  import { insertEngine } from './engine/rest/insert.js';
@@ -31,11 +32,8 @@ export class SqlApi {
31
32
  }
32
33
  getTableConf(tableName) {
33
34
  const conf = this.dbTables[tableName];
34
- if (!conf) {
35
- const err = new Error(`Table "${tableName}" not found in DbTables`);
36
- err.statusCode = 400;
37
- throw err;
38
- }
35
+ if (!conf)
36
+ throw httpError(400, `Table "${tableName}" not found in DbTables`);
39
37
  return conf;
40
38
  }
41
39
  async getTenant(tableName, request) {
@@ -52,15 +50,17 @@ export class SqlApi {
52
50
  tableConf,
53
51
  filters: params.filters,
54
52
  conditions: params.conditions,
55
- joinFilters: params.joinFilters,
56
- joins: params.joins,
57
- joinGroups: params.joinGroups,
53
+ joinMustExist: params.joinMustExist,
54
+ joinMultiple: params.joinMultiple,
55
+ joinGroup: params.joinGroup,
56
+ joinLeft: params.joinLeft,
58
57
  orderBy: params.orderBy,
59
58
  paginator: params.paginator,
60
59
  computeMin: params.computeMin,
61
60
  computeMax: params.computeMax,
62
61
  computeSum: params.computeSum,
63
62
  computeAvg: params.computeAvg,
63
+ selectComputed: params.selectComputed,
64
64
  tenant,
65
65
  });
66
66
  }
@@ -10,7 +10,12 @@ export declare function exportTableInfo<F extends Record<string, TSchema>, EF ex
10
10
  extraFilters: Record<string, TSchema>;
11
11
  };
12
12
  export declare function defineTable<F extends Record<string, TSchema>>(config: ITable<F>): ITable<F>;
13
- 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, selection?: string): JoinDefinition;
13
+ export interface BuildRelationOptions {
14
+ alias?: string;
15
+ selection?: string;
16
+ unique?: boolean;
17
+ }
18
+ 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;
14
19
  export declare function buildUpsertRule<F extends Record<string, TSchema>>(schema: SchemaDefinition<F>, columns: (string & keyof F)[]): [SchemaDefinition, string[]];
15
20
  export declare function buildUpsertRules(...rules: [SchemaDefinition, string[]][]): Map<SchemaDefinition, string[]>;
16
21
  export {};
@@ -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,EAAE,MAAM,wBAAwB,CAAC;AAC1F,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;AAGrB,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,CA0BhG;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAChB,MAAM,CAAC,CAAC,CAAC,CAEX;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,SAAS,GAAE,MAAY,GACtB,cAAc,CAEhB;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,EAAE,MAAM,wBAAwB,CAAC;AAC1F,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;AAYrB,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,CA0BhG;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAChB,MAAM,CAAC,CAAC,CAAC,CAKX;AA2CD,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,11 +1,19 @@
1
1
  import { ConditionBuilder } from 'node-condition-builder';
2
+ import { getDialect } from './dialect.js';
3
+ // Quote an identifier using the current global ConditionBuilder dialect.
4
+ // Needed so that DB columns with uppercase letters (e.g. betterauth "userId")
5
+ // are preserved on PostgreSQL (which folds unquoted identifiers to lowercase)
6
+ // and on MySQL (which is case-sensitive on Linux filesystems).
7
+ function qi(field) {
8
+ return getDialect(ConditionBuilder.DIALECT).qi(field);
9
+ }
2
10
  export function exportTableInfo(Schema, extraFilters = {}, extendedCondition) {
3
11
  const filters = (filterValues) => {
4
12
  const condition = new ConditionBuilder('AND');
5
13
  // Only auto-match real schema fields (DB columns)
6
14
  for (const field of Object.keys(Schema.fields)) {
7
15
  if (field in filterValues && filterValues[field] !== null && filterValues[field] !== undefined) {
8
- condition.isEqual(Schema.col(field), filterValues[field]);
16
+ condition.isEqual(qi(Schema.col(field)), filterValues[field]);
9
17
  }
10
18
  }
11
19
  // extraFilters are handled exclusively by extendedCondition
@@ -21,10 +29,50 @@ export function exportTableInfo(Schema, extraFilters = {}, extendedCondition) {
21
29
  return { Schema, filters, extraFilters: efRecord };
22
30
  }
23
31
  export function defineTable(config) {
32
+ validateAliasUniqueness(config.allowedReadJoins, 'allowedReadJoins');
33
+ validateAliasUniqueness(config.allowedWriteJoins, 'allowedWriteJoins');
34
+ validateComputedFields(config);
24
35
  return config;
25
36
  }
26
- export function buildRelation(mainSchema, mainField, joinSchema, joinField, selection = '*') {
27
- return [joinSchema, joinField, mainField, selection];
37
+ function validateComputedFields(config) {
38
+ const computed = config.computedFields;
39
+ if (!computed)
40
+ return;
41
+ const schemaFields = Object.keys(config.Schema.fields);
42
+ const extraKeys = Object.keys(config.extraFilters || {});
43
+ for (const name of Object.keys(computed)) {
44
+ if (schemaFields.includes(name)) {
45
+ throw new Error(`defineTable: computedFields name '${name}' collides with a schema field on ` +
46
+ `table '${config.Schema.tableName}'. Choose a different name.`);
47
+ }
48
+ if (extraKeys.includes(name)) {
49
+ throw new Error(`defineTable: computedFields name '${name}' collides with an extraFilters key on ` +
50
+ `table '${config.Schema.tableName}'. Choose a different name.`);
51
+ }
52
+ }
53
+ }
54
+ function validateAliasUniqueness(joins, label) {
55
+ if (!joins?.length)
56
+ return;
57
+ const seen = new Set();
58
+ for (const j of joins) {
59
+ if (seen.has(j.alias)) {
60
+ throw new Error(`defineTable: duplicate alias '${j.alias}' in ${label}. ` +
61
+ `When omitted, alias defaults to joinSchema.tableName — ` +
62
+ `declare an explicit alias to disambiguate.`);
63
+ }
64
+ seen.add(j.alias);
65
+ }
66
+ }
67
+ export function buildRelation(mainSchema, mainField, joinSchema, joinField, options) {
68
+ return {
69
+ joinSchema,
70
+ joinField,
71
+ mainField,
72
+ alias: options?.alias ?? joinSchema.tableName,
73
+ selection: options?.selection ?? '*',
74
+ unique: options?.unique ?? false,
75
+ };
28
76
  }
29
77
  export function buildUpsertRule(schema, columns) {
30
78
  return [schema, columns];
@@ -15,5 +15,28 @@ export declare function buildTenantDeleteWhere(db: QueryClient, tableName: strin
15
15
  where: string;
16
16
  values: unknown[];
17
17
  };
18
+ /**
19
+ * Mutates `fields` in-place: removes the tenant column from a SET payload (direct scopes only).
20
+ * No-op for indirect or no scope.
21
+ */
18
22
  export declare function stripTenantColumn(fields: Record<string, unknown>, scope: TenantScope): void;
23
+ /**
24
+ * For insert/bulk-upsert: enforce tenant on records that are about to be written.
25
+ * For direct scopes, mutates each record in-place to inject/validate the tenant column.
26
+ * For indirect scopes, batch-validates the FK values against the tenant scope.
27
+ * Throws 400 (ambiguous tenant) or 403 (mismatch) on violation. No-op when `tenant` is undefined.
28
+ */
29
+ export declare function enforceTenantOnWrites(db: QueryClient, tenant: TenantContext | undefined, records: Record<string, unknown>[]): Promise<void>;
30
+ /**
31
+ * For update: builds the optional `extraCondition` ConditionBuilder for direct tenants.
32
+ * Returns undefined for indirect or no-tenant scenarios — those are handled separately
33
+ * (indirect uses pre-check via `assertTenantOwnership`).
34
+ */
35
+ export declare function buildTenantUpdateExtra(tenant: TenantContext | undefined): ConditionBuilder | undefined;
36
+ /**
37
+ * For update with indirect tenant: pre-check ownership via `SELECT 1 ... INNER JOIN ... LIMIT 1`.
38
+ * Throws 404 if the record either does not exist or does not belong to the tenant.
39
+ * No-op for direct or no-tenant — those are enforced via `extraCondition` in the UPDATE itself.
40
+ */
41
+ export declare function assertTenantOwnership(db: QueryClient, tenant: TenantContext | undefined, tableName: string, pkCol: string, pkValue: ConditionValue): Promise<void>;
19
42
  //# sourceMappingURL=tenant.d.ts.map
@@ -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;AAC/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,GACpB,gBAAgB,CAclB;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,CAqBN;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,CA2Bf;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,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,KAAK,EAAE,WAAW,GACjB,IAAI,CAGN"}
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,GACpB,gBAAgB,CAclB;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;;;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,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,EACb,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CAUf"}
@@ -1,4 +1,5 @@
1
1
  import { ConditionBuilder } from 'node-condition-builder';
2
+ import { httpError } from './errors.js';
2
3
  function isIndirect(scope) {
3
4
  return 'through' in scope;
4
5
  }
@@ -39,9 +40,7 @@ export function injectTenantValue(record, scope, tenantIds) {
39
40
  const col = scope.column;
40
41
  if (col in record) {
41
42
  if (!tenantIds.includes(record[col])) {
42
- const err = new Error('Access denied: tenant value does not match');
43
- err.statusCode = 403;
44
- throw err;
43
+ throw httpError(403, 'Access denied: tenant value does not match');
45
44
  }
46
45
  return;
47
46
  }
@@ -49,9 +48,7 @@ export function injectTenantValue(record, scope, tenantIds) {
49
48
  record[col] = tenantIds[0];
50
49
  return;
51
50
  }
52
- const err = new Error('Ambiguous tenant: specify the tenant value');
53
- err.statusCode = 400;
54
- throw err;
51
+ throw httpError(400, 'Ambiguous tenant: specify the tenant value');
55
52
  }
56
53
  export async function validateTenantFK(db, scope, tenantIds, fkValues) {
57
54
  if (!fkValues.length)
@@ -71,9 +68,7 @@ export async function validateTenantFK(db, scope, tenantIds, fkValues) {
71
68
  `WHERE ${where}`;
72
69
  const result = await db.query(sql, values);
73
70
  if (result.rows.length > 0) {
74
- const err = new Error('Access denied: records do not belong to tenant');
75
- err.statusCode = 403;
76
- throw err;
71
+ throw httpError(403, 'Access denied: records do not belong to tenant');
77
72
  }
78
73
  }
79
74
  /**
@@ -103,8 +98,61 @@ export function buildTenantDeleteWhere(db, tableName, pkCol, pkValue, tenant) {
103
98
  const values = cb.getValues();
104
99
  return { where, values };
105
100
  }
101
+ /**
102
+ * Mutates `fields` in-place: removes the tenant column from a SET payload (direct scopes only).
103
+ * No-op for indirect or no scope.
104
+ */
106
105
  export function stripTenantColumn(fields, scope) {
107
106
  if (isIndirect(scope))
108
107
  return;
109
108
  delete fields[scope.column];
110
109
  }
110
+ /**
111
+ * For insert/bulk-upsert: enforce tenant on records that are about to be written.
112
+ * For direct scopes, mutates each record in-place to inject/validate the tenant column.
113
+ * For indirect scopes, batch-validates the FK values against the tenant scope.
114
+ * Throws 400 (ambiguous tenant) or 403 (mismatch) on violation. No-op when `tenant` is undefined.
115
+ */
116
+ export async function enforceTenantOnWrites(db, tenant, records) {
117
+ if (!tenant)
118
+ return;
119
+ if (isIndirect(tenant.scope)) {
120
+ const fkCol = tenant.scope.through.localField;
121
+ await validateTenantFK(db, tenant.scope, tenant.ids, records.map((r) => r[fkCol]));
122
+ return;
123
+ }
124
+ for (const r of records)
125
+ injectTenantValue(r, tenant.scope, tenant.ids);
126
+ }
127
+ /**
128
+ * For update: builds the optional `extraCondition` ConditionBuilder for direct tenants.
129
+ * Returns undefined for indirect or no-tenant scenarios — those are handled separately
130
+ * (indirect uses pre-check via `assertTenantOwnership`).
131
+ */
132
+ export function buildTenantUpdateExtra(tenant) {
133
+ if (!tenant || isIndirect(tenant.scope))
134
+ return undefined;
135
+ const cb = new ConditionBuilder('AND');
136
+ if (tenant.ids.length === 1)
137
+ cb.isEqual(tenant.scope.column, tenant.ids[0]);
138
+ else
139
+ cb.isIn(tenant.scope.column, tenant.ids);
140
+ return cb;
141
+ }
142
+ /**
143
+ * For update with indirect tenant: pre-check ownership via `SELECT 1 ... INNER JOIN ... LIMIT 1`.
144
+ * Throws 404 if the record either does not exist or does not belong to the tenant.
145
+ * No-op for direct or no-tenant — those are enforced via `extraCondition` in the UPDATE itself.
146
+ */
147
+ export async function assertTenantOwnership(db, tenant, tableName, pkCol, pkValue) {
148
+ if (!tenant || !isIndirect(tenant.scope))
149
+ return;
150
+ const cb = new ConditionBuilder('AND');
151
+ cb.isEqual(`${db.qi(tableName)}.${db.qi(pkCol)}`, pkValue);
152
+ cb.append(buildTenantCondition(db, tenant.scope, tenant.ids));
153
+ const sql = `SELECT 1 FROM ${db.qi(tableName)} ${buildTenantJoin(db, tenant.scope, tableName)} WHERE ${cb.build(1, db.ph)} LIMIT 1`;
154
+ const r = await db.query(sql, cb.getValues());
155
+ if (r.rows.length === 0) {
156
+ throw httpError(404, 'Record not found');
157
+ }
158
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"bulk-delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-delete.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAyBf"}
1
+ {"version":3,"file":"bulk-delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-delete.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAiBf"}