fastify-auto-sqlapi 0.0.5 → 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.
- package/AGENTS.md +15 -2
- package/AGENTS_BACKEND.md +144 -20
- package/AGENTS_FRONTEND.md +161 -67
- package/README.md +47 -26
- package/dist/bin/generate-schema.js +9 -26
- package/dist/bin/generate-tables.js +7 -30
- package/dist/bin/utils.d.ts +28 -0
- package/dist/bin/utils.d.ts.map +1 -1
- package/dist/bin/utils.js +69 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/cli/load-dependency.d.ts +8 -0
- package/dist/lib/cli/load-dependency.d.ts.map +1 -0
- package/dist/lib/cli/load-dependency.js +21 -0
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
- package/dist/lib/cli/mysql-introspect.js +2 -18
- package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
- package/dist/lib/cli/pg-introspect.js +2 -17
- package/dist/lib/db.d.ts +1 -0
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +33 -55
- package/dist/lib/dialect.d.ts +17 -0
- package/dist/lib/dialect.d.ts.map +1 -1
- package/dist/lib/dialect.js +32 -0
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.js +15 -43
- package/dist/lib/engine/rest/delete.d.ts.map +1 -1
- package/dist/lib/engine/rest/delete.js +5 -10
- package/dist/lib/engine/rest/get.d.ts.map +1 -1
- package/dist/lib/engine/rest/get.js +3 -5
- package/dist/lib/engine/rest/insert.d.ts.map +1 -1
- package/dist/lib/engine/rest/insert.js +11 -31
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +13 -46
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +553 -229
- package/dist/lib/engine/write-helpers.d.ts +39 -2
- package/dist/lib/engine/write-helpers.d.ts.map +1 -1
- package/dist/lib/engine/write-helpers.js +55 -12
- package/dist/lib/errors.d.ts +11 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +12 -0
- package/dist/lib/schema/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/schema/bulk-upsert.js +9 -25
- package/dist/lib/schema/helpers.d.ts +28 -1
- package/dist/lib/schema/helpers.d.ts.map +1 -1
- package/dist/lib/schema/helpers.js +38 -0
- package/dist/lib/schema/insert.d.ts.map +1 -1
- package/dist/lib/schema/insert.js +9 -19
- package/dist/lib/schema/search.d.ts.map +1 -1
- package/dist/lib/schema/search.js +138 -50
- package/dist/lib/schema/update.d.ts.map +1 -1
- package/dist/lib/schema/update.js +9 -25
- package/dist/lib/sql-api.d.ts +6 -6
- package/dist/lib/sql-api.d.ts.map +1 -1
- package/dist/lib/sql-api.js +8 -8
- package/dist/lib/table-helpers.d.ts +6 -1
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +42 -2
- package/dist/lib/tenant.d.ts +23 -0
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/lib/tenant.js +57 -9
- package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -1
- package/dist/routes/auto/bulk-delete.routes.js +17 -23
- package/dist/routes/auto/bulk-upsert.routes.d.ts.map +1 -1
- package/dist/routes/auto/bulk-upsert.routes.js +16 -22
- package/dist/routes/auto/delete.routes.d.ts.map +1 -1
- package/dist/routes/auto/delete.routes.js +18 -24
- package/dist/routes/auto/get.routes.d.ts.map +1 -1
- package/dist/routes/auto/get.routes.js +16 -23
- package/dist/routes/auto/insert.routes.d.ts.map +1 -1
- package/dist/routes/auto/insert.routes.js +19 -26
- package/dist/routes/auto/route-helpers.d.ts +27 -2
- package/dist/routes/auto/route-helpers.d.ts.map +1 -1
- package/dist/routes/auto/route-helpers.js +39 -1
- package/dist/routes/auto/search.routes.d.ts.map +1 -1
- package/dist/routes/auto/search.routes.js +47 -47
- package/dist/routes/auto/update.routes.d.ts.map +1 -1
- package/dist/routes/auto/update.routes.js +20 -27
- package/dist/types/cli.d.ts +22 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/cli.js +1 -0
- package/dist/types/computed.d.ts +29 -0
- package/dist/types/computed.d.ts.map +1 -0
- package/dist/types/computed.js +1 -0
- package/dist/types/conditions.d.ts +16 -0
- package/dist/types/conditions.d.ts.map +1 -0
- package/dist/types/conditions.js +1 -0
- package/dist/types/db.d.ts +22 -0
- package/dist/types/db.d.ts.map +1 -0
- package/dist/types/db.js +1 -0
- package/dist/types/dml.d.ts +64 -0
- package/dist/types/dml.d.ts.map +1 -0
- package/dist/types/dml.js +1 -0
- package/dist/types/join.d.ts +44 -0
- package/dist/types/join.d.ts.map +1 -0
- package/dist/types/join.js +1 -0
- package/dist/types/plugin.d.ts +20 -0
- package/dist/types/plugin.d.ts.map +1 -0
- package/dist/types/plugin.js +1 -0
- package/dist/types/schema.d.ts +10 -0
- package/dist/types/schema.d.ts.map +1 -0
- package/dist/types/schema.js +1 -0
- package/dist/types/search.d.ts +45 -0
- package/dist/types/search.d.ts.map +1 -0
- package/dist/types/search.js +1 -0
- package/dist/types/table.d.ts +52 -0
- package/dist/types/table.d.ts.map +1 -0
- package/dist/types/table.js +7 -0
- package/dist/types/tenant.d.ts +19 -0
- package/dist/types/tenant.d.ts.map +1 -0
- package/dist/types/tenant.js +1 -0
- package/dist/types/validation.d.ts +21 -0
- package/dist/types/validation.d.ts.map +1 -0
- package/dist/types/validation.js +1 -0
- package/dist/types.d.ts +12 -277
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +12 -7
- package/package.json +3 -3
|
@@ -4,102 +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
|
-
// Condition item shape (shared across main, joinFilters, joins, joinGroups)
|
|
14
15
|
const methodEnum = Type.Union(ALLOWED_METHODS.map((m) => Type.Literal(m)));
|
|
15
16
|
const conditionItemSchema = Type.Object({
|
|
16
17
|
field: Type.String(),
|
|
17
18
|
method: methodEnum,
|
|
18
19
|
params: Type.Optional(Type.Array(Type.Any())),
|
|
19
20
|
});
|
|
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
|
+
}
|
|
21
40
|
const filterFields = {
|
|
22
41
|
...schema.fields,
|
|
23
42
|
...tableConf.extraFilters,
|
|
43
|
+
...computedTypes,
|
|
24
44
|
};
|
|
25
45
|
const filtersSchema = Type.Optional(Type.Partial(Type.Object(filterFields)));
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
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 = {};
|
|
30
53
|
if (tableConf.allowedReadJoins) {
|
|
31
|
-
for (const
|
|
32
|
-
const
|
|
33
|
-
const joinTableConf = dbTables[
|
|
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
|
+
}
|
|
34
73
|
const joinFilterFields = joinTableConf
|
|
35
|
-
? { ...joinSchema.fields, ...joinTableConf.extraFilters }
|
|
74
|
+
? { ...joinSchema.fields, ...joinTableConf.extraFilters, ...joinComputedTypes }
|
|
36
75
|
: { ...joinSchema.fields };
|
|
37
|
-
// Shared { filters?, conditions? } shape for joinFilters, joins, and joinGroups inner filters
|
|
38
76
|
const joinRefShape = {
|
|
39
77
|
filters: Type.Optional(Type.Partial(Type.Object(joinFilterFields))),
|
|
40
78
|
conditions: Type.Optional(Type.Array(conditionItemSchema)),
|
|
41
79
|
};
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
}
|
|
57
104
|
}
|
|
58
105
|
}
|
|
59
106
|
const bodyProperties = {
|
|
60
107
|
filters: filtersSchema,
|
|
61
108
|
conditions: Type.Optional(Type.Array(conditionItemSchema)),
|
|
62
109
|
};
|
|
63
|
-
if (Object.keys(
|
|
64
|
-
bodyProperties.
|
|
65
|
-
|
|
66
|
-
|
|
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)));
|
|
67
120
|
}
|
|
68
121
|
return Type.Object(bodyProperties);
|
|
69
122
|
}
|
|
70
123
|
export const SearchTableQueryString = Type.Object({
|
|
71
124
|
orderBy: Type.Optional(Type.String()),
|
|
72
125
|
page: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
73
|
-
itemsPerPage: Type.Optional(Type.Integer({ minimum: 1,
|
|
126
|
+
itemsPerPage: Type.Optional(Type.Integer({ minimum: 1, default: 500 })),
|
|
74
127
|
computeMin: Type.Optional(Type.String()),
|
|
75
128
|
computeMax: Type.Optional(Type.String()),
|
|
76
129
|
computeSum: Type.Optional(Type.String()),
|
|
77
130
|
computeAvg: Type.Optional(Type.String()),
|
|
131
|
+
// selectComputed list goes in the body (POST), not querystring — see SearchTableBodyPost.
|
|
78
132
|
});
|
|
79
133
|
export function SearchTableResponse(dbTables, tableName) {
|
|
80
134
|
const tableConf = dbTables[tableName];
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
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 = {};
|
|
84
159
|
if (tableConf.allowedReadJoins) {
|
|
85
|
-
for (const
|
|
86
|
-
|
|
87
|
-
|
|
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
|
+
}
|
|
88
170
|
}
|
|
89
171
|
}
|
|
90
|
-
|
|
172
|
+
const responseProps = {
|
|
91
173
|
table: Type.String(),
|
|
92
174
|
main: Type.Array(mainItem),
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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);
|
|
105
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;
|
|
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 {
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
}
|
package/dist/lib/sql-api.d.ts
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,OAAO,EAAqB,WAAW,EAAE,MAAM,SAAS,CAAC;AACzD,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"sql-api.d.ts","sourceRoot":"","sources":["../../src/lib/sql-api.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/sql-api.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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;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,
|
|
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"}
|
|
@@ -29,10 +29,50 @@ export function exportTableInfo(Schema, extraFilters = {}, extendedCondition) {
|
|
|
29
29
|
return { Schema, filters, extraFilters: efRecord };
|
|
30
30
|
}
|
|
31
31
|
export function defineTable(config) {
|
|
32
|
+
validateAliasUniqueness(config.allowedReadJoins, 'allowedReadJoins');
|
|
33
|
+
validateAliasUniqueness(config.allowedWriteJoins, 'allowedWriteJoins');
|
|
34
|
+
validateComputedFields(config);
|
|
32
35
|
return config;
|
|
33
36
|
}
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
};
|
|
36
76
|
}
|
|
37
77
|
export function buildUpsertRule(schema, columns) {
|
|
38
78
|
return [schema, columns];
|
package/dist/lib/tenant.d.ts
CHANGED
|
@@ -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
|
package/dist/lib/tenant.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/lib/tenant.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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"}
|