fastify-auto-sqlapi 0.0.1 → 0.0.4
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 +56 -0
- package/AGENTS_BACKEND.md +833 -0
- package/AGENTS_FRONTEND.md +414 -0
- package/README.md +19 -1
- package/dist/bin/generate-tables.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
- package/dist/lib/cli/pg-introspect.js +8 -1
- package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
- package/dist/lib/cli/tables-codegen.js +28 -0
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.js +14 -4
- package/dist/lib/engine/rest/insert.d.ts.map +1 -1
- package/dist/lib/engine/rest/insert.js +8 -5
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +8 -5
- package/dist/lib/engine/validate.d.ts +11 -0
- package/dist/lib/engine/validate.d.ts.map +1 -0
- package/dist/lib/engine/validate.js +29 -0
- package/dist/lib/schema/bulk-upsert.js +2 -2
- package/dist/lib/schema/helpers.d.ts +5 -0
- package/dist/lib/schema/helpers.d.ts.map +1 -1
- package/dist/lib/schema/helpers.js +15 -0
- package/dist/lib/schema/insert.js +2 -2
- package/dist/lib/schema/update.d.ts.map +1 -1
- package/dist/lib/schema/update.js +4 -3
- package/dist/routes/auto/plugin.d.ts.map +1 -1
- package/dist/routes/auto/plugin.js +12 -1
- package/dist/types.d.ts +21 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# fastify-auto-sqlapi — Frontend API Usage
|
|
2
|
+
|
|
3
|
+
How to call the auto-generated CRUD endpoints.
|
|
4
|
+
|
|
5
|
+
## Generated Endpoints
|
|
6
|
+
|
|
7
|
+
The plugin defines routes with these base paths: `/search/`, `/rest/`, `/bulk/`. The prefix from `register()` is prepended by Fastify. For a table `customer` with `prefix: '/auto'`:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
POST /auto/search/customer — search with filters in body
|
|
11
|
+
GET /auto/rest/customer/:id — get single record by PK
|
|
12
|
+
POST /auto/rest/customer — insert record (+ secondaries)
|
|
13
|
+
PUT /auto/rest/customer — update record (+ secondaries + deletions)
|
|
14
|
+
DELETE /auto/rest/customer/:id — delete record by PK
|
|
15
|
+
PUT /auto/bulk/customer — bulk upsert (array of items)
|
|
16
|
+
POST /auto/bulk/customer/delete — bulk delete (array of PKs)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Without prefix, routes are at root: `/search/customer`, `/rest/customer/:id`, etc.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## POST /search/{tableName}
|
|
24
|
+
|
|
25
|
+
**Important**: Search uses `POST` (not GET) because filters are passed in the request body as JSON.
|
|
26
|
+
|
|
27
|
+
### Query parameters (all optional)
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
?orderBy=name ASC, id DESC
|
|
31
|
+
&page=1&itemsPerPage=20
|
|
32
|
+
&computeMin=id&computeMax=id&computeSum=total&computeAvg=total
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- `orderBy` — SQL ORDER BY clause (default: `defaultOrder` from table config)
|
|
36
|
+
- `page` + `itemsPerPage` — pagination (when either is present, response includes `pagination` object; `page` defaults to 1, `itemsPerPage` defaults to 500)
|
|
37
|
+
- `computeMin`, `computeMax`, `computeSum`, `computeAvg` — aggregate a column, returned in `pagination.computed`
|
|
38
|
+
|
|
39
|
+
### Body (all fields optional, empty `{}` returns all records)
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"filters": { "name": "Mario", "q": "search term" },
|
|
44
|
+
"conditions": [
|
|
45
|
+
{ "field": "createdAt", "method": "isGreater", "params": ["2024-01-01"] }
|
|
46
|
+
],
|
|
47
|
+
"joinFilters": {
|
|
48
|
+
"customer_label_link": { "labelId": 1 }
|
|
49
|
+
},
|
|
50
|
+
"joins": {
|
|
51
|
+
"customer_order": { "filters": { "status": "pending" } }
|
|
52
|
+
},
|
|
53
|
+
"joinGroups": {
|
|
54
|
+
"customer_order": {
|
|
55
|
+
"aggregations": {
|
|
56
|
+
"by": "status",
|
|
57
|
+
"sum": ["total"],
|
|
58
|
+
"min": ["total"],
|
|
59
|
+
"max": ["total"],
|
|
60
|
+
"distinctCount": ["status"]
|
|
61
|
+
},
|
|
62
|
+
"filters": { "status": "active" }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Filters
|
|
69
|
+
|
|
70
|
+
Pass any schema field as a key in `filters`. The plugin auto-applies `WHERE col = value` for each. Extra filters (defined via `extraFiltersValidation` in the table config) are also accepted but handled by custom logic.
|
|
71
|
+
|
|
72
|
+
### Conditions (advanced filters)
|
|
73
|
+
|
|
74
|
+
`conditions` allows using any ConditionBuilder method for advanced comparisons. Each condition specifies a `field`, a `method`, and `params` (values after the field).
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"filters": { "status": "active" },
|
|
79
|
+
"conditions": [
|
|
80
|
+
{ "field": "total", "method": "isGreater", "params": [100] },
|
|
81
|
+
{ "field": "total", "method": "isLess", "params": [500] },
|
|
82
|
+
{ "field": "name", "method": "isILike", "params": ["%mario%"] },
|
|
83
|
+
{ "field": "createdAt", "method": "isBetween", "params": ["2024-01-01", "2024-12-31"] },
|
|
84
|
+
{ "field": "status", "method": "isIn", "params": [["active", "pending"]] },
|
|
85
|
+
{ "field": "deletedAt", "method": "isNull", "params": [] }
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Available methods** (mapped to ConditionBuilder):
|
|
91
|
+
|
|
92
|
+
| Method | SQL | params |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `isEqual`, `isNotEqual` | `= $1` / `!= $1` | `[value]` |
|
|
95
|
+
| `isGreater`, `isGreaterOrEqual` | `> $1` / `>= $1` | `[value]` |
|
|
96
|
+
| `isLess`, `isLessOrEqual` | `< $1` / `<= $1` | `[value]` |
|
|
97
|
+
| `isLike`, `isNotLike` | `LIKE $1` | `["%pattern%"]` |
|
|
98
|
+
| `isILike`, `isNotILike` | `ILIKE $1` | `["%pattern%"]` |
|
|
99
|
+
| `isIn`, `isNotIn` | `IN (...)` | `[[val1, val2]]` |
|
|
100
|
+
| `isBetween`, `isNotBetween` | `BETWEEN $1 AND $2` | `[from, to]` |
|
|
101
|
+
| `isNull`, `isNotNull` | `IS NULL` / `IS NOT NULL` | `[]` |
|
|
102
|
+
|
|
103
|
+
Also available: `isNotGreater`, `isNotGreaterOrEqual`, `isNotLess`, `isNotLessOrEqual`.
|
|
104
|
+
|
|
105
|
+
- **Multiple conditions on the same field** — it's an array, so `total > 100 AND total < 500` is natural
|
|
106
|
+
- **Combinable** with `filters` (equality), `joinFilters` (EXISTS), pagination, etc.
|
|
107
|
+
- **Field validation** — field names are validated against the table schema (400 if unknown)
|
|
108
|
+
- **Method validation** — only whitelisted methods are allowed (`raw`, `append` etc. are blocked)
|
|
109
|
+
|
|
110
|
+
### Join Filters (filter main by related table)
|
|
111
|
+
|
|
112
|
+
`joinFilters` restrict **main table results** based on conditions on a related table. Only tables listed in `allowedReadJoins` are available. Uses `EXISTS` subquery — no duplicate rows, works correctly with pagination.
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"filters": { "name": "Mario" },
|
|
117
|
+
"joinFilters": {
|
|
118
|
+
"customer_label_link": { "labelId": 1 }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
SQL generated:
|
|
124
|
+
```sql
|
|
125
|
+
SELECT * FROM "customer"
|
|
126
|
+
WHERE "name" = $1
|
|
127
|
+
AND EXISTS (SELECT 1 FROM "customer_label_link"
|
|
128
|
+
WHERE "customer_id" = "customer"."id" AND "label_id" = $2)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`joinFilters` support the same filter fields as the related table (schema fields + extra filters defined via `extendedCondition`). Can be combined with `filters`, `joins`, `joinGroups`, and pagination.
|
|
132
|
+
|
|
133
|
+
**Key difference from `joins`**: `joinFilters` filter which main records are returned. `joins` fetch related data for the returned main records. They can be used together.
|
|
134
|
+
|
|
135
|
+
### Joins
|
|
136
|
+
|
|
137
|
+
Request related table data via `joins`. Only tables listed in `allowedReadJoins` are available. Each join key is the table name, with optional `filters` to narrow the joined results.
|
|
138
|
+
|
|
139
|
+
The join executes as a separate query: `SELECT ... FROM {joinTable} WHERE {fk} IN ({main PKs})`. Results are grouped by FK.
|
|
140
|
+
|
|
141
|
+
### Join Groups (Aggregations)
|
|
142
|
+
|
|
143
|
+
`joinGroups` compute aggregated data on related tables:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"joinGroups": {
|
|
148
|
+
"customer_order": {
|
|
149
|
+
"aggregations": {
|
|
150
|
+
"by": "status",
|
|
151
|
+
"sum": ["total"],
|
|
152
|
+
"min": ["total"],
|
|
153
|
+
"max": ["total"],
|
|
154
|
+
"distinctCount": ["status"]
|
|
155
|
+
},
|
|
156
|
+
"filters": { "status": "active" }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- `by` — GROUP BY field
|
|
163
|
+
- `sum`, `min`, `max` — aggregate functions on specified columns
|
|
164
|
+
- `distinctCount` — COUNT(DISTINCT col)
|
|
165
|
+
- `filters` — optional, narrow rows before aggregation
|
|
166
|
+
|
|
167
|
+
### Ordering
|
|
168
|
+
|
|
169
|
+
Use the `orderBy` query parameter:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
?orderBy=name ASC
|
|
173
|
+
?orderBy=name ASC, id DESC
|
|
174
|
+
?orderBy=created_at DESC
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
If not specified, the table's `defaultOrder` is used.
|
|
178
|
+
|
|
179
|
+
### Pagination
|
|
180
|
+
|
|
181
|
+
Add `page` or `itemsPerPage` to the query string to enable pagination:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
?page=1&itemsPerPage=20
|
|
185
|
+
?itemsPerPage=10 # page defaults to 1
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
When either is present, the response includes a `pagination` object with total count, total pages, and the paginator values. `page` defaults to 1, `itemsPerPage` defaults to 500.
|
|
189
|
+
|
|
190
|
+
### Computed values (aggregations on main table)
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
?computeMin=id&computeMax=id&computeSum=total&computeAvg=total
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
These compute aggregates on the main table's result set (respecting filters). Results appear in `pagination.computed`. Requires pagination to be active (`page` or `itemsPerPage` set).
|
|
197
|
+
|
|
198
|
+
### Response
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"table": "customer",
|
|
203
|
+
"main": [{ "id": 1, "name": "Mario", "email": "m@t.it" }],
|
|
204
|
+
"joins": {
|
|
205
|
+
"customer_order": [{ "id": 10, "customerId": 1, "total": 50 }]
|
|
206
|
+
},
|
|
207
|
+
"joinGroups": {
|
|
208
|
+
"customer_order": {
|
|
209
|
+
"sum": { "total": 150 },
|
|
210
|
+
"rows": [{ "by": "pending", "sum_total": 100 }]
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
"pagination": {
|
|
214
|
+
"total": 25,
|
|
215
|
+
"pages": 3,
|
|
216
|
+
"computed": { "min": { "id": 1 }, "max": { "id": 100 } },
|
|
217
|
+
"paginator": { "page": 1, "itemsPerPage": 20 }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`joins`, `joinGroups`, `pagination` appear ONLY if requested. A simple search returns just `{ table, main }`.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## GET /rest/{tableName}/:id
|
|
227
|
+
|
|
228
|
+
Returns a single record by primary key.
|
|
229
|
+
|
|
230
|
+
**Response (200):**
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{ "main": { "id": 1, "name": "Mario", "email": "m@t.it" } }
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Returns 404 if not found.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## POST /rest/{tableName} (Insert)
|
|
241
|
+
|
|
242
|
+
**Body:**
|
|
243
|
+
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"main": { "name": "Mario", "email": "m@t.it" },
|
|
247
|
+
"secondaries": {
|
|
248
|
+
"customer_order": [
|
|
249
|
+
{ "total": 50, "status": "pending" }
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
- `main` — the record to insert. Omit auto-increment PKs (they are in `excludeFromCreation`).
|
|
256
|
+
- `secondaries` — optional. Related records to insert. Only tables in `allowedWriteJoins` are accepted. FK fields (e.g. `customerId` in orders) are auto-filled from the inserted main record's PK.
|
|
257
|
+
|
|
258
|
+
**Response (201):** PK-only for main and secondaries.
|
|
259
|
+
|
|
260
|
+
```json
|
|
261
|
+
{
|
|
262
|
+
"main": { "id": 1 },
|
|
263
|
+
"secondaries": {
|
|
264
|
+
"customer_order": [{ "id": 10 }]
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## PUT /rest/{tableName} (Update)
|
|
272
|
+
|
|
273
|
+
**Body:**
|
|
274
|
+
|
|
275
|
+
```json
|
|
276
|
+
{
|
|
277
|
+
"main": { "id": 1, "name": "Mario Updated", "email": "new@t.it" },
|
|
278
|
+
"secondaries": {
|
|
279
|
+
"customer_order": [{ "total": 75, "status": "shipped" }]
|
|
280
|
+
},
|
|
281
|
+
"deletions": {
|
|
282
|
+
"customer_order": [{ "id": 10 }]
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
- `main` — MUST include the PK. Only changed fields need to be sent (partial update).
|
|
288
|
+
- `secondaries` — optional. New related records to insert (FK auto-filled).
|
|
289
|
+
- `deletions` — optional. Related records to delete (by PK).
|
|
290
|
+
|
|
291
|
+
**Response (200):** PK-only for main, secondaries, and deletions.
|
|
292
|
+
|
|
293
|
+
```json
|
|
294
|
+
{
|
|
295
|
+
"main": { "id": 1 },
|
|
296
|
+
"secondaries": { "customer_order": [{ "id": 20 }] },
|
|
297
|
+
"deletions": { "customer_order": [{ "id": 10 }] }
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## DELETE /rest/{tableName}/:id
|
|
304
|
+
|
|
305
|
+
Deletes a single record by primary key.
|
|
306
|
+
|
|
307
|
+
**Response (200):** PK-only.
|
|
308
|
+
|
|
309
|
+
```json
|
|
310
|
+
{ "main": { "id": 1 } }
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
404 if not found.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## PUT /bulk/{tableName} (Bulk Upsert)
|
|
318
|
+
|
|
319
|
+
Insert or update multiple records in a single request.
|
|
320
|
+
|
|
321
|
+
**Body:**
|
|
322
|
+
|
|
323
|
+
```json
|
|
324
|
+
[
|
|
325
|
+
{
|
|
326
|
+
"main": { "name": "Mario", "email": "m@t.it" },
|
|
327
|
+
"secondaries": { "customer_order": [{ "total": 50 }] },
|
|
328
|
+
"deletions": { "customer_order": [{ "id": 99 }] }
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
"main": { "name": "Luigi", "email": "l@t.it" }
|
|
332
|
+
}
|
|
333
|
+
]
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
All main records are inserted/upserted in a single SQL query. Secondaries and deletions are processed per-item.
|
|
337
|
+
|
|
338
|
+
**Response (200):** Array of `{ main (PK-only), secondaries? (PK-only), deletions? }`.
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## POST /bulk/{tableName}/delete (Bulk Delete)
|
|
343
|
+
|
|
344
|
+
Delete multiple records by PK.
|
|
345
|
+
|
|
346
|
+
**Body:**
|
|
347
|
+
|
|
348
|
+
```json
|
|
349
|
+
[{ "id": 1 }, { "id": 2 }, { "id": 3 }]
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Each object must contain the PK field. All deletions execute as a single `DELETE WHERE pk IN (...)`.
|
|
353
|
+
|
|
354
|
+
**Response (200):** Array of PK-only objects.
|
|
355
|
+
|
|
356
|
+
```json
|
|
357
|
+
[{ "main": { "id": 1 } }, { "main": { "id": 2 } }]
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Validation Errors
|
|
363
|
+
|
|
364
|
+
Insert, update, and bulk-upsert endpoints may return **structured field-level validation errors** (400). Two sources:
|
|
365
|
+
|
|
366
|
+
1. **Schema validation** (TypeBox/Ajv) — type mismatches, missing required fields
|
|
367
|
+
2. **Custom validation** (`validate` / `validateBulk` on the table) — business rules, cross-entity checks
|
|
368
|
+
|
|
369
|
+
Both return the same response format:
|
|
370
|
+
|
|
371
|
+
```json
|
|
372
|
+
{
|
|
373
|
+
"statusCode": 400,
|
|
374
|
+
"error": "Bad Request",
|
|
375
|
+
"message": "Validation failed",
|
|
376
|
+
"fields": [
|
|
377
|
+
{ "path": "body.main.name", "code": "required", "message": "must have required property 'name'" },
|
|
378
|
+
{ "path": "session_period[1].startDate", "code": "overlap", "message": "overlaps with another period" }
|
|
379
|
+
]
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Each entry in `fields`:
|
|
384
|
+
- `path` — the field path (e.g. `body.main.name` for schema errors, `name` or `session_period[1].startDate` for custom validation)
|
|
385
|
+
- `code` — machine-readable error code (e.g. `required`, `type`, `overlap`, `unique`)
|
|
386
|
+
- `message` — human-readable description
|
|
387
|
+
|
|
388
|
+
### Handling validation errors in the frontend
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
const response = await fetch('/api/rest/session', {
|
|
392
|
+
method: 'POST',
|
|
393
|
+
headers: { 'Content-Type': 'application/json' },
|
|
394
|
+
body: JSON.stringify({ main: { name: '' }, secondaries: { session_period: periods } }),
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
if (!response.ok) {
|
|
398
|
+
const error = await response.json();
|
|
399
|
+
if (error.fields) {
|
|
400
|
+
// Field-level errors — display inline on the form
|
|
401
|
+
for (const { path, message } of error.fields) {
|
|
402
|
+
setFieldError(path, message);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Response Conventions
|
|
411
|
+
|
|
412
|
+
- **PK-only responses**: Insert, update, delete, bulk operations return only the primary key fields, not the full record. This is by design for performance and consistency.
|
|
413
|
+
- **camelCase fields**: All request and response fields use camelCase (e.g. `customerId`, not `customer_id`). The plugin converts automatically.
|
|
414
|
+
- **Joins are virtual**: Not SQL JOINs. They execute separate queries and group results by FK. This keeps the main query simple and avoids N+1 issues through batching.
|
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ Point it at your database, and get a full REST API with search, pagination, join
|
|
|
12
12
|
- **Virtual joins** — fetch related records without complex SQL
|
|
13
13
|
- **Bulk operations** — batch insert/upsert/delete in single queries
|
|
14
14
|
- **Multi-tenant** — automatic row-level isolation, zero code in handlers
|
|
15
|
+
- **Validation** — structured field-level validation with cross-entity support
|
|
15
16
|
- **Hooks** — `beforeInsert`, `beforeUpdate`, `afterInsert` for custom logic
|
|
16
17
|
- **Swagger UI** — optional, auto-configured from your schemas
|
|
17
18
|
- **Composable** — register all routes or pick only what you need
|
|
@@ -155,10 +156,27 @@ const TableCustomer = defineTable({
|
|
|
155
156
|
buildUpsertRule(SchemaCustomer, ['id']),
|
|
156
157
|
),
|
|
157
158
|
|
|
159
|
+
// Schema overrides (tighten generated schema without editing Schema files)
|
|
160
|
+
schemaOverrides: {
|
|
161
|
+
email: Type.String({ format: 'email' }),
|
|
162
|
+
},
|
|
163
|
+
|
|
158
164
|
// Multi-tenant isolation
|
|
159
165
|
tenantScope: { column: 'organization_id' },
|
|
160
166
|
|
|
161
|
-
//
|
|
167
|
+
// Validation (runs after schema validation, before hooks)
|
|
168
|
+
validate: async (db, req, main, secondaries) => {
|
|
169
|
+
// Return ValidationError[] — tuple: [field, code] or [field, code, message]
|
|
170
|
+
// message defaults to code if omitted
|
|
171
|
+
if (!main.name) return [['name', 'required']];
|
|
172
|
+
return [];
|
|
173
|
+
},
|
|
174
|
+
validateBulk: async (db, req, items) => {
|
|
175
|
+
// Bulk-upsert only. Called once with all items for cross-item validation.
|
|
176
|
+
return [];
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// Hooks (runs after validation)
|
|
162
180
|
beforeInsert: async (db, req, record) => { /* record is snake_case */ },
|
|
163
181
|
beforeUpdate: async (db, req, fields) => { /* fields is snake_case */ },
|
|
164
182
|
afterInsert: async (db, req, record, secondaryRecords) => { /* record is camelCase */ },
|
|
@@ -54,8 +54,8 @@ async function main() {
|
|
|
54
54
|
display('++++++ fastify-auto-sqlapi: generating tables template ++++++', CONSOLE_COLORS.yellow);
|
|
55
55
|
const config = await loadConfig();
|
|
56
56
|
const outputDir = path.resolve(process.cwd(), cliArgs.output || config.outputDir);
|
|
57
|
-
const schemasDir =
|
|
58
|
-
const tablesDir =
|
|
57
|
+
const schemasDir = outputDir;
|
|
58
|
+
const tablesDir = outputDir;
|
|
59
59
|
if (!fs.existsSync(schemasDir)) {
|
|
60
60
|
error(`Schemas directory not found: ${schemasDir}`);
|
|
61
61
|
display('Run sqlapi-generate-schema first.', CONSOLE_COLORS.magenta);
|
package/dist/index.d.ts
CHANGED
|
@@ -30,5 +30,5 @@ export { BulkDeleteTableBody, BulkDeleteTableResponse } from './lib/schema/bulk-
|
|
|
30
30
|
export { default as bulkDeleteRoutes } from './routes/auto/bulk-delete.routes.js';
|
|
31
31
|
export { Type, type Static } from '@sinclair/typebox';
|
|
32
32
|
export { ConditionBuilder, Expression } from 'node-condition-builder';
|
|
33
|
-
export type { SqlApiConfig, ColumnInfo, TableMap, Queryable, SqlResult, DbRecord, DbRecordValue, SelectOptions, SchemaDefinition, JoinDefinition, ITable, DbTables, SqlApiPluginOptions, SwaggerOptions, SearchParams, SearchResult, Paginator, PaginationResult, TableFilterFn, ExtendedConditionFn, FilterRecord, AggregationRequest, JoinGroupRequest, GetParams, GetResult, DeleteParams, DeleteResult, InsertParams, InsertResult, BulkUpsertItem, BulkUpsertParams, BulkUpsertResult, BulkDeleteParams, BulkDeleteResult, ConditionMethod, SearchCondition, UpdateParams, UpdateResult, TenantId, TenantScope, TenantScopeDirect, TenantScopeIndirect, TenantContext, } from './types.js';
|
|
33
|
+
export type { SqlApiConfig, ColumnInfo, TableMap, Queryable, SqlResult, DbRecord, DbRecordValue, SelectOptions, SchemaDefinition, JoinDefinition, ITable, DbTables, SqlApiPluginOptions, SwaggerOptions, SearchParams, SearchResult, Paginator, PaginationResult, TableFilterFn, ExtendedConditionFn, FilterRecord, AggregationRequest, JoinGroupRequest, GetParams, GetResult, DeleteParams, DeleteResult, InsertParams, InsertResult, BulkUpsertItem, BulkUpsertParams, BulkUpsertResult, BulkDeleteParams, BulkDeleteResult, ConditionMethod, SearchCondition, UpdateParams, UpdateResult, TenantId, TenantScope, TenantScopeDirect, TenantScopeIndirect, TenantContext, ValidationError, ValidatorFn, BulkValidatorItem, BulkValidatorFn, } from './types.js';
|
|
34
34
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pg-introspect.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/pg-introspect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,wBAAgB,qBAAqB,IAAI,MAAM,CAY9C;AAED,wBAAsB,gBAAgB,CACpC,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"pg-introspect.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/pg-introspect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,wBAAgB,qBAAqB,IAAI,MAAM,CAY9C;AAED,wBAAsB,gBAAgB,CACpC,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,CAkCvB"}
|
|
@@ -19,7 +19,14 @@ export async function introspectTables(connectionString, schema) {
|
|
|
19
19
|
pg = require('pg');
|
|
20
20
|
}
|
|
21
21
|
catch {
|
|
22
|
-
|
|
22
|
+
try {
|
|
23
|
+
// Fallback: resolve from the package's own node_modules
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
pg = require('pg');
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error('pg is required for PostgreSQL introspection. Install it with: npm install pg');
|
|
29
|
+
}
|
|
23
30
|
}
|
|
24
31
|
const client = new pg.Client({ connectionString });
|
|
25
32
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tables-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/tables-codegen.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA2BpE;AAgBD,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,gBAAgB,EAAE,CAwB3E;AAID,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"tables-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/tables-codegen.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CA2BpE;AAgBD,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,gBAAgB,EAAE,CAwB3E;AAID,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,MAAM,CAuHhG;AAID,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAqBrE;AAID,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CA6FlE"}
|
|
@@ -140,6 +140,27 @@ export function generateSingleTableFile(schema, allSchemas) {
|
|
|
140
140
|
lines.push(` // allowedReadJoins: [],`);
|
|
141
141
|
}
|
|
142
142
|
lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(Schema, ['${pk}'])),`);
|
|
143
|
+
// schemaOverrides: suggest email format if email field exists, otherwise minLength on first string field
|
|
144
|
+
const emailField = schema.fields.find(f => f.toLowerCase().includes('email'));
|
|
145
|
+
if (emailField) {
|
|
146
|
+
lines.push(` // schemaOverrides: { ${emailField}: Type.String({ format: 'email' }) },`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
const stringField = schema.fields.find(f => f !== pk && schema.fieldTypes[f]?.includes('String'));
|
|
150
|
+
if (stringField) {
|
|
151
|
+
lines.push(` // schemaOverrides: { ${stringField}: Type.String({ minLength: 1 }) },`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
lines.push(` // schemaOverrides: {},`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// validate: use first non-PK field for example
|
|
158
|
+
const exampleField = schema.fields.find(f => f !== pk) || schema.fields[0];
|
|
159
|
+
lines.push(` validate: async (db, req, main, secondaries) => {`);
|
|
160
|
+
lines.push(` const errors = [];`);
|
|
161
|
+
lines.push(` // if (!main.${exampleField}) errors.push(['${exampleField}', 'required']);`);
|
|
162
|
+
lines.push(` return errors;`);
|
|
163
|
+
lines.push(` },`);
|
|
143
164
|
lines.push(` // beforeInsert: async (db, req, record) => {},`);
|
|
144
165
|
lines.push(` // beforeUpdate: async (db, req, fields) => {},`);
|
|
145
166
|
if (childRels.length > 0) {
|
|
@@ -223,6 +244,13 @@ export function generateTablesFile(schemas) {
|
|
|
223
244
|
lines.push(` // allowedReadJoins: [],`);
|
|
224
245
|
}
|
|
225
246
|
lines.push(` // upsertMap: buildUpsertRules(buildUpsertRule(${schema.schemaName}, ['${pk}'])),`);
|
|
247
|
+
lines.push(` // schemaOverrides: {},`);
|
|
248
|
+
const exampleFieldLegacy = schema.fields.find(f => f !== pk) || schema.fields[0];
|
|
249
|
+
lines.push(` validate: async (db, req, main, secondaries) => {`);
|
|
250
|
+
lines.push(` const errors = [];`);
|
|
251
|
+
lines.push(` // if (!main.${exampleFieldLegacy}) errors.push(['${exampleFieldLegacy}', 'required']);`);
|
|
252
|
+
lines.push(` return errors;`);
|
|
253
|
+
lines.push(` },`);
|
|
226
254
|
lines.push(` // beforeInsert: async (db, req, record) => {},`);
|
|
227
255
|
lines.push(` // beforeUpdate: async (db, req, fields) => {},`);
|
|
228
256
|
const childRels = relsByChild.get(schema.schemaName) || [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-upsert.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"bulk-upsert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/bulk/bulk-upsert.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAGjB,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAgG5F"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { camelcaseObject, snakecaseRecord } from '../../naming.js';
|
|
2
2
|
import { removeExcludedFields, processSecondaries, processDeletions } from '../write-helpers.js';
|
|
3
3
|
import { injectTenantValue, validateTenantFK } from '../../tenant.js';
|
|
4
|
+
import { runValidation, runBulkValidation } from '../validate.js';
|
|
4
5
|
import { primaryAsString } from '../../../types.js';
|
|
5
6
|
export async function bulkUpsertEngine(params) {
|
|
6
7
|
const { db, tableConf, dbTables, request, items, tenant } = params;
|
|
@@ -29,13 +30,22 @@ export async function bulkUpsertEngine(params) {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
|
-
// 2.
|
|
33
|
+
// 2. Custom validation (receives original camelCase records)
|
|
34
|
+
if (tableConf.validateBulk) {
|
|
35
|
+
await runBulkValidation(db, request, tableConf, items);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
for (let i = 0; i < items.length; i++) {
|
|
39
|
+
await runValidation(db, request, tableConf, items[i].main, items[i].secondaries);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// 3. beforeInsert hook per record
|
|
33
43
|
if (tableConf.beforeInsert) {
|
|
34
44
|
for (const rec of preparedMains) {
|
|
35
45
|
await tableConf.beforeInsert(db, request, rec);
|
|
36
46
|
}
|
|
37
47
|
}
|
|
38
|
-
//
|
|
48
|
+
// 4. Bulk upsert all mains in one query → returns PK-only
|
|
39
49
|
const upsertKeys = tableConf.upsertMap?.get(tableConf.Schema);
|
|
40
50
|
let pkRows;
|
|
41
51
|
if (upsertKeys) {
|
|
@@ -46,7 +56,7 @@ export async function bulkUpsertEngine(params) {
|
|
|
46
56
|
pkRows = await db.bulkInsert(tableConf.Schema.tableName, preparedMains, pkCol);
|
|
47
57
|
}
|
|
48
58
|
const pksCamel = pkRows.map((r) => camelcaseObject(r));
|
|
49
|
-
//
|
|
59
|
+
// 5. Process secondaries and deletions per item
|
|
50
60
|
const results = [];
|
|
51
61
|
for (let i = 0; i < items.length; i++) {
|
|
52
62
|
const item = items[i];
|
|
@@ -64,7 +74,7 @@ export async function bulkUpsertEngine(params) {
|
|
|
64
74
|
if (Object.keys(del).length > 0)
|
|
65
75
|
result.deletions = del;
|
|
66
76
|
}
|
|
67
|
-
//
|
|
77
|
+
// 6. afterInsert hook per item
|
|
68
78
|
if (tableConf.afterInsert) {
|
|
69
79
|
await tableConf.afterInsert(db, request, mainForFK, result.secondaries);
|
|
70
80
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/insert.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"insert.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/insert.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAGb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAyE9E"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { camelcaseObject, snakecaseRecord } from '../../naming.js';
|
|
2
2
|
import { removeExcludedFields, processSecondaries } from '../write-helpers.js';
|
|
3
3
|
import { injectTenantValue, validateTenantFK } from '../../tenant.js';
|
|
4
|
+
import { runValidation } from '../validate.js';
|
|
4
5
|
import { primaryAsString } from '../../../types.js';
|
|
5
6
|
export async function insertEngine(params) {
|
|
6
7
|
const { db, tableConf, dbTables, request, record, secondaries, tenant } = params;
|
|
@@ -22,11 +23,13 @@ export async function insertEngine(params) {
|
|
|
22
23
|
injectTenantValue(mainRecord, tenant.scope, tenant.ids);
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
|
-
// 2.
|
|
26
|
+
// 2. Custom validation (receives original camelCase record)
|
|
27
|
+
await runValidation(db, request, tableConf, record, secondaries);
|
|
28
|
+
// 3. beforeInsert hook
|
|
26
29
|
if (tableConf.beforeInsert) {
|
|
27
30
|
await tableConf.beforeInsert(db, request, mainRecord);
|
|
28
31
|
}
|
|
29
|
-
//
|
|
32
|
+
// 4. Insert main → returns PK-only
|
|
30
33
|
let pkResult;
|
|
31
34
|
const upsertKeys = tableConf.upsertMap?.get(tableConf.Schema);
|
|
32
35
|
if (upsertKeys) {
|
|
@@ -37,19 +40,19 @@ export async function insertEngine(params) {
|
|
|
37
40
|
pkResult = await db.insert(tableConf.Schema.tableName, mainRecord, pkCol);
|
|
38
41
|
}
|
|
39
42
|
const mainPkCamel = camelcaseObject(pkResult);
|
|
40
|
-
//
|
|
43
|
+
// 5. Secondaries: need full record for FK auto-fill
|
|
41
44
|
let secondaryResults;
|
|
42
45
|
if (secondaries && Object.keys(secondaries).length > 0) {
|
|
43
46
|
// Merge input record + PK for FK auto-fill
|
|
44
47
|
const mainForFK = { ...camelcaseObject(mainRecord), ...mainPkCamel };
|
|
45
48
|
secondaryResults = await processSecondaries(db, tableConf, dbTables, mainForFK, secondaries);
|
|
46
49
|
}
|
|
47
|
-
//
|
|
50
|
+
// 6. afterInsert hook
|
|
48
51
|
if (tableConf.afterInsert) {
|
|
49
52
|
const mainForHook = { ...camelcaseObject(mainRecord), ...mainPkCamel };
|
|
50
53
|
await tableConf.afterInsert(db, request, mainForHook, secondaryResults);
|
|
51
54
|
}
|
|
52
|
-
//
|
|
55
|
+
// 7. Return PK-only
|
|
53
56
|
const result = { main: mainPkCamel };
|
|
54
57
|
if (secondaryResults && Object.keys(secondaryResults).length > 0) {
|
|
55
58
|
result.secondaries = secondaryResults;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/lib/engine/rest/update.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAGb,MAAM,mBAAmB,CAAC;AAE3B,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAgI9E"}
|