fastify-auto-sqlapi 0.0.5 → 0.1.2
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-delete.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-delete.js +8 -2
- 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 +14 -12
- 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 +10 -10
- 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 +30 -0
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/lib/tenant.js +78 -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/plugin.d.ts.map +1 -1
- package/dist/routes/auto/plugin.js +40 -5
- 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 +66 -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 +76 -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
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,82 @@ 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
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Assert that EVERY pk in `ids` exists and belongs to the tenant, for both direct and indirect
|
|
161
|
+
* scopes. Throws 404 if any id is missing or not owned. No-op when `tenant` is undefined or `ids`
|
|
162
|
+
* is empty. Used by the delete engines to enforce tenant ownership BEFORE running a `beforeDelete`/
|
|
163
|
+
* `beforeBulkDelete` hook, so user code never runs against rows the caller cannot access.
|
|
164
|
+
*/
|
|
165
|
+
export async function assertTenantOwnsAll(db, tenant, tableName, pkCol, ids) {
|
|
166
|
+
if (!tenant || !ids.length)
|
|
167
|
+
return;
|
|
168
|
+
const qualifiedPk = `${db.qi(tableName)}.${db.qi(pkCol)}`;
|
|
169
|
+
const cb = new ConditionBuilder('AND');
|
|
170
|
+
cb.isIn(qualifiedPk, ids);
|
|
171
|
+
cb.append(buildTenantCondition(db, tenant.scope, tenant.ids));
|
|
172
|
+
const join = isIndirect(tenant.scope) ? ` ${buildTenantJoin(db, tenant.scope, tableName)}` : '';
|
|
173
|
+
const sql = `SELECT DISTINCT ${qualifiedPk} AS pk FROM ${db.qi(tableName)}${join} WHERE ${cb.build(1, db.ph)}`;
|
|
174
|
+
const r = await db.query(sql, cb.getValues());
|
|
175
|
+
const uniqueRequested = new Set(ids.map((v) => String(v))).size;
|
|
176
|
+
if (r.rows.length < uniqueRequested) {
|
|
177
|
+
throw httpError(404, 'Record not found');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -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"}
|
|
@@ -1,27 +1,21 @@
|
|
|
1
1
|
import { BulkDeleteTableBody, BulkDeleteTableResponse } from '../../lib/schema/bulk-delete.js';
|
|
2
2
|
import { primaryAsString } from '../../types.js';
|
|
3
|
-
import {
|
|
3
|
+
import { registerForAllTables } from './route-helpers.js';
|
|
4
4
|
export default async function bulkDeleteRoutes(fastify, options) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const items = request.body;
|
|
22
|
-
const ids = items.map((item) => item[primaryAsString(tableConf.primary)]);
|
|
23
|
-
reply.send(await fastify.sqlApi.bulkDelete(tableName, ids, request));
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
}
|
|
5
|
+
await registerForAllTables(fastify, options, {
|
|
6
|
+
method: 'POST',
|
|
7
|
+
url: (tc) => `/bulk/${tc.Schema.tableName}/delete`,
|
|
8
|
+
successStatus: 200,
|
|
9
|
+
schemas: (db, table) => ({
|
|
10
|
+
body: BulkDeleteTableBody(db, table),
|
|
11
|
+
response: BulkDeleteTableResponse(db, table),
|
|
12
|
+
}),
|
|
13
|
+
summary: 'Bulk delete',
|
|
14
|
+
description: (name) => `Delete multiple records from ${name} by primary key`,
|
|
15
|
+
handle: (fastify, tableName, tc, request) => {
|
|
16
|
+
const items = request.body;
|
|
17
|
+
const ids = items.map((item) => item[primaryAsString(tc.primary)]);
|
|
18
|
+
return fastify.sqlApi.bulkDelete(tableName, ids, request);
|
|
19
|
+
},
|
|
20
|
+
});
|
|
27
21
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bulk-upsert.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-upsert.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAkB,MAAM,gBAAgB,CAAC;AAE1E,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"bulk-upsert.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/bulk-upsert.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAkB,MAAM,gBAAgB,CAAC;AAE1E,wBAA8B,gBAAgB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAgBf"}
|
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
import { BulkUpsertTableBody, BulkUpsertTableResponse } from '../../lib/schema/bulk-upsert.js';
|
|
2
|
-
import {
|
|
2
|
+
import { registerForAllTables, buildWriteDescription } from './route-helpers.js';
|
|
3
3
|
export default async function bulkUpsertRoutes(fastify, options) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
handler: async (request, reply) => {
|
|
20
|
-
const items = request.body;
|
|
21
|
-
reply.send(await fastify.sqlApi.bulkUpsert(tableName, items, request));
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
}
|
|
4
|
+
await registerForAllTables(fastify, options, {
|
|
5
|
+
method: 'PUT',
|
|
6
|
+
url: (tc) => `/bulk/${tc.Schema.tableName}`,
|
|
7
|
+
successStatus: 200,
|
|
8
|
+
schemas: (db, table) => ({
|
|
9
|
+
body: BulkUpsertTableBody(db, table),
|
|
10
|
+
response: BulkUpsertTableResponse(db, table),
|
|
11
|
+
}),
|
|
12
|
+
summary: 'Bulk upsert',
|
|
13
|
+
description: (name, tc) => buildWriteDescription('Bulk upsert records in', name, tc),
|
|
14
|
+
handle: (fastify, tableName, _tc, request) => {
|
|
15
|
+
const items = request.body;
|
|
16
|
+
return fastify.sqlApi.bulkUpsert(tableName, items, request);
|
|
17
|
+
},
|
|
18
|
+
});
|
|
25
19
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/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,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"delete.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/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,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAmBf"}
|
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
import { Type } from '@sinclair/typebox';
|
|
2
2
|
import { primaryAsString } from '../../types.js';
|
|
3
|
-
import {
|
|
3
|
+
import { registerForAllTables } from './route-helpers.js';
|
|
4
4
|
export default async function deleteRoutes(fastify, options) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
fastify.route({
|
|
13
|
-
method: 'DELETE',
|
|
14
|
-
url: `/rest/${tableConf.Schema.tableName}/:id`,
|
|
15
|
-
schema: {
|
|
5
|
+
await registerForAllTables(fastify, options, {
|
|
6
|
+
method: 'DELETE',
|
|
7
|
+
url: (tc) => `/rest/${tc.Schema.tableName}/:id`,
|
|
8
|
+
successStatus: 200,
|
|
9
|
+
schemas: (_db, _table, tc) => {
|
|
10
|
+
const pkField = primaryAsString(tc.primary);
|
|
11
|
+
return {
|
|
16
12
|
params: Type.Object({ id: Type.String() }),
|
|
17
|
-
response: {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
});
|
|
28
|
-
}
|
|
13
|
+
response: Type.Object({ main: Type.Object({ [pkField]: tc.Schema.fields[pkField] }) }),
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
summary: 'Delete',
|
|
17
|
+
description: (name) => `Delete a record from ${name} by primary key`,
|
|
18
|
+
handle: (fastify, tableName, _tc, request) => {
|
|
19
|
+
const { id } = request.params;
|
|
20
|
+
return fastify.sqlApi.delete(tableName, id, request);
|
|
21
|
+
},
|
|
22
|
+
});
|
|
29
23
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/get.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,SAAS,CACrC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"get.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/get.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,SAAS,CACrC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAgBf"}
|
|
@@ -1,26 +1,19 @@
|
|
|
1
1
|
import { Type } from '@sinclair/typebox';
|
|
2
|
-
import {
|
|
2
|
+
import { registerForAllTables } from './route-helpers.js';
|
|
3
3
|
export default async function getRoutes(fastify, options) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
onRequest: mergeOnRequests(options, tableConf),
|
|
20
|
-
handler: async (request, reply) => {
|
|
21
|
-
const { id } = request.params;
|
|
22
|
-
reply.send(await fastify.sqlApi.get(tableName, id, request));
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
}
|
|
4
|
+
await registerForAllTables(fastify, options, {
|
|
5
|
+
method: 'GET',
|
|
6
|
+
url: (tc) => `/rest/${tc.Schema.tableName}/:id`,
|
|
7
|
+
successStatus: 200,
|
|
8
|
+
schemas: (_db, _table, tc) => ({
|
|
9
|
+
params: Type.Object({ id: Type.String() }),
|
|
10
|
+
response: Type.Object({ main: Type.Partial(Type.Object(tc.Schema.fields)) }),
|
|
11
|
+
}),
|
|
12
|
+
summary: 'Get',
|
|
13
|
+
description: (name) => `Get a record from ${name} by primary key`,
|
|
14
|
+
handle: (fastify, tableName, _tc, request) => {
|
|
15
|
+
const { id } = request.params;
|
|
16
|
+
return fastify.sqlApi.get(tableName, id, request);
|
|
17
|
+
},
|
|
18
|
+
});
|
|
26
19
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"insert.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/insert.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"insert.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/insert.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAsBf"}
|
|
@@ -1,29 +1,22 @@
|
|
|
1
1
|
import { InsertTableBody, InsertTableResponse } from '../../lib/schema/insert.js';
|
|
2
|
-
import {
|
|
2
|
+
import { registerForAllTables, buildWriteDescription } from './route-helpers.js';
|
|
3
3
|
export default async function insertRoutes(fastify, options) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
record: body.main,
|
|
23
|
-
secondaries: body.secondaries,
|
|
24
|
-
}, request);
|
|
25
|
-
reply.status(201).send(result);
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
}
|
|
4
|
+
await registerForAllTables(fastify, options, {
|
|
5
|
+
method: 'POST',
|
|
6
|
+
url: (tc) => `/rest/${tc.Schema.tableName}`,
|
|
7
|
+
successStatus: 201,
|
|
8
|
+
schemas: (db, table) => ({
|
|
9
|
+
body: InsertTableBody(db, table),
|
|
10
|
+
response: InsertTableResponse(db, table),
|
|
11
|
+
}),
|
|
12
|
+
summary: 'Insert',
|
|
13
|
+
description: (name, tc) => buildWriteDescription('Insert a record into', name, tc),
|
|
14
|
+
handle: (fastify, tableName, _tc, request) => {
|
|
15
|
+
const body = request.body;
|
|
16
|
+
return fastify.sqlApi.insert(tableName, {
|
|
17
|
+
record: body.main,
|
|
18
|
+
secondaries: body.secondaries,
|
|
19
|
+
}, request);
|
|
20
|
+
},
|
|
21
|
+
});
|
|
29
22
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAO7D,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,sBAAsB,CAAC;AASjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,MAAM,EAAE,MAAM,CAAC;KAChB;CACF;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAO7D,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,sBAAsB,CAAC;AASjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,MAAM,EAAE,MAAM,CAAC;KAChB;CACF;kCAuDU,eAAe,WACf,mBAAmB,KAC3B,OAAO,CAAC,IAAI,CAAC;AAHhB,wBAiFoC"}
|
|
@@ -13,6 +13,45 @@ import updateRoutes from './update.routes.js';
|
|
|
13
13
|
import deleteRoutes from './delete.routes.js';
|
|
14
14
|
import bulkUpsertRoutes from './bulk-upsert.routes.js';
|
|
15
15
|
import bulkDeleteRoutes from './bulk-delete.routes.js';
|
|
16
|
+
// Schema combinators for which Ajv reports one error per failed branch plus a
|
|
17
|
+
// single combinator-level summary error, all targeting the same path.
|
|
18
|
+
const COMBINATOR_KEYWORDS = new Set(['anyOf', 'oneOf']);
|
|
19
|
+
/**
|
|
20
|
+
* Converts Ajv validation errors into response fields, stripping the noise Ajv
|
|
21
|
+
* emits for schema combinators.
|
|
22
|
+
*
|
|
23
|
+
* An enum modelled as `anyOf` of `const`s fails every branch when invalid, so Ajv
|
|
24
|
+
* produces one `const` error per branch plus an `anyOf` error — all on the same
|
|
25
|
+
* path. We keep only the combinator error for those paths and drop the per-branch
|
|
26
|
+
* errors, then deduplicate any remaining identical entries. `required` errors are
|
|
27
|
+
* re-pointed from the parent object to the actually missing property.
|
|
28
|
+
*/
|
|
29
|
+
function normalizeAjvErrors(errors) {
|
|
30
|
+
const toPath = (v) => {
|
|
31
|
+
const path = (v.instancePath || '').replace(/\//g, '.').replace(/^\./, '');
|
|
32
|
+
const missing = v.params?.missingProperty;
|
|
33
|
+
if (v.keyword === 'required' && missing) {
|
|
34
|
+
return path ? `${path}.${missing}` : missing;
|
|
35
|
+
}
|
|
36
|
+
return path;
|
|
37
|
+
};
|
|
38
|
+
const mapped = errors.map((v) => ({
|
|
39
|
+
path: toPath(v),
|
|
40
|
+
message: v.message || 'invalid',
|
|
41
|
+
code: v.keyword || 'unknown',
|
|
42
|
+
}));
|
|
43
|
+
const combinatorPaths = new Set(mapped.filter((f) => COMBINATOR_KEYWORDS.has(f.code)).map((f) => f.path));
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
return mapped.filter((f) => {
|
|
46
|
+
if (combinatorPaths.has(f.path) && !COMBINATOR_KEYWORDS.has(f.code))
|
|
47
|
+
return false;
|
|
48
|
+
const key = `${f.path}|${f.code}|${f.message}`;
|
|
49
|
+
if (seen.has(key))
|
|
50
|
+
return false;
|
|
51
|
+
seen.add(key);
|
|
52
|
+
return true;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
16
55
|
export default fp(async function fastifyAutoSqlApi(fastify, options) {
|
|
17
56
|
// Set ConditionBuilder dialect globally
|
|
18
57
|
const dialect = getDialect(options.dialect || 'postgres');
|
|
@@ -49,11 +88,7 @@ export default fp(async function fastifyAutoSqlApi(fastify, options) {
|
|
|
49
88
|
instance.setErrorHandler((error, request, reply) => {
|
|
50
89
|
// Schema validation errors (Ajv)
|
|
51
90
|
if (error.validation) {
|
|
52
|
-
const fields = error.validation
|
|
53
|
-
path: (v.instancePath || '').replace(/\//g, '.').replace(/^\./, ''),
|
|
54
|
-
message: v.message || 'invalid',
|
|
55
|
-
code: v.keyword || 'unknown',
|
|
56
|
-
}));
|
|
91
|
+
const fields = normalizeAjvErrors(error.validation);
|
|
57
92
|
return reply.status(400).send({
|
|
58
93
|
statusCode: 400,
|
|
59
94
|
error: 'Bad Request',
|
|
@@ -1,7 +1,32 @@
|
|
|
1
|
-
import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
2
|
-
import type { ITable, SqlApiPluginOptions } from '../../types.js';
|
|
1
|
+
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
2
|
+
import type { ITable, SqlApiPluginOptions, DbTables } from '../../types.js';
|
|
3
3
|
type RequestHook = (request: FastifyRequest, reply: FastifyReply) => Promise<void | FastifyReply>;
|
|
4
4
|
export declare function mergeOnRequests(options: SqlApiPluginOptions, tableConf: ITable): RequestHook[];
|
|
5
5
|
export declare function buildWriteDescription(action: string, tableName: string, tableConf: ITable): string;
|
|
6
|
+
interface RouteSchema {
|
|
7
|
+
body?: any;
|
|
8
|
+
params?: any;
|
|
9
|
+
querystring?: any;
|
|
10
|
+
response?: any;
|
|
11
|
+
}
|
|
12
|
+
export interface AutoRouteSpec {
|
|
13
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
14
|
+
/** URL builder receiving the table configuration to read `Schema.tableName`. */
|
|
15
|
+
url: (tableConf: ITable) => string;
|
|
16
|
+
successStatus: 200 | 201;
|
|
17
|
+
/** Build per-table schemas (body, params, querystring, response). Response is wrapped with `{ [successStatus]: ... }` automatically. */
|
|
18
|
+
schemas: (dbTables: DbTables, tableName: string, tableConf: ITable) => RouteSchema;
|
|
19
|
+
summary: string;
|
|
20
|
+
description: (tableName: string, tableConf: ITable, schemas: RouteSchema) => string;
|
|
21
|
+
/** Translates the request to a sqlApi call. If it does not call reply.send/status itself,
|
|
22
|
+
* the registrar will reply.status(successStatus).send(returnValue). */
|
|
23
|
+
handle: (fastify: FastifyInstance, tableName: string, tableConf: ITable, request: FastifyRequest, reply: FastifyReply) => Promise<unknown>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Iterates over every table in `options.DbTables` and registers one Fastify route for it
|
|
27
|
+
* following `spec`. Centralizes onRequest hook merging, tags, schema response wrapping,
|
|
28
|
+
* default reply when handler returns a value.
|
|
29
|
+
*/
|
|
30
|
+
export declare function registerForAllTables(fastify: FastifyInstance, options: SqlApiPluginOptions, spec: AutoRouteSpec): Promise<void>;
|
|
6
31
|
export {};
|
|
7
32
|
//# sourceMappingURL=route-helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-helpers.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/route-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"route-helpers.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/route-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE5E,KAAK,WAAW,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,GAAG,YAAY,CAAC,CAAC;AAElG,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,EAAE,CAE9F;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAQlG;AAED,UAAU,WAAW;IAEnB,IAAI,CAAC,EAAE,GAAG,CAAC;IAEX,MAAM,CAAC,EAAE,GAAG,CAAC;IAEb,WAAW,CAAC,EAAE,GAAG,CAAC;IAElB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC1C,gFAAgF;IAChF,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC,aAAa,EAAE,GAAG,GAAG,GAAG,CAAC;IACzB,wIAAwI;IACxI,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,WAAW,CAAC;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC;IACpF;2EACuE;IACvE,MAAM,EAAE,CACN,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,YAAY,KAChB,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CA8Bf"}
|
|
@@ -3,10 +3,48 @@ export function mergeOnRequests(options, tableConf) {
|
|
|
3
3
|
}
|
|
4
4
|
export function buildWriteDescription(action, tableName, tableConf) {
|
|
5
5
|
const joinList = tableConf.allowedWriteJoins
|
|
6
|
-
?.map((
|
|
6
|
+
?.map((j) => j.alias)
|
|
7
7
|
.join(', ');
|
|
8
8
|
return [
|
|
9
9
|
`${action} ${tableName}`,
|
|
10
10
|
joinList && `Available secondaries: ${joinList}`,
|
|
11
11
|
].filter(Boolean).join('. ');
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Iterates over every table in `options.DbTables` and registers one Fastify route for it
|
|
15
|
+
* following `spec`. Centralizes onRequest hook merging, tags, schema response wrapping,
|
|
16
|
+
* default reply when handler returns a value.
|
|
17
|
+
*/
|
|
18
|
+
export async function registerForAllTables(fastify, options, spec) {
|
|
19
|
+
const { DbTables } = options;
|
|
20
|
+
for (const [tableName, tableConf] of Object.entries(DbTables)) {
|
|
21
|
+
const schemas = spec.schemas(DbTables, tableName, tableConf);
|
|
22
|
+
// Only include schema keys that are actually defined: Fastify checks for key
|
|
23
|
+
// *presence* (not undefined value) to decide whether to emit FSTWRN001.
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
+
const schema = {
|
|
26
|
+
tags: [`SqlAPI-${tableName}`],
|
|
27
|
+
summary: `${spec.summary} ${tableName}`,
|
|
28
|
+
description: spec.description(tableName, tableConf, schemas),
|
|
29
|
+
};
|
|
30
|
+
if (schemas.body !== undefined)
|
|
31
|
+
schema.body = schemas.body;
|
|
32
|
+
if (schemas.params !== undefined)
|
|
33
|
+
schema.params = schemas.params;
|
|
34
|
+
if (schemas.querystring !== undefined)
|
|
35
|
+
schema.querystring = schemas.querystring;
|
|
36
|
+
if (schemas.response !== undefined)
|
|
37
|
+
schema.response = { [spec.successStatus]: schemas.response };
|
|
38
|
+
fastify.route({
|
|
39
|
+
method: spec.method,
|
|
40
|
+
url: spec.url(tableConf),
|
|
41
|
+
schema,
|
|
42
|
+
onRequest: mergeOnRequests(options, tableConf),
|
|
43
|
+
handler: async (request, reply) => {
|
|
44
|
+
const result = await spec.handle(fastify, tableName, tableConf, request, reply);
|
|
45
|
+
if (!reply.sent)
|
|
46
|
+
reply.status(spec.successStatus).send(result);
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/search.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"search.routes.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/search.routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAE1D,wBAA8B,YAAY,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAiDf"}
|