schematic-pg 0.1.12 → 0.1.14
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/README.md +3 -3
- package/dist/api-generator/openapi-generator.js +32 -23
- package/dist/cli/templates/agents.md +1 -1
- package/dist/cli/templates.d.ts +1 -1
- package/dist/cli/templates.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/db/bootstrap.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -182,7 +182,7 @@ models {
|
|
|
182
182
|
|
|
183
183
|
model User {
|
|
184
184
|
id: UUID @id @default(gen_random_uuid())
|
|
185
|
-
email: VARCHAR(255) @unique
|
|
185
|
+
email: VARCHAR(255) @unique @regex(pattern: "^[\\w.-]+@[\\w.-]+\\.\\w+$", message: "Invalid email address")
|
|
186
186
|
name: VARCHAR(150)
|
|
187
187
|
role: UserRole @default(USER)
|
|
188
188
|
age: SMALLINT?
|
|
@@ -449,14 +449,14 @@ npm run start # schematic-pg start
|
|
|
449
449
|
|
|
450
450
|
```bash
|
|
451
451
|
schematic-pg db:ping [schema] # Test DATABASE_URL connection (SELECT 1)
|
|
452
|
-
schematic-pg db:bootstrap [schema] #
|
|
452
|
+
schematic-pg db:bootstrap [schema] # Reset public schema, apply DDL, write .schema-state snapshot
|
|
453
453
|
schematic-pg db:diff [schema] # Print pending schema changes (snapshot vs app.schema)
|
|
454
454
|
schematic-pg db:diff --name add_users # Write a migration file under migrations/
|
|
455
455
|
schematic-pg db:migrate [schema] # Apply pending migration files
|
|
456
456
|
schematic-pg db:migrate:status [schema] # Show snapshot + migration file status
|
|
457
457
|
```
|
|
458
458
|
|
|
459
|
-
`db:bootstrap`
|
|
459
|
+
`db:bootstrap` resets the `public` schema then applies full DDL — safe to re-run locally (including via `dev` watch). Use `db:diff` / `db:migrate` when evolving a database you need to keep.
|
|
460
460
|
|
|
461
461
|
For a full walkthrough (mental model, local loop, and automating staging/production with GitHub Actions), see [Migrations tutorial](docs/migrations.md).
|
|
462
462
|
|
|
@@ -3,14 +3,20 @@ import { fieldHasAttribute, getModelNames, getPrimaryKey, getStoredFields, } fro
|
|
|
3
3
|
import { getFilterableFields, getIncludableRelationFields, getOmittedFields, getSortableFieldNames, isStoredScalarField, } from './utils/api-fields.js';
|
|
4
4
|
import { buildFilterFieldMeta, queryParamKey } from './utils/filter-operators.js';
|
|
5
5
|
const ERROR_REF = { $ref: '#/components/schemas/Error' };
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
},
|
|
10
|
-
};
|
|
6
|
+
const ERROR_EXAMPLE_VALIDATION = 'Validation failed';
|
|
7
|
+
const ERROR_EXAMPLE_FORBIDDEN = 'Role "USER" is not allowed to list this resource';
|
|
8
|
+
const ERROR_EXAMPLE_CONFLICT = 'Unique constraint violation on email';
|
|
11
9
|
const OPTIONAL_BEARER_SECURITY = [{}, { bearerAuth: [] }];
|
|
12
|
-
function errorResponse(description) {
|
|
13
|
-
return {
|
|
10
|
+
function errorResponse(description, exampleMessage = description) {
|
|
11
|
+
return {
|
|
12
|
+
description,
|
|
13
|
+
content: {
|
|
14
|
+
'application/json': {
|
|
15
|
+
schema: ERROR_REF,
|
|
16
|
+
example: { error: exampleMessage },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
};
|
|
14
20
|
}
|
|
15
21
|
function jsonContent(schema) {
|
|
16
22
|
return {
|
|
@@ -32,7 +38,10 @@ export class OpenApiGenerator {
|
|
|
32
38
|
type: 'object',
|
|
33
39
|
required: ['error'],
|
|
34
40
|
properties: {
|
|
35
|
-
error: {
|
|
41
|
+
error: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'Human-readable error message',
|
|
44
|
+
},
|
|
36
45
|
},
|
|
37
46
|
},
|
|
38
47
|
};
|
|
@@ -161,9 +170,9 @@ export class OpenApiGenerator {
|
|
|
161
170
|
description: `List of ${model.name}`,
|
|
162
171
|
content: jsonContent({ type: 'array', items: responseRef }),
|
|
163
172
|
},
|
|
164
|
-
'400': errorResponse('Validation error'),
|
|
173
|
+
'400': errorResponse('Validation error', ERROR_EXAMPLE_VALIDATION),
|
|
165
174
|
'401': errorResponse('Unauthorized'),
|
|
166
|
-
'403': errorResponse('Forbidden'),
|
|
175
|
+
'403': errorResponse('Forbidden', ERROR_EXAMPLE_FORBIDDEN),
|
|
167
176
|
'500': errorResponse('Internal server error'),
|
|
168
177
|
},
|
|
169
178
|
},
|
|
@@ -181,10 +190,10 @@ export class OpenApiGenerator {
|
|
|
181
190
|
description: `Created ${model.name}`,
|
|
182
191
|
content: jsonContent(responseRef),
|
|
183
192
|
},
|
|
184
|
-
'400': errorResponse('Validation error'),
|
|
193
|
+
'400': errorResponse('Validation error', ERROR_EXAMPLE_VALIDATION),
|
|
185
194
|
'401': errorResponse('Unauthorized'),
|
|
186
|
-
'403': errorResponse('Forbidden'),
|
|
187
|
-
'409': errorResponse('Conflict'),
|
|
195
|
+
'403': errorResponse('Forbidden', ERROR_EXAMPLE_FORBIDDEN),
|
|
196
|
+
'409': errorResponse('Conflict', ERROR_EXAMPLE_CONFLICT),
|
|
188
197
|
'500': errorResponse('Internal server error'),
|
|
189
198
|
},
|
|
190
199
|
},
|
|
@@ -204,9 +213,9 @@ export class OpenApiGenerator {
|
|
|
204
213
|
description: `${model.name} record`,
|
|
205
214
|
content: jsonContent(responseRef),
|
|
206
215
|
},
|
|
207
|
-
'400': errorResponse('Validation error'),
|
|
216
|
+
'400': errorResponse('Validation error', ERROR_EXAMPLE_VALIDATION),
|
|
208
217
|
'401': errorResponse('Unauthorized'),
|
|
209
|
-
'403': errorResponse('Forbidden'),
|
|
218
|
+
'403': errorResponse('Forbidden', ERROR_EXAMPLE_FORBIDDEN),
|
|
210
219
|
'404': errorResponse('Not found'),
|
|
211
220
|
'500': errorResponse('Internal server error'),
|
|
212
221
|
},
|
|
@@ -226,11 +235,11 @@ export class OpenApiGenerator {
|
|
|
226
235
|
description: `Updated ${model.name}`,
|
|
227
236
|
content: jsonContent(responseRef),
|
|
228
237
|
},
|
|
229
|
-
'400': errorResponse('Validation error'),
|
|
238
|
+
'400': errorResponse('Validation error', ERROR_EXAMPLE_VALIDATION),
|
|
230
239
|
'401': errorResponse('Unauthorized'),
|
|
231
|
-
'403': errorResponse('Forbidden'),
|
|
240
|
+
'403': errorResponse('Forbidden', ERROR_EXAMPLE_FORBIDDEN),
|
|
232
241
|
'404': errorResponse('Not found'),
|
|
233
|
-
'409': errorResponse('Conflict'),
|
|
242
|
+
'409': errorResponse('Conflict', ERROR_EXAMPLE_CONFLICT),
|
|
234
243
|
'500': errorResponse('Internal server error'),
|
|
235
244
|
},
|
|
236
245
|
},
|
|
@@ -245,9 +254,9 @@ export class OpenApiGenerator {
|
|
|
245
254
|
description: `Deleted ${model.name}`,
|
|
246
255
|
content: jsonContent(responseRef),
|
|
247
256
|
},
|
|
248
|
-
'400': errorResponse('Validation error'),
|
|
257
|
+
'400': errorResponse('Validation error', ERROR_EXAMPLE_VALIDATION),
|
|
249
258
|
'401': errorResponse('Unauthorized'),
|
|
250
|
-
'403': errorResponse('Forbidden'),
|
|
259
|
+
'403': errorResponse('Forbidden', ERROR_EXAMPLE_FORBIDDEN),
|
|
251
260
|
'404': errorResponse('Not found'),
|
|
252
261
|
'500': errorResponse('Internal server error'),
|
|
253
262
|
},
|
|
@@ -445,8 +454,8 @@ export class OpenApiGenerator {
|
|
|
445
454
|
description: 'Registered user with access token',
|
|
446
455
|
content: jsonContent({ $ref: '#/components/schemas/AuthTokenResponse' }),
|
|
447
456
|
},
|
|
448
|
-
'400': errorResponse('Validation error'),
|
|
449
|
-
'409': errorResponse('Conflict'),
|
|
457
|
+
'400': errorResponse('Validation error', ERROR_EXAMPLE_VALIDATION),
|
|
458
|
+
'409': errorResponse('Conflict', ERROR_EXAMPLE_CONFLICT),
|
|
450
459
|
'500': errorResponse('Internal server error'),
|
|
451
460
|
},
|
|
452
461
|
},
|
|
@@ -465,7 +474,7 @@ export class OpenApiGenerator {
|
|
|
465
474
|
description: 'Access token and user',
|
|
466
475
|
content: jsonContent({ $ref: '#/components/schemas/AuthTokenResponse' }),
|
|
467
476
|
},
|
|
468
|
-
'400': errorResponse('Validation error'),
|
|
477
|
+
'400': errorResponse('Validation error', ERROR_EXAMPLE_VALIDATION),
|
|
469
478
|
'401': errorResponse('Invalid email or password'),
|
|
470
479
|
'500': errorResponse('Internal server error'),
|
|
471
480
|
},
|
|
@@ -264,7 +264,7 @@ Flow: `validate → assertPolicy → beforeHooks → db op → afterHooks → re
|
|
|
264
264
|
schematic-pg generate # schema.sql + db client + API
|
|
265
265
|
schematic-pg dev [--no-watch] # generate + bootstrap + server + watch
|
|
266
266
|
schematic-pg start [--no-migrate] # production: migrate + run server
|
|
267
|
-
schematic-pg db:bootstrap #
|
|
267
|
+
schematic-pg db:bootstrap # reset public schema + apply DDL
|
|
268
268
|
schematic-pg db:diff [--name label] # print or write migration
|
|
269
269
|
schematic-pg db:migrate # apply pending migrations
|
|
270
270
|
schematic-pg hooks:add [--model X] # scaffold src/hooks/{Model}.ts
|
package/dist/cli/templates.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const AGENTS_TEMPLATE: string;
|
|
2
|
-
export declare const APP_SCHEMA_TEMPLATE = "extensions {\n\n}\n\nenums {\n UserRole { ADMIN, USER }\n}\n\nmodels {\n model User {\n id: UUID @id @default(gen_random_uuid())\n email: VARCHAR(255) @unique\n name: VARCHAR(150)?\n role: UserRole @default(USER)\n passwordHash: VARCHAR(255)? @omit @unfilterable\n createdAt: TIMESTAMP @default(now())\n\n @policy(role: USER, allow: [select, update], where: \"id = {{auth.user.id}}\")\n @policy(role: ADMIN, allow: all)\n }\n}\n";
|
|
2
|
+
export declare const APP_SCHEMA_TEMPLATE = "extensions {\n\n}\n\nenums {\n UserRole { ADMIN, USER }\n}\n\nmodels {\n model User {\n id: UUID @id @default(gen_random_uuid())\n email: VARCHAR(255) @unique @regex(pattern: \"^[\\w.-]+@[\\w.-]+\\.\\w+$\", message: \"Invalid email address\")\n name: VARCHAR(150)?\n role: UserRole @default(USER)\n passwordHash: VARCHAR(255)? @omit @unfilterable\n createdAt: TIMESTAMP @default(now())\n\n @policy(role: USER, allow: [select, update], where: \"id = {{auth.user.id}}\")\n @policy(role: ADMIN, allow: all)\n }\n}\n";
|
|
3
3
|
export declare const ENV_TEMPLATE = "DATABASE_URL=postgresql://postgrest:postgrest@localhost:5432/postgrest\nJWT_SECRET=\nAUTH_PEPPER=\nAUTH_ACCESS_TOKEN_TTL=1h\nJWT_ROLE_CLAIM=role\nJWT_USER_ID_CLAIM=sub\n";
|
|
4
4
|
export declare const GITIGNORE_TEMPLATE = "node_modules/\ndist/\n.env\ndocker_data/\n.DS_Store\n*.log\nnpm-debug.log*\n";
|
|
5
5
|
export declare const DOCKER_COMPOSE_TEMPLATE = "services:\n postgres:\n image: postgres:18.4-bookworm\n container_name: schematic-pg\n restart: unless-stopped\n ports:\n - \"5432:5432\"\n environment:\n POSTGRES_USER: postgrest\n POSTGRES_PASSWORD: postgrest\n POSTGRES_DB: postgrest\n volumes:\n - ./docker_data/postgres:/var/lib/postgresql\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U postgrest -d postgrest\"]\n interval: 5s\n timeout: 5s\n retries: 5\n";
|
package/dist/cli/templates.js
CHANGED
|
@@ -15,7 +15,7 @@ enums {
|
|
|
15
15
|
models {
|
|
16
16
|
model User {
|
|
17
17
|
id: UUID @id @default(gen_random_uuid())
|
|
18
|
-
email: VARCHAR(255) @unique
|
|
18
|
+
email: VARCHAR(255) @unique @regex(pattern: "^[\\w.-]+@[\\w.-]+\\.\\w+$", message: "Invalid email address")
|
|
19
19
|
name: VARCHAR(150)?
|
|
20
20
|
role: UserRole @default(USER)
|
|
21
21
|
passwordHash: VARCHAR(255)? @omit @unfilterable
|
package/dist/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ Commands:
|
|
|
18
18
|
dev [schema] [--no-watch] Generate, bootstrap DB, start server, watch schema
|
|
19
19
|
start [schema] [--no-migrate] Run production server (migrate DB, no generate/watch)
|
|
20
20
|
db:ping Test database connection
|
|
21
|
-
db:bootstrap [schema]
|
|
21
|
+
db:bootstrap [schema] Reset public schema, apply DDL, snapshot state
|
|
22
22
|
db:diff [schema] Show schema diff (--name <name> to write migration)
|
|
23
23
|
db:migrate [schema] Apply pending migrations
|
|
24
24
|
db:migrate:status [schema] Show migration status
|
package/dist/db/bootstrap.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { SqlGenerator } from '../sql-generator/sql-generator.js';
|
|
4
4
|
import { DatabaseClient } from './client.js';
|
|
5
|
+
import { resetPublicSchema } from './reset-database.js';
|
|
5
6
|
import { writeSnapshot } from './schema-state.js';
|
|
6
7
|
export function generateBootstrapSql(schemaPath) {
|
|
7
8
|
const source = readFileSync(schemaPath, 'utf8');
|
|
@@ -10,6 +11,8 @@ export function generateBootstrapSql(schemaPath) {
|
|
|
10
11
|
export async function bootstrapDatabase(schemaPath = join(process.cwd(), 'app.schema'), client = new DatabaseClient()) {
|
|
11
12
|
const sql = generateBootstrapSql(schemaPath);
|
|
12
13
|
await client.withClient(async (pgClient) => {
|
|
14
|
+
// Bootstrap is greenfield: wipe existing objects so re-runs (e.g. `dev` watch) are idempotent.
|
|
15
|
+
await resetPublicSchema(pgClient);
|
|
13
16
|
await pgClient.query(sql);
|
|
14
17
|
});
|
|
15
18
|
writeSnapshot(schemaPath);
|