fastify-auto-sqlapi 0.0.4 → 0.0.5
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_BACKEND.md +18 -14
- package/AGENTS_FRONTEND.md +78 -1
- package/README.md +117 -18
- package/dist/bin/generate-schema.js +3 -3
- package/dist/bin/generate-tables.js +3 -3
- package/dist/bin/utils.d.ts +1 -1
- package/dist/bin/utils.d.ts.map +1 -1
- package/dist/bin/utils.js +2 -2
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
- package/dist/lib/cli/mysql-introspect.js +9 -2
- package/dist/lib/cli/schema-codegen.d.ts +1 -1
- package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
- package/dist/lib/cli/schema-codegen.js +11 -4
- package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
- package/dist/lib/cli/tables-codegen.js +4 -3
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +10 -4
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.js +40 -36
- package/dist/lib/engine/rest/get.js +1 -1
- package/dist/lib/engine/rest/insert.d.ts.map +1 -1
- package/dist/lib/engine/rest/insert.js +24 -23
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +41 -40
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +235 -39
- package/dist/lib/engine/write-helpers.js +4 -4
- package/dist/lib/naming.d.ts +11 -2
- package/dist/lib/naming.d.ts.map +1 -1
- package/dist/lib/naming.js +33 -5
- package/dist/lib/schema/search.d.ts.map +1 -1
- package/dist/lib/schema/search.js +16 -10
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +9 -1
- package/dist/types.d.ts +26 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -5
package/AGENTS_BACKEND.md
CHANGED
|
@@ -21,15 +21,16 @@ npm install @fastify/swagger @fastify/swagger-ui
|
|
|
21
21
|
Create `sqlapi.config.ts` (or `.js`) in the project root. This config is used **only by the CLI** (`sqlapi-generate-schema` / `sqlapi-generate-tables`), NOT at runtime by the Fastify plugin.
|
|
22
22
|
|
|
23
23
|
```typescript
|
|
24
|
-
// sqlapi.config.ts — interface: { outputDir: string; schema?: string; dialect?: DialectName }
|
|
24
|
+
// sqlapi.config.ts — interface: { outputDir: string; schema?: string; dialect?: DialectName; envFile?: string }
|
|
25
25
|
export default {
|
|
26
26
|
outputDir: './src/db', // base directory for generated files (default: './src/db')
|
|
27
27
|
schema: 'public', // DB schema to introspect (default: 'public', PostgreSQL only)
|
|
28
28
|
dialect: 'postgres', // 'postgres' | 'mysql' | 'mariadb' (default: 'postgres')
|
|
29
|
+
// envFile: '../../.env', // path to .env file, relative to cwd (default: '.env')
|
|
29
30
|
};
|
|
30
31
|
```
|
|
31
32
|
|
|
32
|
-
That's it — only these
|
|
33
|
+
That's it — only these four fields exist. **No connection string, no migration path.** If the file is missing, defaults are used. The `dialect` can also be passed via CLI flag `--dialect mysql`. The `envFile` is useful in monorepo setups where the `.env` lives at the repo root.
|
|
33
34
|
|
|
34
35
|
### 3. Configure database connection (env vars)
|
|
35
36
|
|
|
@@ -282,17 +283,17 @@ export const TableCustomer = defineTable({
|
|
|
282
283
|
return []; // ValidationError[]
|
|
283
284
|
},
|
|
284
285
|
|
|
285
|
-
// HOOKS — side effects (runs after validation)
|
|
286
|
+
// HOOKS — side effects (runs after validation). All receive camelCase (schema field names).
|
|
286
287
|
beforeInsert: async (db, req, record) => {
|
|
287
|
-
// Mutate record before INSERT.
|
|
288
|
-
record.
|
|
288
|
+
// Mutate record before INSERT. Use camelCase (schema) field names — conversion is automatic.
|
|
289
|
+
record.createdBy = req.user.id;
|
|
289
290
|
},
|
|
290
291
|
afterInsert: async (db, req, record, secondaryRecords) => {
|
|
291
|
-
// Called after INSERT + secondaries. record is camelCase.
|
|
292
|
+
// Called after INSERT + secondaries. record is camelCase (input merged with generated PK).
|
|
292
293
|
},
|
|
293
294
|
beforeUpdate: async (db, req, fields) => {
|
|
294
|
-
// Mutate fields before UPDATE.
|
|
295
|
-
fields.
|
|
295
|
+
// Mutate fields before UPDATE. camelCase. PK is present for reference but excluded from UPDATE SET.
|
|
296
|
+
fields.updatedBy = req.user.id;
|
|
296
297
|
},
|
|
297
298
|
|
|
298
299
|
// AUTH — per-table request hooks
|
|
@@ -430,6 +431,8 @@ const sqlApi = createSqlApi(db, dbTables, { dialect: 'mysql' });
|
|
|
430
431
|
|
|
431
432
|
```typescript
|
|
432
433
|
// Search — full filter, join, pagination, aggregation support
|
|
434
|
+
// orderBy supports dot notation for joinGroup aggregations: "session.sum.duration DESC"
|
|
435
|
+
// (requires the joinGroup to be declared in the same request, see AGENTS_FRONTEND.md)
|
|
433
436
|
sqlApi.search(tableName, {
|
|
434
437
|
filters?, joinFilters?, joins?, joinGroups?,
|
|
435
438
|
orderBy?, paginator?, computeMin?, computeMax?, computeSum?, computeAvg?,
|
|
@@ -590,14 +593,14 @@ Tables without `tenantScope` are unaffected — no filtering regardless of `getT
|
|
|
590
593
|
|
|
591
594
|
## Key Conventions
|
|
592
595
|
|
|
593
|
-
- **camelCase in API,
|
|
596
|
+
- **camelCase in API, any case in DB**: all request/response fields are camelCase. The plugin converts via `col()` and `colMap`. For snake_case DB columns (default), conversion is automatic. For camelCase DB columns (e.g. betterauth), the CLI generates a `colMap` that preserves the original column names — no conversion needed. Manual schemas without `colMap` fall back to `toUnderscore()`.
|
|
594
597
|
- **All fields Optional in response**: `RETURNING *` may return any subset. Response schemas use `Type.Partial`.
|
|
595
598
|
- **`excludeFromCreation`**: **IMPORTANT** — auto-increment PKs (e.g. `id` serial/auto_increment) MUST be listed here, otherwise INSERT will try to send them and fail. The CLI auto-detects this and adds it by default. Also useful for `createdAt`/`updatedAt` columns managed by DB defaults or hooks.
|
|
596
599
|
- **`upsertMap`**: when present for a schema, INSERT becomes upsert. PostgreSQL: `ON CONFLICT (...) DO UPDATE`. MySQL/MariaDB: `ON DUPLICATE KEY UPDATE`. Applies to both main and secondary tables.
|
|
597
600
|
- **`schemaOverrides`**: override auto-generated schema fields with stricter TypeBox types (e.g. `{ email: Type.String({ format: 'email' }) }`). Overrides are merged into the body schema for insert, update, and bulk-upsert. The original Schema file is never modified. In updates, overridden fields are still wrapped in Optional (validates only when present). Overrides appear in Swagger.
|
|
598
601
|
- **Validation receives camelCase**: `validate` receives the original camelCase record (as sent by the client) and secondaries. Field names match the schema definition, with full TypeScript inference (`main.startDate`, not `main.start_date`). It returns `ValidationError[]` — tuples of `[field, code]` or `[field, code, message]`. If any errors are returned, the request is rejected with 400 before hooks or SQL execute.
|
|
599
602
|
- **`validateBulk` replaces `validate` in bulk**: when `validateBulk` is defined, it is called once with all items and per-item `validate` is skipped. This allows optimized batch queries instead of N individual checks. When only `validate` is defined, it runs per-item as fallback.
|
|
600
|
-
- **
|
|
603
|
+
- **All hooks and validators receive camelCase records**: `validate`, `beforeInsert`, `beforeUpdate`, and `afterInsert` all get records keyed by schema field names (camelCase). Mutations propagate to the SQL (plugin converts to DB column format via `colMap` after the hook). The engine internally uses `snakecaseRecord(..., schema)` after user mutations to map field names to actual DB columns.
|
|
601
604
|
- **Filters validation**: TypeBox schemas use `additionalProperties: false`. By default Fastify strips unknown fields silently. For 400 errors on unknown filters: `Fastify({ ajv: { customOptions: { removeAdditional: false } } })`.
|
|
602
605
|
- **Tenant filtering**: when `tenantScope` is set on a table and `getTenantId` is provided in plugin options, all CRUD operations are automatically scoped to the tenant. `getTenantId` returning `null`/`undefined` = admin (no filter). Returning an array = multi-tenant user (IN clause).
|
|
603
606
|
|
|
@@ -699,12 +702,13 @@ const TableCustomer = defineTable({
|
|
|
699
702
|
primary: 'id',
|
|
700
703
|
...exportTableInfo(SchemaCustomer),
|
|
701
704
|
beforeInsert: async (db, req, record) => {
|
|
702
|
-
|
|
703
|
-
record.
|
|
705
|
+
// camelCase — schema field names. The plugin converts to the actual DB column names.
|
|
706
|
+
record.createdBy = req.user.id;
|
|
707
|
+
record.createdAt = db.expression('NOW()');
|
|
704
708
|
},
|
|
705
709
|
beforeUpdate: async (db, req, fields) => {
|
|
706
|
-
fields.
|
|
707
|
-
fields.
|
|
710
|
+
fields.updatedBy = req.user.id;
|
|
711
|
+
fields.updatedAt = db.expression('NOW()');
|
|
708
712
|
},
|
|
709
713
|
});
|
|
710
714
|
```
|
package/AGENTS_FRONTEND.md
CHANGED
|
@@ -107,6 +107,40 @@ Also available: `isNotGreater`, `isNotGreaterOrEqual`, `isNotLess`, `isNotLessOr
|
|
|
107
107
|
- **Field validation** — field names are validated against the table schema (400 if unknown)
|
|
108
108
|
- **Method validation** — only whitelisted methods are allowed (`raw`, `append` etc. are blocked)
|
|
109
109
|
|
|
110
|
+
#### Conditions on joinGroup aggregations (HAVING-style)
|
|
111
|
+
|
|
112
|
+
You can filter main rows based on the value of a joinGroup aggregation using the same dot notation as `orderBy`: `<joinTable>.<fn>.<field>`.
|
|
113
|
+
|
|
114
|
+
Example: "users with at least 4 sessions":
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"conditions": [
|
|
119
|
+
{ "field": "session.count.id", "method": "isGreaterOrEqual", "params": [4] }
|
|
120
|
+
],
|
|
121
|
+
"joinGroups": {
|
|
122
|
+
"session": { "aggregations": { "count": ["id"] } }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Generated SQL:
|
|
128
|
+
```sql
|
|
129
|
+
SELECT * FROM "user"
|
|
130
|
+
WHERE COALESCE((
|
|
131
|
+
SELECT COUNT("session"."id")
|
|
132
|
+
FROM "session"
|
|
133
|
+
WHERE "session"."user_id" = "user"."id"
|
|
134
|
+
), 0) >= $1
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Rules:**
|
|
138
|
+
- Same declaration requirement as orderBy: the joinGroup must be declared in `joinGroups.<table>.aggregations.<fn>` with the referenced field.
|
|
139
|
+
- The joinGroup's own `filters` are applied inside the correlated subquery (consistent with the breakdown). Example: filter customers whose SUM of *completed* orders > 1000 by declaring `joinGroups.order.filters: { status: 'completed' }`.
|
|
140
|
+
- All ConditionBuilder methods work: `isEqual`, `isGreater`, `isLess`, `isBetween`, `isIn`, `isNull`, `isNotNull`, etc.
|
|
141
|
+
- Can be combined with plain-field conditions and with aggregation `orderBy` in the same request.
|
|
142
|
+
- Respects `aggregations.by` rules: allowed when `by` equals the correlation FK, otherwise 400.
|
|
143
|
+
|
|
110
144
|
### Join Filters (filter main by related table)
|
|
111
145
|
|
|
112
146
|
`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.
|
|
@@ -151,6 +185,8 @@ The join executes as a separate query: `SELECT ... FROM {joinTable} WHERE {fk} I
|
|
|
151
185
|
"sum": ["total"],
|
|
152
186
|
"min": ["total"],
|
|
153
187
|
"max": ["total"],
|
|
188
|
+
"avg": ["total"],
|
|
189
|
+
"count": ["id"],
|
|
154
190
|
"distinctCount": ["status"]
|
|
155
191
|
},
|
|
156
192
|
"filters": { "status": "active" }
|
|
@@ -160,7 +196,8 @@ The join executes as a separate query: `SELECT ... FROM {joinTable} WHERE {fk} I
|
|
|
160
196
|
```
|
|
161
197
|
|
|
162
198
|
- `by` — GROUP BY field
|
|
163
|
-
- `sum`, `min`, `max` — aggregate functions on specified columns
|
|
199
|
+
- `sum`, `min`, `max`, `avg` — aggregate functions on specified columns
|
|
200
|
+
- `count` — COUNT(col)
|
|
164
201
|
- `distinctCount` — COUNT(DISTINCT col)
|
|
165
202
|
- `filters` — optional, narrow rows before aggregation
|
|
166
203
|
|
|
@@ -176,6 +213,46 @@ Use the `orderBy` query parameter:
|
|
|
176
213
|
|
|
177
214
|
If not specified, the table's `defaultOrder` is used.
|
|
178
215
|
|
|
216
|
+
#### Ordering by joinGroup aggregations
|
|
217
|
+
|
|
218
|
+
You can order the main results by an aggregation computed on a joined table using dot notation: `<joinTable>.<fn>.<field>`.
|
|
219
|
+
|
|
220
|
+
Supported functions: `sum`, `min`, `max`, `avg`, `count`, `distinctCount`.
|
|
221
|
+
|
|
222
|
+
Example: get users ordered by total session duration DESC.
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
POST /auto/search/user?orderBy=session.sum.duration%20DESC
|
|
226
|
+
```
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"joinGroups": {
|
|
230
|
+
"session": {
|
|
231
|
+
"aggregations": { "sum": ["duration"] }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Generated SQL (simplified):
|
|
238
|
+
```sql
|
|
239
|
+
SELECT * FROM "user"
|
|
240
|
+
ORDER BY (
|
|
241
|
+
SELECT SUM("session"."duration")
|
|
242
|
+
FROM "session"
|
|
243
|
+
WHERE "session"."user_id" = "user"."id"
|
|
244
|
+
) DESC
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Rules:**
|
|
248
|
+
- The joinGroup **must be declared in the request body** (`joinGroups.<table>.aggregations.<fn>` must include the field). The engine refuses undeclared references with 400.
|
|
249
|
+
- If the joinGroup declares `aggregations.filters`, those filters are applied inside the correlated subquery — consistent with the breakdown.
|
|
250
|
+
- **`aggregations.by`**: allowed **only** when `by` is the same field used for the join correlation (e.g. `by: "userId"` when joining user→session via userId). In that case each main row maps to exactly one group, so the ordering is well-defined *and* the response contains the per-row breakdown you can render next to each row. Using `by` on any other column is rejected with 400 (the result would be bucketed per group, not per row).
|
|
251
|
+
- **Not allowed** when the main table has `distinctResults: true`. 400 error.
|
|
252
|
+
- Can be combined with plain field ordering: `?orderBy=session.sum.duration DESC, name ASC`.
|
|
253
|
+
- The joinGroup breakdown is still returned in the response (`joinGroups.session.sum.duration` etc.) as before — you get both ordered main results and the aggregated summary.
|
|
254
|
+
- **No-data rows**: rows with no matching joined records are coalesced to `0` via `COALESCE(..., 0)`. Practically: on DESC they appear last, on ASC they appear first. This is universal across PostgreSQL, MySQL, and MariaDB.
|
|
255
|
+
|
|
179
256
|
### Pagination
|
|
180
257
|
|
|
181
258
|
Add `page` or `itemsPerPage` to the query string to enable pagination:
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# fastify-auto-sqlapi
|
|
2
2
|
|
|
3
|
-
Automatic CRUD API generation from PostgreSQL tables for [Fastify](https://fastify.dev/). No ORM, no magic — just raw SQL
|
|
3
|
+
Automatic CRUD API generation from PostgreSQL, MySQL, and MariaDB tables for [Fastify](https://fastify.dev/). No ORM, no magic — just raw SQL, with TypeBox validation and Swagger docs out of the box.
|
|
4
4
|
|
|
5
|
-
Point it at your database, and get a full REST API with search, pagination, joins, bulk operations, multi-tenant isolation, and hooks.
|
|
5
|
+
Point it at your database, and get a full REST API with search, advanced conditions, pagination, joins, aggregations, bulk operations, multi-tenant isolation, validation, and hooks.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
@@ -176,10 +176,10 @@ const TableCustomer = defineTable({
|
|
|
176
176
|
return [];
|
|
177
177
|
},
|
|
178
178
|
|
|
179
|
-
// Hooks (runs after validation)
|
|
180
|
-
beforeInsert: async (db, req, record) => { /*
|
|
181
|
-
beforeUpdate: async (db, req, fields) => { /*
|
|
182
|
-
afterInsert: async (db, req, record, secondaryRecords) => { /*
|
|
179
|
+
// Hooks (runs after validation) — all receive camelCase records (schema field names)
|
|
180
|
+
beforeInsert: async (db, req, record) => { /* camelCase; mutations propagate to INSERT */ },
|
|
181
|
+
beforeUpdate: async (db, req, fields) => { /* camelCase; PK included for reference, excluded from UPDATE SET */ },
|
|
182
|
+
afterInsert: async (db, req, record, secondaryRecords) => { /* camelCase; input merged with generated PK */ },
|
|
183
183
|
|
|
184
184
|
// Auth (per-table)
|
|
185
185
|
onRequests: [
|
|
@@ -219,37 +219,80 @@ const TableCustomer = defineTable({
|
|
|
219
219
|
```typescript
|
|
220
220
|
await app.register(fastifyAutoSqlApi, {
|
|
221
221
|
DbTables: dbTables, // Required — your table definitions
|
|
222
|
+
dialect: 'postgres', // Optional — 'postgres' | 'mysql' | 'mariadb' (default: 'postgres')
|
|
222
223
|
prefix: '/api', // Optional — URL prefix for all routes
|
|
223
224
|
swagger: true, // Optional — enable Swagger UI (or pass SwaggerOptions)
|
|
224
225
|
onRequests: [authMiddleware], // Optional — global hooks applied to every route
|
|
225
226
|
getTenantId: (req) => id, // Optional — multi-tenant function
|
|
227
|
+
debug: true, // Optional — log all SQL queries and params
|
|
226
228
|
});
|
|
227
229
|
```
|
|
228
230
|
|
|
229
231
|
| Option | Type | Required | Description |
|
|
230
232
|
|--------|------|----------|-------------|
|
|
231
233
|
| `DbTables` | `Record<string, ITable>` | Yes | Table configurations |
|
|
234
|
+
| `dialect` | `'postgres' \| 'mysql' \| 'mariadb'` | No | DB dialect (default: `'postgres'`) |
|
|
232
235
|
| `prefix` | `string` | No | URL prefix (e.g. `/api`) |
|
|
233
236
|
| `swagger` | `boolean \| SwaggerOptions` | No | Enable Swagger UI |
|
|
234
237
|
| `onRequests` | `Function[]` | No | Global auth/middleware hooks |
|
|
235
238
|
| `getTenantId` | `(req) => id \| null` | No | Tenant resolver for multi-tenant |
|
|
239
|
+
| `debug` | `boolean` | No | Log all SQL queries to console |
|
|
240
|
+
|
|
241
|
+
For MySQL/MariaDB, register `mysql2/promise` pool instead of `@fastify/postgres` and pass `dialect: 'mysql'` or `'mariadb'`.
|
|
236
242
|
|
|
237
243
|
## API Reference
|
|
238
244
|
|
|
239
245
|
### POST /search/{table}
|
|
240
246
|
|
|
241
|
-
Search with filters, pagination, ordering, joins, and aggregations.
|
|
247
|
+
Search with filters, advanced conditions, pagination, ordering, joins, and aggregations.
|
|
242
248
|
|
|
243
249
|
**Request body** (all fields optional):
|
|
244
250
|
|
|
245
251
|
```json
|
|
246
252
|
{
|
|
247
253
|
"filters": { "name": "Mario" },
|
|
248
|
-
"
|
|
249
|
-
|
|
254
|
+
"conditions": [
|
|
255
|
+
{ "field": "total", "method": "isGreater", "params": [100] },
|
|
256
|
+
{ "field": "createdAt", "method": "isBetween", "params": ["2024-01-01", "2024-12-31"] }
|
|
257
|
+
],
|
|
258
|
+
"joinFilters": {
|
|
259
|
+
"order": {
|
|
260
|
+
"filters": { "status": "completed" },
|
|
261
|
+
"conditions": [{ "field": "total", "method": "isGreater", "params": [50] }]
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
"joins": {
|
|
265
|
+
"order": {
|
|
266
|
+
"filters": { "status": "completed" },
|
|
267
|
+
"conditions": [{ "field": "total", "method": "isGreater", "params": [50] }]
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
"joinGroups": {
|
|
271
|
+
"order": {
|
|
272
|
+
"aggregations": {
|
|
273
|
+
"by": "status",
|
|
274
|
+
"sum": ["total"],
|
|
275
|
+
"min": ["total"],
|
|
276
|
+
"max": ["total"],
|
|
277
|
+
"avg": ["total"],
|
|
278
|
+
"count": ["id"],
|
|
279
|
+
"distinctCount": ["status"]
|
|
280
|
+
},
|
|
281
|
+
"filters": { "status": "completed" },
|
|
282
|
+
"conditions": [{ "field": "total", "method": "isGreater", "params": [0] }]
|
|
283
|
+
}
|
|
284
|
+
}
|
|
250
285
|
}
|
|
251
286
|
```
|
|
252
287
|
|
|
288
|
+
- **`filters`** — equality-based, flat key/value. Supports schema fields + `extraFilters`.
|
|
289
|
+
- **`conditions`** — array of `{ field, method, params }`. Methods: `isEqual`, `isNotEqual`, `isGreater`, `isGreaterOrEqual`, `isLess`, `isLessOrEqual`, `isLike`, `isILike`, `isIn`, `isNotIn`, `isBetween`, `isNotBetween`, `isNull`, `isNotNull`.
|
|
290
|
+
- **`joinFilters`** — EXISTS-based filtering: "main rows where at least one related row matches". Accepts `{ filters, conditions }` (both optional). Only tables in `allowedReadJoins`.
|
|
291
|
+
- **`joins`** — fetch related records (virtual join via separate query). Accepts `{ filters, conditions }`.
|
|
292
|
+
- **`joinGroups`** — aggregations on related tables. Supports `sum`, `min`, `max`, `avg`, `count`, `distinctCount`, and optional `by` for GROUP BY. Accepts `{ filters, conditions }` inside the subquery.
|
|
293
|
+
|
|
294
|
+
**Dot-notation in `conditions` and `orderBy`** — use `<joinTable>.<fn>.<field>` to filter/order the main rows by an aggregation value (HAVING-style). Example: `orderBy=order.sum.total DESC` or `conditions: [{ field: 'order.count.id', method: 'isGreaterOrEqual', params: [4] }]`. The joinGroup must be declared in the body.
|
|
295
|
+
|
|
253
296
|
**Querystring** (optional): `orderBy`, `page`, `itemsPerPage`, `computeMin`, `computeMax`, `computeSum`, `computeAvg`
|
|
254
297
|
|
|
255
298
|
**Response:**
|
|
@@ -259,11 +302,23 @@ Search with filters, pagination, ordering, joins, and aggregations.
|
|
|
259
302
|
"table": "customer",
|
|
260
303
|
"main": [{ "id": 1, "name": "Mario", "email": "m@test.it" }],
|
|
261
304
|
"joins": { "order": [{ "id": 10, "customerId": 1, "total": 50 }] },
|
|
262
|
-
"
|
|
305
|
+
"joinGroups": {
|
|
306
|
+
"order": {
|
|
307
|
+
"sum": { "total": 300 },
|
|
308
|
+
"count": { "id": 2 },
|
|
309
|
+
"rows": [{ "by": "completed", "sum_total": 300, "count_id": 2 }]
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
"pagination": {
|
|
313
|
+
"total": 25,
|
|
314
|
+
"pages": 3,
|
|
315
|
+
"computed": { "min": { "id": 1 }, "max": { "id": 100 } },
|
|
316
|
+
"paginator": { "page": 1, "itemsPerPage": 20 }
|
|
317
|
+
}
|
|
263
318
|
}
|
|
264
319
|
```
|
|
265
320
|
|
|
266
|
-
`joins`, `joinGroups`, and `pagination` only appear when requested. A simple `{}` body returns `{ table, main }`.
|
|
321
|
+
`joins`, `joinGroups`, and `pagination` only appear when requested. A simple `{}` body returns `{ table, main }`. `pagination.computed` appears only if `computeMin`/`computeMax`/`computeSum`/`computeAvg` are used.
|
|
267
322
|
|
|
268
323
|
### GET /rest/{table}/:id
|
|
269
324
|
|
|
@@ -312,6 +367,24 @@ Returns the deleted record or 404.
|
|
|
312
367
|
|
|
313
368
|
**Body:** Array of objects with the PK field, e.g. `[{ "id": 1 }, { "id": 2 }]`. Executes as a single `DELETE WHERE pk IN (...)`.
|
|
314
369
|
|
|
370
|
+
### Validation errors (400)
|
|
371
|
+
|
|
372
|
+
Both schema-level (TypeBox/Ajv) and custom (`validate` / `validateBulk`) errors use the same response shape:
|
|
373
|
+
|
|
374
|
+
```json
|
|
375
|
+
{
|
|
376
|
+
"statusCode": 400,
|
|
377
|
+
"error": "Bad Request",
|
|
378
|
+
"message": "Validation failed",
|
|
379
|
+
"fields": [
|
|
380
|
+
{ "path": "main.email", "code": "format", "message": "must match format \"email\"" },
|
|
381
|
+
{ "path": "name", "code": "required", "message": "is required" }
|
|
382
|
+
]
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Custom validators return tuples `[field, code]` or `[field, code, message]` — `message` defaults to `code` if omitted. `validateBulk` replaces per-item `validate` in bulk-upsert requests.
|
|
387
|
+
|
|
315
388
|
## Multi-Tenant
|
|
316
389
|
|
|
317
390
|
Automatic row-level isolation on all CRUD operations. Configure once, no code in handlers.
|
|
@@ -405,9 +478,9 @@ await app.register(async (instance) => {
|
|
|
405
478
|
|
|
406
479
|
## Conventions
|
|
407
480
|
|
|
408
|
-
- **camelCase in API,
|
|
409
|
-
- **
|
|
410
|
-
- **
|
|
481
|
+
- **camelCase everywhere in the API** — requests, responses, `validate`, and all hooks (`beforeInsert`, `beforeUpdate`, `afterInsert`) use schema field names
|
|
482
|
+
- **Conversion to DB column format is automatic** via `colMap` — supports both snake_case and camelCase DB columns (e.g. betterauth-style)
|
|
483
|
+
- **Joins are virtual** — separate `SELECT ... WHERE fk IN (...)` queries, not SQL JOINs
|
|
411
484
|
- **All response fields are Optional** — response schemas use `Type.Partial` since `RETURNING *` may return any subset
|
|
412
485
|
|
|
413
486
|
## Re-exports
|
|
@@ -417,23 +490,49 @@ The package re-exports commonly needed utilities so you don't need to install th
|
|
|
417
490
|
```typescript
|
|
418
491
|
import {
|
|
419
492
|
Type, // from @sinclair/typebox
|
|
493
|
+
type Static, // from @sinclair/typebox
|
|
420
494
|
ConditionBuilder, // from node-condition-builder
|
|
421
495
|
Expression, // from node-condition-builder
|
|
422
|
-
|
|
496
|
+
|
|
497
|
+
// Table configuration
|
|
423
498
|
defineTable,
|
|
424
499
|
exportTableInfo,
|
|
425
500
|
buildRelation,
|
|
426
501
|
buildUpsertRule,
|
|
427
502
|
buildUpsertRules,
|
|
503
|
+
|
|
504
|
+
// DB layer
|
|
505
|
+
QueryClient, // raw SQL query helper
|
|
506
|
+
createQueryClient, // factory with dialect string
|
|
507
|
+
pgQueryable, // pg pool adapter
|
|
508
|
+
mysqlQueryable, // mysql2 pool adapter
|
|
509
|
+
|
|
510
|
+
// Programmatic high-level API
|
|
511
|
+
createSqlApi, // standalone SqlApi (for scripts/tests)
|
|
512
|
+
setupSwagger, // manual Swagger registration
|
|
513
|
+
} from 'fastify-auto-sqlapi';
|
|
514
|
+
|
|
515
|
+
import type {
|
|
516
|
+
ValidationError, // [field, code] | [field, code, message]
|
|
517
|
+
ValidatorFn,
|
|
518
|
+
BulkValidatorFn,
|
|
519
|
+
JoinRefFilter,
|
|
520
|
+
JoinGroupRequest,
|
|
521
|
+
SearchCondition,
|
|
522
|
+
ConditionMethod,
|
|
428
523
|
} from 'fastify-auto-sqlapi';
|
|
429
524
|
```
|
|
430
525
|
|
|
526
|
+
After registering the plugin, `app.sqlApi` is decorated on the Fastify instance and exposes `search`, `get`, `insert`, `update`, `delete`, `bulkUpsert`, `bulkDelete` for custom routes — same code path as the auto-generated endpoints.
|
|
527
|
+
|
|
431
528
|
## Requirements
|
|
432
529
|
|
|
433
530
|
- Node.js >= 18
|
|
434
|
-
-
|
|
435
|
-
-
|
|
436
|
-
- `@fastify/postgres`
|
|
531
|
+
- Fastify >= 4 (peer dependency)
|
|
532
|
+
- One of:
|
|
533
|
+
- **PostgreSQL** + `pg` + `@fastify/postgres`
|
|
534
|
+
- **MySQL** / **MariaDB** + `mysql2`
|
|
535
|
+
- Optional: `@fastify/swagger` + `@fastify/swagger-ui` for Swagger UI
|
|
437
536
|
|
|
438
537
|
## License
|
|
439
538
|
|
|
@@ -6,7 +6,6 @@ import { buildConnectionString, introspectTables } from '../lib/cli/pg-introspec
|
|
|
6
6
|
import { buildMysqlConnectionConfig, introspectMysqlTables } from '../lib/cli/mysql-introspect.js';
|
|
7
7
|
import { buildTableMap, generateSchemaFile } from '../lib/cli/schema-codegen.js';
|
|
8
8
|
import { loadEnvFile, CONSOLE_COLORS, display, displayAsTableRow, error } from './utils.js';
|
|
9
|
-
loadEnvFile();
|
|
10
9
|
function parseCliArgs() {
|
|
11
10
|
const args = process.argv.slice(2);
|
|
12
11
|
const result = {};
|
|
@@ -27,6 +26,7 @@ async function main() {
|
|
|
27
26
|
display('++++++ fastify-auto-sqlapi: generating schemas ++++++', CONSOLE_COLORS.yellow);
|
|
28
27
|
const cliArgs = parseCliArgs();
|
|
29
28
|
const config = await loadConfig();
|
|
29
|
+
loadEnvFile(config.envFile);
|
|
30
30
|
const dialect = (cliArgs.dialect || config.dialect || 'postgres');
|
|
31
31
|
const schema = config.schema || (dialect === 'postgres' ? 'public' : config.schema || 'public');
|
|
32
32
|
const outputDir = path.resolve(process.cwd(), cliArgs.output || config.outputDir);
|
|
@@ -67,10 +67,10 @@ async function main() {
|
|
|
67
67
|
let untouched = 0;
|
|
68
68
|
const generatedFiles = new Set();
|
|
69
69
|
for (const schemaName of Object.keys(tableMap)) {
|
|
70
|
-
const { name: tableName, fields } = tableMap[schemaName];
|
|
70
|
+
const { name: tableName, fields, colMap } = tableMap[schemaName];
|
|
71
71
|
const filename = path.join(schemasDir, `${schemaName}.ts`);
|
|
72
72
|
generatedFiles.add(`${schemaName}.ts`);
|
|
73
|
-
const content = generateSchemaFile(schemaName, tableName, fields);
|
|
73
|
+
const content = generateSchemaFile(schemaName, tableName, fields, colMap);
|
|
74
74
|
let status = 'created';
|
|
75
75
|
let color = CONSOLE_COLORS.green;
|
|
76
76
|
if (fs.existsSync(filename)) {
|
|
@@ -4,7 +4,6 @@ import * as path from 'node:path';
|
|
|
4
4
|
import { loadConfig } from '../lib/cli/config.js';
|
|
5
5
|
import { parseSchemaFile, generateSingleTableFile, generateDbTablesIndex } from '../lib/cli/tables-codegen.js';
|
|
6
6
|
import { loadEnvFile, CONSOLE_COLORS, display, displayAsTableRow, error } from './utils.js';
|
|
7
|
-
loadEnvFile();
|
|
8
7
|
function printHelp() {
|
|
9
8
|
console.log('');
|
|
10
9
|
display('Usage:', CONSOLE_COLORS.yellow);
|
|
@@ -53,9 +52,10 @@ async function main() {
|
|
|
53
52
|
}
|
|
54
53
|
display('++++++ fastify-auto-sqlapi: generating tables template ++++++', CONSOLE_COLORS.yellow);
|
|
55
54
|
const config = await loadConfig();
|
|
55
|
+
loadEnvFile(config.envFile);
|
|
56
56
|
const outputDir = path.resolve(process.cwd(), cliArgs.output || config.outputDir);
|
|
57
|
-
const schemasDir = outputDir;
|
|
58
|
-
const tablesDir = outputDir;
|
|
57
|
+
const schemasDir = path.join(outputDir, 'schemas');
|
|
58
|
+
const tablesDir = path.join(outputDir, 'tables');
|
|
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/bin/utils.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Load .env file from cwd if it exists.
|
|
3
3
|
* Does not override already-set environment variables.
|
|
4
4
|
*/
|
|
5
|
-
export declare function loadEnvFile(): void;
|
|
5
|
+
export declare function loadEnvFile(envFile?: string): void;
|
|
6
6
|
export declare const CONSOLE_COLORS: {
|
|
7
7
|
readonly black: 30;
|
|
8
8
|
readonly gray: 90;
|
package/dist/bin/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/bin/utils.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,WAAW,
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/bin/utils.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAqBlD;AAED,eAAO,MAAM,cAAc;;;;;;;;CAQjB,CAAC;AAEX,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,MAAU,GAChB,IAAI,CAQN;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,MAAU,GAAG,IAAI,CAE7D;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAExC"}
|
package/dist/bin/utils.js
CHANGED
|
@@ -4,8 +4,8 @@ import { resolve } from 'node:path';
|
|
|
4
4
|
* Load .env file from cwd if it exists.
|
|
5
5
|
* Does not override already-set environment variables.
|
|
6
6
|
*/
|
|
7
|
-
export function loadEnvFile() {
|
|
8
|
-
const envPath = resolve(process.cwd(), '.env');
|
|
7
|
+
export function loadEnvFile(envFile) {
|
|
8
|
+
const envPath = resolve(process.cwd(), envFile || '.env');
|
|
9
9
|
if (!existsSync(envPath))
|
|
10
10
|
return;
|
|
11
11
|
const content = readFileSync(envPath, 'utf-8');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mysql-introspect.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/mysql-introspect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,wBAAgB,0BAA0B,IAAI;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAmBA;AAkDD,wBAAsB,qBAAqB,CACzC,gBAAgB,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAClG,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"mysql-introspect.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/mysql-introspect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,wBAAgB,0BAA0B,IAAI;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAmBA;AAkDD,wBAAsB,qBAAqB,CACzC,gBAAgB,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAClG,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,EAAE,CAAC,CAuCvB"}
|
|
@@ -67,7 +67,8 @@ function mapMysqlType(dataType) {
|
|
|
67
67
|
}
|
|
68
68
|
export async function introspectMysqlTables(connectionConfig, schema) {
|
|
69
69
|
// Dynamic import: mysql2 is optional peer dependency.
|
|
70
|
-
//
|
|
70
|
+
// Try the consumer's CWD first (so it works when installed), then fall back to
|
|
71
|
+
// the plugin's own node_modules (so it works in the plugin's own tests).
|
|
71
72
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
72
73
|
let mysql2;
|
|
73
74
|
try {
|
|
@@ -75,7 +76,13 @@ export async function introspectMysqlTables(connectionConfig, schema) {
|
|
|
75
76
|
mysql2 = require('mysql2/promise');
|
|
76
77
|
}
|
|
77
78
|
catch {
|
|
78
|
-
|
|
79
|
+
try {
|
|
80
|
+
const require = createRequire(import.meta.url);
|
|
81
|
+
mysql2 = require('mysql2/promise');
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new Error('mysql2 is required for MySQL/MariaDB introspection. Install it with: npm install mysql2');
|
|
85
|
+
}
|
|
79
86
|
}
|
|
80
87
|
const connection = await mysql2.createConnection(connectionConfig);
|
|
81
88
|
try {
|
|
@@ -4,5 +4,5 @@ export declare function convertColType(udtName: string, columnInfo: {
|
|
|
4
4
|
is_nullable: string;
|
|
5
5
|
}): string;
|
|
6
6
|
export declare function buildTableMap(rows: ColumnInfo[]): TableMap;
|
|
7
|
-
export declare function generateSchemaFile(schemaName: string, tableName: string, fields: Record<string, string>): string;
|
|
7
|
+
export declare function generateSchemaFile(schemaName: string, tableName: string, fields: Record<string, string>, colMap: Record<string, string>): string;
|
|
8
8
|
//# sourceMappingURL=schema-codegen.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/schema-codegen.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE3D,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE;IAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GACjE,MAAM,CAoDR;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,QAAQ,
|
|
1
|
+
{"version":3,"file":"schema-codegen.d.ts","sourceRoot":"","sources":["../../../src/lib/cli/schema-codegen.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE3D,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE;IAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GACjE,MAAM,CAoDR;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,QAAQ,CAqB1D;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,MAAM,CA2CR"}
|
|
@@ -57,20 +57,26 @@ export function buildTableMap(rows) {
|
|
|
57
57
|
acc[schemaName] = {
|
|
58
58
|
name: row.table_name,
|
|
59
59
|
fields: {},
|
|
60
|
+
colMap: {},
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
+
const fieldName = toCamelCase(row.column_name);
|
|
64
|
+
acc[schemaName].fields[fieldName] = convertColType(row.udt_name, row);
|
|
65
|
+
acc[schemaName].colMap[fieldName] = row.column_name;
|
|
63
66
|
return acc;
|
|
64
67
|
}, {});
|
|
65
68
|
}
|
|
66
|
-
export function generateSchemaFile(schemaName, tableName, fields) {
|
|
69
|
+
export function generateSchemaFile(schemaName, tableName, fields, colMap) {
|
|
67
70
|
const fieldsStr = JSON.stringify(fields, null, 2).replace(/"(.*?)"\s*:\s*"Type(.*?)"/g, '$1: Type$2');
|
|
71
|
+
const colMapStr = JSON.stringify(colMap, null, 2);
|
|
68
72
|
return `// Auto-generated by fastify-auto-sqlapi. Do not edit.
|
|
69
|
-
import { Type
|
|
73
|
+
import { Type } from "fastify-auto-sqlapi";
|
|
70
74
|
import type { Static } from "fastify-auto-sqlapi";
|
|
71
75
|
|
|
72
76
|
const _Schema = ${fieldsStr};
|
|
73
77
|
|
|
78
|
+
const _colMap: Record<string, string> = ${colMapStr};
|
|
79
|
+
|
|
74
80
|
const validation = Type.Object(_Schema, { $id: "${schemaName}" });
|
|
75
81
|
const partialValidation = Type.Object(
|
|
76
82
|
Object.fromEntries(
|
|
@@ -83,7 +89,8 @@ const partialValidation = Type.Object(
|
|
|
83
89
|
);
|
|
84
90
|
|
|
85
91
|
export const Schema = {
|
|
86
|
-
col: (f: keyof typeof _Schema) =>
|
|
92
|
+
col: (f: keyof typeof _Schema) => _colMap[f as string] ?? f,
|
|
93
|
+
colMap: _colMap,
|
|
87
94
|
fields: _Schema,
|
|
88
95
|
validation,
|
|
89
96
|
tableName: "${tableName}",
|
|
@@ -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,CAwHhG;AAID,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAqBrE;AAID,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CA6FlE"}
|
|
@@ -98,6 +98,7 @@ export function generateSingleTableFile(schema, allSchemas) {
|
|
|
98
98
|
const lines = [];
|
|
99
99
|
// Imports
|
|
100
100
|
lines.push(`import {defineTable, exportTableInfo, Type} from 'fastify-auto-sqlapi';`);
|
|
101
|
+
lines.push(`import type {ValidationError} from 'fastify-auto-sqlapi';`);
|
|
101
102
|
lines.push(`import {${schema.schemaName} as Schema} from '../schemas/${schema.schemaName}';`);
|
|
102
103
|
// Commented imports for related schemas
|
|
103
104
|
for (const relSchema of relatedSchemas) {
|
|
@@ -157,7 +158,7 @@ export function generateSingleTableFile(schema, allSchemas) {
|
|
|
157
158
|
// validate: use first non-PK field for example
|
|
158
159
|
const exampleField = schema.fields.find(f => f !== pk) || schema.fields[0];
|
|
159
160
|
lines.push(` validate: async (db, req, main, secondaries) => {`);
|
|
160
|
-
lines.push(` const errors = [];`);
|
|
161
|
+
lines.push(` const errors: ValidationError[] = [];`);
|
|
161
162
|
lines.push(` // if (!main.${exampleField}) errors.push(['${exampleField}', 'required']);`);
|
|
162
163
|
lines.push(` return errors;`);
|
|
163
164
|
lines.push(` },`);
|
|
@@ -209,7 +210,7 @@ export function generateTablesFile(schemas) {
|
|
|
209
210
|
const lines = [];
|
|
210
211
|
// Imports
|
|
211
212
|
lines.push(`import {defineTable, exportTableInfo, Type} from 'fastify-auto-sqlapi';`);
|
|
212
|
-
lines.push(`import type { DbTables } from 'fastify-auto-sqlapi';`);
|
|
213
|
+
lines.push(`import type { DbTables, ValidationError } from 'fastify-auto-sqlapi';`);
|
|
213
214
|
lines.push(``);
|
|
214
215
|
// Schema imports
|
|
215
216
|
for (const schema of schemas) {
|
|
@@ -247,7 +248,7 @@ export function generateTablesFile(schemas) {
|
|
|
247
248
|
lines.push(` // schemaOverrides: {},`);
|
|
248
249
|
const exampleFieldLegacy = schema.fields.find(f => f !== pk) || schema.fields[0];
|
|
249
250
|
lines.push(` validate: async (db, req, main, secondaries) => {`);
|
|
250
|
-
lines.push(` const errors = [];`);
|
|
251
|
+
lines.push(` const errors: ValidationError[] = [];`);
|
|
251
252
|
lines.push(` // if (!main.${exampleFieldLegacy}) errors.push(['${exampleFieldLegacy}', 'required']);`);
|
|
252
253
|
lines.push(` return errors;`);
|
|
253
254
|
lines.push(` },`);
|