fastify-auto-sqlapi 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +422 -0
- package/dist/bin/generate-schema.d.ts +3 -0
- package/dist/bin/generate-schema.d.ts.map +1 -0
- package/dist/bin/generate-schema.js +122 -0
- package/dist/bin/generate-tables.d.ts +3 -0
- package/dist/bin/generate-tables.d.ts.map +1 -0
- package/dist/bin/generate-tables.js +135 -0
- package/dist/bin/utils.d.ts +18 -0
- package/dist/bin/utils.d.ts.map +1 -0
- package/dist/bin/utils.js +54 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/lib/adapters/mysql-adapter.d.ts +9 -0
- package/dist/lib/adapters/mysql-adapter.d.ts.map +1 -0
- package/dist/lib/adapters/mysql-adapter.js +21 -0
- package/dist/lib/adapters/pg-adapter.d.ts +9 -0
- package/dist/lib/adapters/pg-adapter.d.ts.map +1 -0
- package/dist/lib/adapters/pg-adapter.js +11 -0
- package/dist/lib/cli/config.d.ts +3 -0
- package/dist/lib/cli/config.d.ts.map +1 -0
- package/dist/lib/cli/config.js +29 -0
- package/dist/lib/cli/mysql-introspect.d.ts +16 -0
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -0
- package/dist/lib/cli/mysql-introspect.js +98 -0
- package/dist/lib/cli/pg-introspect.d.ts +4 -0
- package/dist/lib/cli/pg-introspect.d.ts.map +1 -0
- package/dist/lib/cli/pg-introspect.js +36 -0
- package/dist/lib/cli/schema-codegen.d.ts +8 -0
- package/dist/lib/cli/schema-codegen.d.ts.map +1 -0
- package/dist/lib/cli/schema-codegen.js +100 -0
- package/dist/lib/cli/tables-codegen.d.ts +18 -0
- package/dist/lib/cli/tables-codegen.d.ts.map +1 -0
- package/dist/lib/cli/tables-codegen.js +249 -0
- package/dist/lib/condition-methods.d.ts +11 -0
- package/dist/lib/condition-methods.d.ts.map +1 -0
- package/dist/lib/condition-methods.js +20 -0
- package/dist/lib/db.d.ts +28 -0
- package/dist/lib/db.d.ts.map +1 -0
- package/dist/lib/db.js +224 -0
- package/dist/lib/dialect.d.ts +17 -0
- package/dist/lib/dialect.d.ts.map +1 -0
- package/dist/lib/dialect.js +56 -0
- package/dist/lib/engine/bulk/bulk-delete.d.ts +3 -0
- package/dist/lib/engine/bulk/bulk-delete.d.ts.map +1 -0
- package/dist/lib/engine/bulk/bulk-delete.js +30 -0
- package/dist/lib/engine/bulk/bulk-upsert.d.ts +3 -0
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -0
- package/dist/lib/engine/bulk/bulk-upsert.js +74 -0
- package/dist/lib/engine/rest/delete.d.ts +3 -0
- package/dist/lib/engine/rest/delete.d.ts.map +1 -0
- package/dist/lib/engine/rest/delete.js +25 -0
- package/dist/lib/engine/rest/get.d.ts +3 -0
- package/dist/lib/engine/rest/get.d.ts.map +1 -0
- package/dist/lib/engine/rest/get.js +32 -0
- package/dist/lib/engine/rest/insert.d.ts +3 -0
- package/dist/lib/engine/rest/insert.d.ts.map +1 -0
- package/dist/lib/engine/rest/insert.js +58 -0
- package/dist/lib/engine/rest/update.d.ts +3 -0
- package/dist/lib/engine/rest/update.d.ts.map +1 -0
- package/dist/lib/engine/rest/update.js +110 -0
- package/dist/lib/engine/search/search.d.ts +3 -0
- package/dist/lib/engine/search/search.d.ts.map +1 -0
- package/dist/lib/engine/search/search.js +300 -0
- package/dist/lib/engine/write-helpers.d.ts +8 -0
- package/dist/lib/engine/write-helpers.d.ts.map +1 -0
- package/dist/lib/engine/write-helpers.js +83 -0
- package/dist/lib/naming.d.ts +6 -0
- package/dist/lib/naming.d.ts.map +1 -0
- package/dist/lib/naming.js +23 -0
- package/dist/lib/schema/bulk-delete.d.ts +10 -0
- package/dist/lib/schema/bulk-delete.d.ts.map +1 -0
- package/dist/lib/schema/bulk-delete.js +16 -0
- package/dist/lib/schema/bulk-upsert.d.ts +5 -0
- package/dist/lib/schema/bulk-upsert.d.ts.map +1 -0
- package/dist/lib/schema/bulk-upsert.js +43 -0
- package/dist/lib/schema/helpers.d.ts +14 -0
- package/dist/lib/schema/helpers.d.ts.map +1 -0
- package/dist/lib/schema/helpers.js +33 -0
- package/dist/lib/schema/insert.d.ts +5 -0
- package/dist/lib/schema/insert.d.ts.map +1 -0
- package/dist/lib/schema/insert.js +43 -0
- package/dist/lib/schema/search.d.ts +14 -0
- package/dist/lib/schema/search.d.ts.map +1 -0
- package/dist/lib/schema/search.js +99 -0
- package/dist/lib/schema/update.d.ts +5 -0
- package/dist/lib/schema/update.d.ts.map +1 -0
- package/dist/lib/schema/update.js +47 -0
- package/dist/lib/setup-swagger.d.ts +4 -0
- package/dist/lib/setup-swagger.d.ts.map +1 -0
- package/dist/lib/setup-swagger.js +27 -0
- package/dist/lib/sql-api.d.ts +49 -0
- package/dist/lib/sql-api.d.ts.map +1 -0
- package/dist/lib/sql-api.js +124 -0
- package/dist/lib/table-helpers.d.ts +17 -0
- package/dist/lib/table-helpers.d.ts.map +1 -0
- package/dist/lib/table-helpers.js +34 -0
- package/dist/lib/tenant.d.ts +19 -0
- package/dist/lib/tenant.d.ts.map +1 -0
- package/dist/lib/tenant.js +110 -0
- package/dist/routes/auto/bulk-delete.routes.d.ts +4 -0
- package/dist/routes/auto/bulk-delete.routes.d.ts.map +1 -0
- package/dist/routes/auto/bulk-delete.routes.js +27 -0
- package/dist/routes/auto/bulk-upsert.routes.d.ts +4 -0
- package/dist/routes/auto/bulk-upsert.routes.d.ts.map +1 -0
- package/dist/routes/auto/bulk-upsert.routes.js +25 -0
- package/dist/routes/auto/delete.routes.d.ts +4 -0
- package/dist/routes/auto/delete.routes.d.ts.map +1 -0
- package/dist/routes/auto/delete.routes.js +29 -0
- package/dist/routes/auto/get.routes.d.ts +4 -0
- package/dist/routes/auto/get.routes.d.ts.map +1 -0
- package/dist/routes/auto/get.routes.js +26 -0
- package/dist/routes/auto/insert.routes.d.ts +4 -0
- package/dist/routes/auto/insert.routes.d.ts.map +1 -0
- package/dist/routes/auto/insert.routes.js +29 -0
- package/dist/routes/auto/plugin.d.ts +11 -0
- package/dist/routes/auto/plugin.d.ts.map +1 -0
- package/dist/routes/auto/plugin.js +81 -0
- package/dist/routes/auto/route-helpers.d.ts +7 -0
- package/dist/routes/auto/route-helpers.d.ts.map +1 -0
- package/dist/routes/auto/route-helpers.js +12 -0
- package/dist/routes/auto/search.routes.d.ts +4 -0
- package/dist/routes/auto/search.routes.d.ts.map +1 -0
- package/dist/routes/auto/search.routes.js +50 -0
- package/dist/routes/auto/update.routes.d.ts +4 -0
- package/dist/routes/auto/update.routes.d.ts.map +1 -0
- package/dist/routes/auto/update.routes.js +30 -0
- package/dist/types.d.ts +240 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/package.json +106 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luca Rainone (luca.rainone@gmail.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# fastify-auto-sqlapi
|
|
2
|
+
|
|
3
|
+
Automatic CRUD API generation from PostgreSQL tables for [Fastify](https://fastify.dev/). No ORM, no magic — just raw SQL via `pg`, with TypeBox validation and Swagger docs out of the box.
|
|
4
|
+
|
|
5
|
+
Point it at your database, and get a full REST API with search, pagination, joins, bulk operations, multi-tenant isolation, and hooks.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Zero boilerplate** — define your tables, get 7 endpoints each
|
|
10
|
+
- **No ORM** — raw SQL via `pg` + parameterized queries
|
|
11
|
+
- **TypeBox validation** — request/response schemas auto-generated from your DB
|
|
12
|
+
- **Virtual joins** — fetch related records without complex SQL
|
|
13
|
+
- **Bulk operations** — batch insert/upsert/delete in single queries
|
|
14
|
+
- **Multi-tenant** — automatic row-level isolation, zero code in handlers
|
|
15
|
+
- **Hooks** — `beforeInsert`, `beforeUpdate`, `afterInsert` for custom logic
|
|
16
|
+
- **Swagger UI** — optional, auto-configured from your schemas
|
|
17
|
+
- **Composable** — register all routes or pick only what you need
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### 1. Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install fastify-auto-sqlapi fastify @fastify/postgres
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 2. Create the config file
|
|
28
|
+
|
|
29
|
+
Create `sqlapi.config.ts` in your project root. This is used only by the CLI generators, not at runtime.
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
export default {
|
|
33
|
+
outputDir: './src/schemas', // where to generate Schema files
|
|
34
|
+
schema: 'public', // PostgreSQL schema to introspect
|
|
35
|
+
};
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The DB connection is read from environment variables:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Either a full connection string:
|
|
42
|
+
DATABASE_URL=postgres://user:pass@localhost:5432/mydb
|
|
43
|
+
|
|
44
|
+
# Or individual vars:
|
|
45
|
+
POSTGRES_HOST=127.0.0.1
|
|
46
|
+
POSTGRES_PORT=5432
|
|
47
|
+
POSTGRES_USER=myuser
|
|
48
|
+
POSTGRES_PASSWORD=mypassword
|
|
49
|
+
POSTGRES_DB=mydb
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 3. Generate schemas from your database
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx sqlapi-generate-schema
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This introspects your tables and generates one TypeBox schema file per table (e.g. `SchemaCustomer.ts`, `SchemaOrder.ts`). These files are auto-generated — don't edit them.
|
|
59
|
+
|
|
60
|
+
### 4. Generate the tables template
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx sqlapi-generate-tables
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This creates a `tables.ts` file with a `defineTable()` call for each table. It auto-detects primary keys, foreign key relations, and lays out all available options as commented code. **This file is yours to customize.**
|
|
67
|
+
|
|
68
|
+
### 5. Register the plugin
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import Fastify from 'fastify';
|
|
72
|
+
import fastifyPostgres from '@fastify/postgres';
|
|
73
|
+
import { fastifyAutoSqlApi } from 'fastify-auto-sqlapi';
|
|
74
|
+
import { dbTables } from './src/schemas/tables.js';
|
|
75
|
+
|
|
76
|
+
const app = Fastify();
|
|
77
|
+
|
|
78
|
+
await app.register(fastifyPostgres, {
|
|
79
|
+
connectionString: 'postgres://user:pass@localhost:5432/mydb',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await app.register(fastifyAutoSqlApi, {
|
|
83
|
+
DbTables: dbTables,
|
|
84
|
+
swagger: true,
|
|
85
|
+
prefix: '/api',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await app.listen({ port: 3000 });
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
That's it. For a table called `customer`, you now have:
|
|
92
|
+
|
|
93
|
+
| Method | URL | Description |
|
|
94
|
+
|--------|-----|-------------|
|
|
95
|
+
| `POST` | `/api/search/customer` | Search with filters, pagination, joins |
|
|
96
|
+
| `GET` | `/api/rest/customer/:id` | Get a single record by primary key |
|
|
97
|
+
| `POST` | `/api/rest/customer` | Insert a new record |
|
|
98
|
+
| `PUT` | `/api/rest/customer` | Update a record |
|
|
99
|
+
| `DELETE` | `/api/rest/customer/:id` | Delete a record |
|
|
100
|
+
| `PUT` | `/api/bulk/customer` | Bulk upsert (array of records) |
|
|
101
|
+
| `POST` | `/api/bulk/customer/delete` | Bulk delete (array of PKs) |
|
|
102
|
+
|
|
103
|
+
> **Note:** Search uses `POST` because filters are passed as JSON in the request body.
|
|
104
|
+
|
|
105
|
+
## Table Configuration
|
|
106
|
+
|
|
107
|
+
Tables are configured with `defineTable()`. The only required fields are `primary` and the output of `exportTableInfo()`:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import { defineTable, exportTableInfo } from 'fastify-auto-sqlapi';
|
|
111
|
+
import { SchemaCustomer } from './SchemaCustomer.js';
|
|
112
|
+
|
|
113
|
+
const TableCustomer = defineTable({
|
|
114
|
+
primary: 'id',
|
|
115
|
+
...exportTableInfo(SchemaCustomer),
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`exportTableInfo()` provides the schema, a filter builder (auto-generates WHERE clauses from request fields), and extra filter definitions.
|
|
120
|
+
|
|
121
|
+
Export all your tables as a single record:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
export const dbTables = {
|
|
125
|
+
customer: TableCustomer,
|
|
126
|
+
order: TableOrder,
|
|
127
|
+
};
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The keys in this record (`customer`, `order`) become the table names in the URL paths.
|
|
131
|
+
|
|
132
|
+
### All available options
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const TableCustomer = defineTable({
|
|
136
|
+
// Required
|
|
137
|
+
primary: 'id',
|
|
138
|
+
...exportTableInfo(SchemaCustomer),
|
|
139
|
+
|
|
140
|
+
// Ordering & filtering
|
|
141
|
+
defaultOrder: 'name', // default ORDER BY
|
|
142
|
+
excludeFromCreation: ['id'], // omit from INSERT (e.g. auto-increment)
|
|
143
|
+
distinctResults: true, // SELECT DISTINCT
|
|
144
|
+
|
|
145
|
+
// Relations
|
|
146
|
+
allowedReadJoins: [
|
|
147
|
+
buildRelation(SchemaCustomer, 'id', SchemaOrder, 'customerId'),
|
|
148
|
+
],
|
|
149
|
+
allowedWriteJoins: [
|
|
150
|
+
buildRelation(SchemaCustomer, 'id', SchemaOrder, 'customerId'),
|
|
151
|
+
],
|
|
152
|
+
|
|
153
|
+
// Upsert (ON CONFLICT)
|
|
154
|
+
upsertMap: buildUpsertRules(
|
|
155
|
+
buildUpsertRule(SchemaCustomer, ['id']),
|
|
156
|
+
),
|
|
157
|
+
|
|
158
|
+
// Multi-tenant isolation
|
|
159
|
+
tenantScope: { column: 'organization_id' },
|
|
160
|
+
|
|
161
|
+
// Hooks
|
|
162
|
+
beforeInsert: async (db, req, record) => { /* record is snake_case */ },
|
|
163
|
+
beforeUpdate: async (db, req, fields) => { /* fields is snake_case */ },
|
|
164
|
+
afterInsert: async (db, req, record, secondaryRecords) => { /* record is camelCase */ },
|
|
165
|
+
|
|
166
|
+
// Auth (per-table)
|
|
167
|
+
onRequests: [
|
|
168
|
+
async (request, reply) => {
|
|
169
|
+
if (!request.user) return reply.status(401).send({ error: 'Unauthorized' });
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Custom filters (extraFilters)
|
|
176
|
+
|
|
177
|
+
For filters that don't map to real columns (e.g. a search `q` field):
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { Type, ConditionBuilder } from 'fastify-auto-sqlapi';
|
|
181
|
+
|
|
182
|
+
const TableCustomer = defineTable({
|
|
183
|
+
primary: 'id',
|
|
184
|
+
...exportTableInfo(
|
|
185
|
+
SchemaCustomer,
|
|
186
|
+
{ q: Type.Optional(Type.String()) }, // extra filter definition
|
|
187
|
+
(condition, filters) => { // custom condition builder
|
|
188
|
+
if (filters.q) {
|
|
189
|
+
const or = new ConditionBuilder('OR');
|
|
190
|
+
or.isILike('name', `%${filters.q}%`);
|
|
191
|
+
or.isILike('email', `%${filters.q}%`);
|
|
192
|
+
condition.append(or);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
),
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Plugin Options
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
await app.register(fastifyAutoSqlApi, {
|
|
203
|
+
DbTables: dbTables, // Required — your table definitions
|
|
204
|
+
prefix: '/api', // Optional — URL prefix for all routes
|
|
205
|
+
swagger: true, // Optional — enable Swagger UI (or pass SwaggerOptions)
|
|
206
|
+
onRequests: [authMiddleware], // Optional — global hooks applied to every route
|
|
207
|
+
getTenantId: (req) => id, // Optional — multi-tenant function
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
| Option | Type | Required | Description |
|
|
212
|
+
|--------|------|----------|-------------|
|
|
213
|
+
| `DbTables` | `Record<string, ITable>` | Yes | Table configurations |
|
|
214
|
+
| `prefix` | `string` | No | URL prefix (e.g. `/api`) |
|
|
215
|
+
| `swagger` | `boolean \| SwaggerOptions` | No | Enable Swagger UI |
|
|
216
|
+
| `onRequests` | `Function[]` | No | Global auth/middleware hooks |
|
|
217
|
+
| `getTenantId` | `(req) => id \| null` | No | Tenant resolver for multi-tenant |
|
|
218
|
+
|
|
219
|
+
## API Reference
|
|
220
|
+
|
|
221
|
+
### POST /search/{table}
|
|
222
|
+
|
|
223
|
+
Search with filters, pagination, ordering, joins, and aggregations.
|
|
224
|
+
|
|
225
|
+
**Request body** (all fields optional):
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"filters": { "name": "Mario" },
|
|
230
|
+
"joins": { "order": {} },
|
|
231
|
+
"paginator": { "page": 1, "itemsPerPage": 20 }
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Querystring** (optional): `orderBy`, `page`, `itemsPerPage`, `computeMin`, `computeMax`, `computeSum`, `computeAvg`
|
|
236
|
+
|
|
237
|
+
**Response:**
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
{
|
|
241
|
+
"table": "customer",
|
|
242
|
+
"main": [{ "id": 1, "name": "Mario", "email": "m@test.it" }],
|
|
243
|
+
"joins": { "order": [{ "id": 10, "customerId": 1, "total": 50 }] },
|
|
244
|
+
"pagination": { "total": 25, "pages": 3, "paginator": { "page": 1, "itemsPerPage": 20 } }
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`joins`, `joinGroups`, and `pagination` only appear when requested. A simple `{}` body returns `{ table, main }`.
|
|
249
|
+
|
|
250
|
+
### GET /rest/{table}/:id
|
|
251
|
+
|
|
252
|
+
Returns `{ main: { ... } }` or 404.
|
|
253
|
+
|
|
254
|
+
### POST /rest/{table}
|
|
255
|
+
|
|
256
|
+
**Body:**
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"main": { "name": "Mario", "email": "m@test.it" },
|
|
261
|
+
"secondaries": {
|
|
262
|
+
"order": [{ "total": 50, "status": "pending" }]
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`secondaries` is optional. FK fields are auto-filled from the inserted main record.
|
|
268
|
+
|
|
269
|
+
**Response (201):** `{ main: { ... }, secondaries: { ... } }`
|
|
270
|
+
|
|
271
|
+
### PUT /rest/{table}
|
|
272
|
+
|
|
273
|
+
**Body:**
|
|
274
|
+
|
|
275
|
+
```json
|
|
276
|
+
{
|
|
277
|
+
"main": { "id": 1, "name": "Updated Name" },
|
|
278
|
+
"secondaries": { "order": [{ "total": 75 }] },
|
|
279
|
+
"deletions": { "order": [{ "id": 10 }] }
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`main` must include the primary key. `secondaries` and `deletions` are optional.
|
|
284
|
+
|
|
285
|
+
### DELETE /rest/{table}/:id
|
|
286
|
+
|
|
287
|
+
Returns the deleted record or 404.
|
|
288
|
+
|
|
289
|
+
### PUT /bulk/{table}
|
|
290
|
+
|
|
291
|
+
**Body:** Array of `{ main, secondaries?, deletions? }`. All main records are inserted/upserted in a single SQL query.
|
|
292
|
+
|
|
293
|
+
### POST /bulk/{table}/delete
|
|
294
|
+
|
|
295
|
+
**Body:** Array of objects with the PK field, e.g. `[{ "id": 1 }, { "id": 2 }]`. Executes as a single `DELETE WHERE pk IN (...)`.
|
|
296
|
+
|
|
297
|
+
## Multi-Tenant
|
|
298
|
+
|
|
299
|
+
Automatic row-level isolation on all CRUD operations. Configure once, no code in handlers.
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
await app.register(fastifyAutoSqlApi, {
|
|
303
|
+
DbTables: dbTables,
|
|
304
|
+
getTenantId: (request) => request.user?.organizationId ?? null,
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
When `getTenantId` returns `null`, no filtering is applied (admin mode).
|
|
309
|
+
|
|
310
|
+
### Direct tenant (column on the table)
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
defineTable({
|
|
314
|
+
primary: 'id',
|
|
315
|
+
...exportTableInfo(SchemaCustomer),
|
|
316
|
+
tenantScope: { column: 'organization_id' },
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Indirect tenant (via parent table)
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
defineTable({
|
|
324
|
+
primary: 'id',
|
|
325
|
+
...exportTableInfo(SchemaOrder),
|
|
326
|
+
tenantScope: {
|
|
327
|
+
column: 'organization_id',
|
|
328
|
+
through: { schema: SchemaCustomer, localField: 'customer_id', foreignField: 'id' },
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Tables without `tenantScope` are unaffected.
|
|
334
|
+
|
|
335
|
+
## Swagger
|
|
336
|
+
|
|
337
|
+
Enabled by passing `swagger: true` (or a config object) to the plugin options. Requires `@fastify/swagger` and `@fastify/swagger-ui` as peer dependencies.
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
npm install @fastify/swagger @fastify/swagger-ui
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
await app.register(fastifyAutoSqlApi, {
|
|
345
|
+
DbTables: dbTables,
|
|
346
|
+
swagger: {
|
|
347
|
+
title: 'My API',
|
|
348
|
+
description: 'Auto-generated CRUD API',
|
|
349
|
+
version: '1.0.0',
|
|
350
|
+
routePrefix: '/docs',
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
If the swagger packages are not installed, the plugin logs a warning and continues without Swagger.
|
|
356
|
+
|
|
357
|
+
## Granular Composition
|
|
358
|
+
|
|
359
|
+
Instead of registering the all-in-one plugin, you can register individual route plugins for more control:
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
import {
|
|
363
|
+
searchRoutes, getRoutes, insertRoutes, updateRoutes,
|
|
364
|
+
deleteRoutes, bulkUpsertRoutes, bulkDeleteRoutes, setupSwagger,
|
|
365
|
+
} from 'fastify-auto-sqlapi';
|
|
366
|
+
|
|
367
|
+
// Read-only API
|
|
368
|
+
await app.register(async (instance) => {
|
|
369
|
+
await setupSwagger(instance, { swagger: true });
|
|
370
|
+
const opts = { DbTables: dbTables };
|
|
371
|
+
await instance.register(searchRoutes, opts);
|
|
372
|
+
await instance.register(getRoutes, opts);
|
|
373
|
+
}, { prefix: '/public' });
|
|
374
|
+
|
|
375
|
+
// Full CRUD with auth
|
|
376
|
+
await app.register(async (instance) => {
|
|
377
|
+
const opts = { DbTables: dbTables, onRequests: [authMiddleware] };
|
|
378
|
+
await instance.register(searchRoutes, opts);
|
|
379
|
+
await instance.register(getRoutes, opts);
|
|
380
|
+
await instance.register(insertRoutes, opts);
|
|
381
|
+
await instance.register(updateRoutes, opts);
|
|
382
|
+
await instance.register(deleteRoutes, opts);
|
|
383
|
+
await instance.register(bulkUpsertRoutes, opts);
|
|
384
|
+
await instance.register(bulkDeleteRoutes, opts);
|
|
385
|
+
}, { prefix: '/admin' });
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Conventions
|
|
389
|
+
|
|
390
|
+
- **camelCase in API, snake_case in DB** — the plugin converts automatically
|
|
391
|
+
- **Joins are virtual** — they execute separate `SELECT ... WHERE fk IN (...)` queries, not SQL JOINs
|
|
392
|
+
- **Hooks receive snake_case** — `beforeInsert` and `beforeUpdate` get snake_case records; `afterInsert` gets camelCase
|
|
393
|
+
- **All response fields are Optional** — response schemas use `Type.Partial` since `RETURNING *` may return any subset
|
|
394
|
+
|
|
395
|
+
## Re-exports
|
|
396
|
+
|
|
397
|
+
The package re-exports commonly needed utilities so you don't need to install them separately:
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
import {
|
|
401
|
+
Type, // from @sinclair/typebox
|
|
402
|
+
ConditionBuilder, // from node-condition-builder
|
|
403
|
+
Expression, // from node-condition-builder
|
|
404
|
+
QueryClient, // raw SQL query helper
|
|
405
|
+
defineTable,
|
|
406
|
+
exportTableInfo,
|
|
407
|
+
buildRelation,
|
|
408
|
+
buildUpsertRule,
|
|
409
|
+
buildUpsertRules,
|
|
410
|
+
} from 'fastify-auto-sqlapi';
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Requirements
|
|
414
|
+
|
|
415
|
+
- Node.js >= 18
|
|
416
|
+
- PostgreSQL
|
|
417
|
+
- Fastify >= 4
|
|
418
|
+
- `@fastify/postgres` >= 5
|
|
419
|
+
|
|
420
|
+
## License
|
|
421
|
+
|
|
422
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-schema.d.ts","sourceRoot":"","sources":["../../src/bin/generate-schema.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { loadConfig } from '../lib/cli/config.js';
|
|
5
|
+
import { buildConnectionString, introspectTables } from '../lib/cli/pg-introspect.js';
|
|
6
|
+
import { buildMysqlConnectionConfig, introspectMysqlTables } from '../lib/cli/mysql-introspect.js';
|
|
7
|
+
import { buildTableMap, generateSchemaFile } from '../lib/cli/schema-codegen.js';
|
|
8
|
+
import { loadEnvFile, CONSOLE_COLORS, display, displayAsTableRow, error } from './utils.js';
|
|
9
|
+
loadEnvFile();
|
|
10
|
+
function parseCliArgs() {
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const result = {};
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
if (args[i] === '--output' && i + 1 < args.length) {
|
|
15
|
+
result.output = args[++i];
|
|
16
|
+
}
|
|
17
|
+
else if (args[i] === '--tables' && i + 1 < args.length) {
|
|
18
|
+
result.tables = args[++i].split(',').map((t) => t.trim()).filter(Boolean);
|
|
19
|
+
}
|
|
20
|
+
else if (args[i] === '--dialect' && i + 1 < args.length) {
|
|
21
|
+
result.dialect = args[++i];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
async function main() {
|
|
27
|
+
display('++++++ fastify-auto-sqlapi: generating schemas ++++++', CONSOLE_COLORS.yellow);
|
|
28
|
+
const cliArgs = parseCliArgs();
|
|
29
|
+
const config = await loadConfig();
|
|
30
|
+
const dialect = (cliArgs.dialect || config.dialect || 'postgres');
|
|
31
|
+
const schema = config.schema || (dialect === 'postgres' ? 'public' : config.schema || 'public');
|
|
32
|
+
const outputDir = path.resolve(process.cwd(), cliArgs.output || config.outputDir);
|
|
33
|
+
const schemasDir = path.join(outputDir, 'schemas');
|
|
34
|
+
if (!fs.existsSync(schemasDir)) {
|
|
35
|
+
fs.mkdirSync(schemasDir, { recursive: true });
|
|
36
|
+
display(`Created directory: ${schemasDir}`, CONSOLE_COLORS.green);
|
|
37
|
+
}
|
|
38
|
+
let rows;
|
|
39
|
+
if (dialect === 'mysql' || dialect === 'mariadb') {
|
|
40
|
+
const connConfig = buildMysqlConnectionConfig();
|
|
41
|
+
rows = await introspectMysqlTables(connConfig, connConfig.database);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const connectionString = buildConnectionString();
|
|
45
|
+
rows = await introspectTables(connectionString, schema);
|
|
46
|
+
}
|
|
47
|
+
if (rows.length === 0) {
|
|
48
|
+
display(`No tables found in schema "${schema}".`, CONSOLE_COLORS.magenta);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const tableMap = buildTableMap(rows);
|
|
52
|
+
// Filter tables if --tables flag is provided
|
|
53
|
+
const tableNames = cliArgs.tables;
|
|
54
|
+
if (tableNames) {
|
|
55
|
+
for (const schemaName of Object.keys(tableMap)) {
|
|
56
|
+
if (!tableNames.includes(tableMap[schemaName].name)) {
|
|
57
|
+
delete tableMap[schemaName];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (Object.keys(tableMap).length === 0) {
|
|
61
|
+
display(`No matching tables found for: ${tableNames.join(', ')}`, CONSOLE_COLORS.magenta);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let somethingCreated = false;
|
|
66
|
+
let somethingTouched = false;
|
|
67
|
+
let untouched = 0;
|
|
68
|
+
const generatedFiles = new Set();
|
|
69
|
+
for (const schemaName of Object.keys(tableMap)) {
|
|
70
|
+
const { name: tableName, fields } = tableMap[schemaName];
|
|
71
|
+
const filename = path.join(schemasDir, `${schemaName}.ts`);
|
|
72
|
+
generatedFiles.add(`${schemaName}.ts`);
|
|
73
|
+
const content = generateSchemaFile(schemaName, tableName, fields);
|
|
74
|
+
let status = 'created';
|
|
75
|
+
let color = CONSOLE_COLORS.green;
|
|
76
|
+
if (fs.existsSync(filename)) {
|
|
77
|
+
status = 'untouched';
|
|
78
|
+
color = CONSOLE_COLORS.gray;
|
|
79
|
+
if (fs.readFileSync(filename, 'utf-8') !== content) {
|
|
80
|
+
status = 'updated';
|
|
81
|
+
color = CONSOLE_COLORS.cyan;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
somethingCreated = true;
|
|
86
|
+
}
|
|
87
|
+
if (status !== 'untouched') {
|
|
88
|
+
fs.writeFileSync(filename, content);
|
|
89
|
+
displayAsTableRow(filename, status, 90, color);
|
|
90
|
+
somethingTouched = true;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
untouched++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Remove orphan Schema*.ts files (only when generating all tables)
|
|
97
|
+
if (!tableNames) {
|
|
98
|
+
const existingFiles = fs.readdirSync(schemasDir);
|
|
99
|
+
for (const file of existingFiles) {
|
|
100
|
+
if (file.startsWith('Schema') && file.endsWith('.ts') && !generatedFiles.has(file)) {
|
|
101
|
+
const filePath = path.join(schemasDir, file);
|
|
102
|
+
fs.unlinkSync(filePath);
|
|
103
|
+
displayAsTableRow(filePath, 'removed', 90, CONSOLE_COLORS.red);
|
|
104
|
+
somethingTouched = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
console.log('');
|
|
109
|
+
if (untouched > 0) {
|
|
110
|
+
display(`${untouched} schema(s) untouched`, CONSOLE_COLORS.gray);
|
|
111
|
+
}
|
|
112
|
+
if (somethingCreated) {
|
|
113
|
+
display("Don't forget to git add your new Schema files.", CONSOLE_COLORS.magenta);
|
|
114
|
+
}
|
|
115
|
+
if (!somethingTouched) {
|
|
116
|
+
display('All schemas are already up to date.', CONSOLE_COLORS.magenta);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
main().catch((e) => {
|
|
120
|
+
error(`Error: ${e.message}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-tables.d.ts","sourceRoot":"","sources":["../../src/bin/generate-tables.ts"],"names":[],"mappings":""}
|