schematic-pg 0.1.15 → 0.1.17

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 CHANGED
@@ -425,7 +425,7 @@ The `init` command creates everything you need to get running:
425
425
  |------------------|---------|
426
426
  | `AGENTS.md` | Agent-oriented guide for working with schematic-pg in this project |
427
427
  | `app.schema` | Starter schema (one `User` model) — edit this |
428
- | `.env` | `DATABASE_URL`, JWT settings |
428
+ | `.env` | `DATABASE_URL`, JWT settings, `CORS_ORIGIN` |
429
429
  | `docker-compose.yml` | Local PostgreSQL on `:5432` |
430
430
  | `Makefile` | `make dev` — docker compose (with health wait) + `schematic-pg dev` |
431
431
  | `tsconfig.json` | TypeScript config for `generated/` and `src/routes/` |
@@ -456,9 +456,12 @@ Generated code imports the runtime from the `schematic-pg` package (`schematic-p
456
456
  | `AUTH_ACCESS_TOKEN_TTL` | `1h` | Access token lifetime (`15m`, `1h`, or seconds) |
457
457
  | `JWT_ROLE_CLAIM` | `role` | JWT claim mapped to `auth.role` |
458
458
  | `JWT_USER_ID_CLAIM` | `sub` | JWT claim mapped to `auth.user.id` |
459
+ | `CORS_ORIGIN` | — (disabled) | Allowed browser origins. Unset disables CORS. Use `*` for any origin, or a comma-separated list (`http://localhost:5173,https://app.example.com`) |
459
460
 
460
461
  Set these in `.env` before running `dev`, `start`, or `db:bootstrap`.
461
462
 
463
+ Browser frontends on another origin need `CORS_ORIGIN`. The generated app reads it at runtime (no regenerate). Preflight `OPTIONS` is handled automatically; `Authorization` and `Content-Type` are allowed. See [CORS](docs/rest-api.md#cors).
464
+
462
465
  ---
463
466
 
464
467
  ## Authentication
@@ -0,0 +1,6 @@
1
+ import type { MiddlewareHandler } from 'hono';
2
+ import type { AppEnv } from '../types.js';
3
+ export declare function parseCorsOrigin(raw?: string | undefined): string | string[] | null;
4
+ export declare function createCorsMiddleware(options?: {
5
+ origin?: string | string[] | null;
6
+ }): MiddlewareHandler<AppEnv>;
@@ -0,0 +1,35 @@
1
+ import { cors } from 'hono/cors';
2
+ const CORS_ALLOW_HEADERS = ['Authorization', 'Content-Type'];
3
+ const CORS_ALLOW_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'];
4
+ const CORS_MAX_AGE_SECONDS = 86400;
5
+ export function parseCorsOrigin(raw = process.env.CORS_ORIGIN) {
6
+ const trimmed = raw?.trim() ?? '';
7
+ if (!trimmed) {
8
+ return null;
9
+ }
10
+ if (trimmed === '*') {
11
+ return '*';
12
+ }
13
+ const origins = trimmed
14
+ .split(',')
15
+ .map((origin) => origin.trim())
16
+ .filter((origin) => origin.length > 0);
17
+ if (origins.length === 0) {
18
+ return null;
19
+ }
20
+ return origins.length === 1 ? origins[0] : origins;
21
+ }
22
+ export function createCorsMiddleware(options = {}) {
23
+ const origin = options.origin !== undefined ? options.origin : parseCorsOrigin();
24
+ if (origin == null) {
25
+ return async (_c, next) => {
26
+ await next();
27
+ };
28
+ }
29
+ return cors({
30
+ origin,
31
+ allowHeaders: CORS_ALLOW_HEADERS,
32
+ allowMethods: CORS_ALLOW_METHODS,
33
+ maxAge: CORS_MAX_AGE_SECONDS,
34
+ });
35
+ }
@@ -1,11 +1,16 @@
1
1
  import type { Schema } from '../schema-dsl/ast.js';
2
+ import { type CustomRouteMountEntry } from './custom-route-scanner.js';
2
3
  export interface AppGeneratorOptions {
3
4
  customRoutesDir?: string;
5
+ /** When provided, skip re-discovering and use these standalone mounts only. */
6
+ standaloneCustomRoutes?: CustomRouteMountEntry[];
7
+ overlays?: ReadonlyMap<string, CustomRouteMountEntry>;
4
8
  }
5
9
  export declare class AppGenerator {
6
10
  private readonly schema;
7
11
  private readonly options;
8
12
  constructor(schema: Schema, options?: AppGeneratorOptions);
9
13
  generate(): string;
14
+ private resolveStandaloneCustomRoutes;
10
15
  }
11
16
  export declare function generateAppFile(schema: Schema, options?: AppGeneratorOptions): string;
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { PACKAGE_NAME } from '../constants.js';
3
- import { discoverCustomRoutes } from './custom-route-scanner.js';
3
+ import { discoverCustomRoutes, partitionCustomRoutes, } from './custom-route-scanner.js';
4
4
  import { getRouteMountEntries } from './route-generator.js';
5
5
  const DEFAULT_CUSTOM_ROUTES_DIR = path.resolve('src/routes');
6
6
  export class AppGenerator {
@@ -11,19 +11,19 @@ export class AppGenerator {
11
11
  this.options = options;
12
12
  }
13
13
  generate() {
14
- const mounts = getRouteMountEntries(this.schema);
14
+ const overlays = this.options.overlays ?? new Map();
15
+ const mounts = getRouteMountEntries(this.schema, overlays);
15
16
  const generatedImports = mounts
16
17
  .map((entry) => `import ${entry.importName} from './routes/${entry.fileName.replace(/\.ts$/, '.js')}';`)
17
18
  .join('\n');
18
19
  const generatedRoutes = mounts
19
20
  .map((entry) => ` app.route('/${entry.basePath}', ${entry.importName});`)
20
21
  .join('\n');
21
- const customRoutesDir = this.options.customRoutesDir ?? DEFAULT_CUSTOM_ROUTES_DIR;
22
- const customMounts = discoverCustomRoutes(customRoutesDir);
23
- const customImports = customMounts
22
+ const standaloneMounts = this.resolveStandaloneCustomRoutes();
23
+ const customImports = standaloneMounts
24
24
  .map((entry) => `import ${entry.importName} from '${entry.importPath}';`)
25
25
  .join('\n');
26
- const customRoutes = customMounts
26
+ const customRoutes = standaloneMounts
27
27
  .map((entry) => ` app.route('/${entry.basePath}', ${entry.importName});`)
28
28
  .join('\n');
29
29
  const routeImports = [generatedImports, customImports].filter(Boolean).join('\n');
@@ -46,6 +46,7 @@ export class AppGenerator {
46
46
  `import { createAuthMiddleware } from '${PACKAGE_NAME}/api/auth/middleware';`,
47
47
  `import { createJwtResolver } from '${PACKAGE_NAME}/api/auth/jwt-resolver';`,
48
48
  `import type { AuthResolver } from '${PACKAGE_NAME}/api/auth/types';`,
49
+ `import { createCorsMiddleware } from '${PACKAGE_NAME}/api/middleware/cors';`,
49
50
  `import { createDbMiddleware } from '${PACKAGE_NAME}/api/middleware/db';`,
50
51
  `import { handleError } from '${PACKAGE_NAME}/api/middleware/errors';`,
51
52
  `import { mountApiDocs } from '${PACKAGE_NAME}/api/openapi';`,
@@ -61,6 +62,7 @@ export class AppGenerator {
61
62
  '',
62
63
  'export function createApp(options: CreateAppOptions = {}): Hono<AppEnv> {',
63
64
  ' const app = new Hono<AppEnv>();',
65
+ ' app.use(createCorsMiddleware());',
64
66
  ' mountApiDocs(app, openApiDocument);',
65
67
  ' app.use(logger());',
66
68
  ' app.use(prettyJSON());',
@@ -85,6 +87,14 @@ export class AppGenerator {
85
87
  '',
86
88
  ].join('\n');
87
89
  }
90
+ resolveStandaloneCustomRoutes() {
91
+ if (this.options.standaloneCustomRoutes) {
92
+ return this.options.standaloneCustomRoutes;
93
+ }
94
+ const customRoutesDir = this.options.customRoutesDir ?? DEFAULT_CUSTOM_ROUTES_DIR;
95
+ const { standalone } = partitionCustomRoutes(discoverCustomRoutes(customRoutesDir), this.schema);
96
+ return standalone;
97
+ }
88
98
  }
89
99
  export function generateAppFile(schema, options) {
90
100
  return new AppGenerator(schema, options).generate();
@@ -1,6 +1,15 @@
1
+ import type { Schema } from '../schema-dsl/ast.js';
1
2
  export interface CustomRouteMountEntry {
2
3
  basePath: string;
3
4
  importName: string;
5
+ /** Import path relative to generated/app.ts */
4
6
  importPath: string;
7
+ /** Import path relative to generated/routes/*.ts */
8
+ routeImportPath: string;
9
+ }
10
+ export interface PartitionedCustomRoutes {
11
+ overlays: Map<string, CustomRouteMountEntry>;
12
+ standalone: CustomRouteMountEntry[];
5
13
  }
6
14
  export declare function discoverCustomRoutes(customRoutesDir: string): CustomRouteMountEntry[];
15
+ export declare function partitionCustomRoutes(entries: CustomRouteMountEntry[], schema: Schema): PartitionedCustomRoutes;
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readdirSync, statSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { toRouteImportName } from '../api/utils/route-naming.js';
3
+ import { toRouteBasePath, toRouteImportName } from '../api/utils/route-naming.js';
4
4
  function isRouteFile(filename) {
5
5
  return (filename.endsWith('.ts') &&
6
6
  !filename.endsWith('.test.ts') &&
@@ -29,6 +29,7 @@ function scanDirectory(customRoutesDir, relativeDir, entries) {
29
29
  basePath,
30
30
  importName: toRouteImportName(basePath),
31
31
  importPath: `../src/routes/${basePath}.js`,
32
+ routeImportPath: `../../src/routes/${basePath}.js`,
32
33
  });
33
34
  }
34
35
  }
@@ -40,3 +41,18 @@ export function discoverCustomRoutes(customRoutesDir) {
40
41
  scanDirectory(customRoutesDir, '', entries);
41
42
  return entries.sort((left, right) => left.basePath.localeCompare(right.basePath));
42
43
  }
44
+ export function partitionCustomRoutes(entries, schema) {
45
+ const modelBasePaths = new Map(schema.models.map((model) => [toRouteBasePath(model.name), model.name]));
46
+ const overlays = new Map();
47
+ const standalone = [];
48
+ for (const entry of entries) {
49
+ const modelName = modelBasePaths.get(entry.basePath);
50
+ if (modelName) {
51
+ overlays.set(modelName, entry);
52
+ }
53
+ else {
54
+ standalone.push(entry);
55
+ }
56
+ }
57
+ return { overlays, standalone };
58
+ }
@@ -1,6 +1,6 @@
1
1
  import { DEFAULT_CUSTOM_ROUTES_DIR, DEFAULT_HOOKS_DIR } from '../cli/paths.js';
2
2
  import { generateAppFile } from './app-generator.js';
3
- import { discoverCustomRoutes } from './custom-route-scanner.js';
3
+ import { discoverCustomRoutes, partitionCustomRoutes } from './custom-route-scanner.js';
4
4
  import { discoverHooks } from './hook-scanner.js';
5
5
  import { generateHooksFile } from './hooks-generator.js';
6
6
  import { generateOpenApiFiles } from './openapi-generator.js';
@@ -9,17 +9,23 @@ import { generateRouteFiles } from './route-generator.js';
9
9
  import { generateValidationSchemas } from './zod-schema-generator.js';
10
10
  export function generateApiFiles(schema, options) {
11
11
  const customRoutesDir = options?.customRoutesDir ?? DEFAULT_CUSTOM_ROUTES_DIR;
12
- const appOptions = { customRoutesDir };
13
12
  const hooksDir = options?.hooksDir ?? DEFAULT_HOOKS_DIR;
14
13
  const { entries: hookEntries, modelsWithHooks } = discoverHooks(hooksDir, schema);
15
- const includeAuthPaths = discoverCustomRoutes(customRoutesDir).some((entry) => entry.basePath === 'auth');
14
+ const customEntries = discoverCustomRoutes(customRoutesDir);
15
+ const { overlays, standalone } = partitionCustomRoutes(customEntries, schema);
16
+ const includeAuthPaths = customEntries.some((entry) => entry.basePath === 'auth');
16
17
  const openapi = generateOpenApiFiles(schema, { includeAuthPaths });
18
+ const appOptions = {
19
+ customRoutesDir,
20
+ standaloneCustomRoutes: standalone,
21
+ overlays,
22
+ };
17
23
  return {
18
24
  app: generateAppFile(schema, appOptions),
19
25
  policies: generatePoliciesFile(schema),
20
26
  validation: generateValidationSchemas(schema),
21
27
  hooks: generateHooksFile(hookEntries),
22
- routes: generateRouteFiles(schema, modelsWithHooks),
28
+ routes: generateRouteFiles(schema, { modelsWithHooks, overlays }),
23
29
  openapiTs: openapi.openapiTs,
24
30
  openapiJson: openapi.openapiJson,
25
31
  };
@@ -2,6 +2,7 @@ import { toRouteBasePath } from '../api/utils/route-naming.js';
2
2
  import { fieldHasAttribute, getModelNames, getPrimaryKey, getStoredFields, } from '../sql-generator/utils/ast-helpers.js';
3
3
  import { getFilterableFields, getIncludableRelationFields, getOmittedFields, getSortableFieldNames, isStoredScalarField, } from './utils/api-fields.js';
4
4
  import { buildFilterFieldMeta, queryParamKey } from './utils/filter-operators.js';
5
+ import { hasRestOperation, isRestEnabled } from './utils/rest.js';
5
6
  const ERROR_REF = { $ref: '#/components/schemas/Error' };
6
7
  const ERROR_EXAMPLE_VALIDATION = 'Validation failed';
7
8
  const ERROR_EXAMPLE_FORBIDDEN = 'Role "USER" is not allowed to list this resource';
@@ -47,6 +48,9 @@ export class OpenApiGenerator {
47
48
  };
48
49
  const paths = {};
49
50
  for (const model of this.schema.models) {
51
+ if (!isRestEnabled(model)) {
52
+ continue;
53
+ }
50
54
  Object.assign(schemas, this.buildModelComponentSchemas(model));
51
55
  Object.assign(paths, this.buildModelPaths(model));
52
56
  }
@@ -127,22 +131,27 @@ export class OpenApiGenerator {
127
131
  nullable: Boolean(field.type.optional),
128
132
  });
129
133
  }
130
- return {
134
+ const schemas = {
131
135
  [`${model.name}Response`]: {
132
136
  type: 'object',
133
137
  properties: responseProps,
134
138
  ...(responseRequired.length > 0 ? { required: responseRequired } : {}),
135
139
  },
136
- [`${model.name}Create`]: {
140
+ };
141
+ if (hasRestOperation(model, 'create')) {
142
+ schemas[`${model.name}Create`] = {
137
143
  type: 'object',
138
144
  properties: createProps,
139
145
  ...(createRequired.length > 0 ? { required: createRequired } : {}),
140
- },
141
- [`${model.name}Update`]: {
146
+ };
147
+ }
148
+ if (hasRestOperation(model, 'update')) {
149
+ schemas[`${model.name}Update`] = {
142
150
  type: 'object',
143
151
  properties: updateProps,
144
- },
145
- };
152
+ };
153
+ }
154
+ return schemas;
146
155
  }
147
156
  buildModelPaths(model) {
148
157
  const basePath = toRouteBasePath(model.name);
@@ -157,52 +166,58 @@ export class OpenApiGenerator {
157
166
  const createRef = { $ref: `#/components/schemas/${model.name}Create` };
158
167
  const updateRef = { $ref: `#/components/schemas/${model.name}Update` };
159
168
  const tag = model.name;
160
- const paths = {
161
- [collectionPath]: {
162
- get: {
163
- tags: [tag],
164
- summary: `List ${model.name}`,
165
- operationId: `list${model.name}`,
166
- security: OPTIONAL_BEARER_SECURITY,
167
- parameters: this.buildListQueryParameters(model),
168
- responses: {
169
- '200': {
170
- description: `List of ${model.name}`,
171
- content: jsonContent({ type: 'array', items: responseRef }),
172
- },
173
- '400': errorResponse('Validation error', ERROR_EXAMPLE_VALIDATION),
174
- '401': errorResponse('Unauthorized'),
175
- '403': errorResponse('Forbidden', ERROR_EXAMPLE_FORBIDDEN),
176
- '500': errorResponse('Internal server error'),
169
+ const paths = {};
170
+ const collectionOps = {};
171
+ if (hasRestOperation(model, 'list')) {
172
+ collectionOps.get = {
173
+ tags: [tag],
174
+ summary: `List ${model.name}`,
175
+ operationId: `list${model.name}`,
176
+ security: OPTIONAL_BEARER_SECURITY,
177
+ parameters: this.buildListQueryParameters(model),
178
+ responses: {
179
+ '200': {
180
+ description: `List of ${model.name}`,
181
+ content: jsonContent({ type: 'array', items: responseRef }),
177
182
  },
183
+ '400': errorResponse('Validation error', ERROR_EXAMPLE_VALIDATION),
184
+ '401': errorResponse('Unauthorized'),
185
+ '403': errorResponse('Forbidden', ERROR_EXAMPLE_FORBIDDEN),
186
+ '500': errorResponse('Internal server error'),
178
187
  },
179
- post: {
180
- tags: [tag],
181
- summary: `Create ${model.name}`,
182
- operationId: `create${model.name}`,
183
- security: OPTIONAL_BEARER_SECURITY,
184
- requestBody: {
185
- required: true,
186
- content: jsonContent(createRef),
187
- },
188
- responses: {
189
- '201': {
190
- description: `Created ${model.name}`,
191
- content: jsonContent(responseRef),
192
- },
193
- '400': errorResponse('Validation error', ERROR_EXAMPLE_VALIDATION),
194
- '401': errorResponse('Unauthorized'),
195
- '403': errorResponse('Forbidden', ERROR_EXAMPLE_FORBIDDEN),
196
- '409': errorResponse('Conflict', ERROR_EXAMPLE_CONFLICT),
197
- '500': errorResponse('Internal server error'),
188
+ };
189
+ }
190
+ if (hasRestOperation(model, 'create')) {
191
+ collectionOps.post = {
192
+ tags: [tag],
193
+ summary: `Create ${model.name}`,
194
+ operationId: `create${model.name}`,
195
+ security: OPTIONAL_BEARER_SECURITY,
196
+ requestBody: {
197
+ required: true,
198
+ content: jsonContent(createRef),
199
+ },
200
+ responses: {
201
+ '201': {
202
+ description: `Created ${model.name}`,
203
+ content: jsonContent(responseRef),
198
204
  },
205
+ '400': errorResponse('Validation error', ERROR_EXAMPLE_VALIDATION),
206
+ '401': errorResponse('Unauthorized'),
207
+ '403': errorResponse('Forbidden', ERROR_EXAMPLE_FORBIDDEN),
208
+ '409': errorResponse('Conflict', ERROR_EXAMPLE_CONFLICT),
209
+ '500': errorResponse('Internal server error'),
199
210
  },
200
- },
201
- };
211
+ };
212
+ }
213
+ if (Object.keys(collectionOps).length > 0) {
214
+ paths[collectionPath] = collectionOps;
215
+ }
202
216
  if (pkFields.length > 0) {
203
217
  const pathParams = this.buildPathParameters(pkFields, model);
204
- paths[itemPath] = {
205
- get: {
218
+ const itemOps = {};
219
+ if (hasRestOperation(model, 'get')) {
220
+ itemOps.get = {
206
221
  tags: [tag],
207
222
  summary: `Get ${model.name}`,
208
223
  operationId: `get${model.name}`,
@@ -219,8 +234,10 @@ export class OpenApiGenerator {
219
234
  '404': errorResponse('Not found'),
220
235
  '500': errorResponse('Internal server error'),
221
236
  },
222
- },
223
- put: {
237
+ };
238
+ }
239
+ if (hasRestOperation(model, 'update')) {
240
+ itemOps.put = {
224
241
  tags: [tag],
225
242
  summary: `Update ${model.name}`,
226
243
  operationId: `update${model.name}`,
@@ -242,8 +259,10 @@ export class OpenApiGenerator {
242
259
  '409': errorResponse('Conflict', ERROR_EXAMPLE_CONFLICT),
243
260
  '500': errorResponse('Internal server error'),
244
261
  },
245
- },
246
- delete: {
262
+ };
263
+ }
264
+ if (hasRestOperation(model, 'delete')) {
265
+ itemOps.delete = {
247
266
  tags: [tag],
248
267
  summary: `Delete ${model.name}`,
249
268
  operationId: `delete${model.name}`,
@@ -260,8 +279,11 @@ export class OpenApiGenerator {
260
279
  '404': errorResponse('Not found'),
261
280
  '500': errorResponse('Internal server error'),
262
281
  },
263
- },
264
- };
282
+ };
283
+ }
284
+ if (Object.keys(itemOps).length > 0) {
285
+ paths[itemPath] = itemOps;
286
+ }
265
287
  }
266
288
  return paths;
267
289
  }
@@ -1,10 +1,17 @@
1
1
  import type { Model, Schema } from '../schema-dsl/ast.js';
2
+ import type { CustomRouteMountEntry } from './custom-route-scanner.js';
3
+ export interface RouteGeneratorOptions {
4
+ modelsWithHooks?: ReadonlySet<string>;
5
+ overlays?: ReadonlyMap<string, CustomRouteMountEntry>;
6
+ }
2
7
  export declare class RouteGenerator {
3
8
  private readonly model;
4
9
  private readonly schema;
5
10
  private readonly modelsWithHooks;
6
- constructor(model: Model, schema: Schema, modelsWithHooks?: ReadonlySet<string>);
7
- generate(): string;
11
+ private readonly overlays;
12
+ constructor(model: Model, schema: Schema, options?: RouteGeneratorOptions);
13
+ generate(): string | null;
14
+ private generateOverlayOnly;
8
15
  private jsonRow;
9
16
  private jsonRows;
10
17
  private mutationJsonRow;
@@ -16,8 +23,8 @@ export declare class RouteGenerator {
16
23
  getRouteFileName(): string;
17
24
  getRouteBasePath(): string;
18
25
  }
19
- export declare function generateRouteFiles(schema: Schema, modelsWithHooks?: ReadonlySet<string>): Map<string, string>;
20
- export declare function getRouteMountEntries(schema: Schema): {
26
+ export declare function generateRouteFiles(schema: Schema, modelsWithHooksOrOptions?: ReadonlySet<string> | RouteGeneratorOptions): Map<string, string>;
27
+ export declare function getRouteMountEntries(schema: Schema, overlays?: ReadonlyMap<string, CustomRouteMountEntry>): {
21
28
  basePath: string;
22
29
  fileName: string;
23
30
  importName: string;
@@ -4,74 +4,154 @@ import { getClientExportName } from '../db/type-generator.js';
4
4
  import { toRouteBasePath, toRouteFileName, toRouteImportName } from '../api/utils/route-naming.js';
5
5
  import { toModelConstantPrefix } from './utils/api-fields.js';
6
6
  import { hasPolicies } from './utils/policy.js';
7
+ import { isRestEnabled, normalizeRest } from './utils/rest.js';
7
8
  export class RouteGenerator {
8
9
  model;
9
10
  schema;
10
11
  modelsWithHooks;
11
- constructor(model, schema, modelsWithHooks = new Set()) {
12
+ overlays;
13
+ constructor(model, schema, options = {}) {
12
14
  this.model = model;
13
15
  this.schema = schema;
14
- this.modelsWithHooks = modelsWithHooks;
16
+ this.modelsWithHooks = options.modelsWithHooks ?? new Set();
17
+ this.overlays = options.overlays ?? new Map();
15
18
  }
16
19
  generate() {
17
- const clientKey = getClientExportName(this.model.name);
20
+ const rest = normalizeRest(this.model);
21
+ const overlay = this.overlays.get(this.model.name);
22
+ const operations = rest.operations;
23
+ if (operations.size === 0 && !overlay) {
24
+ return null;
25
+ }
26
+ if (operations.size === 0 && overlay) {
27
+ return this.generateOverlayOnly(overlay);
28
+ }
29
+ const needsPk = operations.has('get') || operations.has('update') || operations.has('delete');
18
30
  const primaryKey = getPrimaryKey(this.model);
19
- if (!primaryKey) {
31
+ if (needsPk && !primaryKey) {
20
32
  throw new Error(`Model ${this.model.name} has no primary key`);
21
33
  }
22
- const pathParams = primaryKey.fields.map((field) => `:${field}`).join('/');
23
- const whereFromParams = primaryKey.fields.map((field) => `${field}: params.${field}`).join(', ');
34
+ const clientKey = getClientExportName(this.model.name);
35
+ const pathParams = primaryKey?.fields.map((field) => `:${field}`).join('/') ?? '';
36
+ const whereFromParams = primaryKey?.fields.map((field) => `${field}: params.${field}`).join(', ') ?? '';
24
37
  const paramSchemaName = `${this.model.name}ParamSchema`;
25
38
  const listQuerySchemaName = `${this.model.name}ListQuerySchema`;
26
39
  const getQuerySchemaName = `${this.model.name}GetQuerySchema`;
27
40
  const modelHasPolicies = hasPolicies(this.model);
28
- const modelHasHooks = this.modelsWithHooks.has(this.model.name);
41
+ const hasWriteOps = operations.has('create') || operations.has('update') || operations.has('delete');
42
+ const modelHasHooks = hasWriteOps && this.modelsWithHooks.has(this.model.name);
29
43
  const constantPrefix = toModelConstantPrefix(this.model.name);
44
+ const needsValidateJson = operations.has('create') || operations.has('update');
45
+ const needsValidateParam = operations.has('get') || operations.has('update') || operations.has('delete');
46
+ const needsValidateQuery = operations.has('list') || operations.has('get');
47
+ const needsNotFound = operations.has('get');
48
+ const needsBuildReadQuery = operations.has('list');
49
+ const needsParseInclude = operations.has('get');
50
+ const needsOmitFields = hasWriteOps;
51
+ const needsShapeResponse = operations.has('list') || operations.has('get');
52
+ const validateImports = [];
53
+ if (needsValidateJson)
54
+ validateImports.push('validateJson');
55
+ if (needsValidateParam)
56
+ validateImports.push('validateParam');
57
+ if (needsValidateQuery)
58
+ validateImports.push('validateQuery');
59
+ const schemaImports = [];
60
+ if (operations.has('create'))
61
+ schemaImports.push(` ${this.model.name}CreateSchema,`);
62
+ if (operations.has('update'))
63
+ schemaImports.push(` ${this.model.name}UpdateSchema,`);
64
+ if (needsValidateParam)
65
+ schemaImports.push(` ${paramSchemaName},`);
66
+ if (operations.has('list'))
67
+ schemaImports.push(` ${listQuerySchemaName},`);
68
+ if (operations.has('get'))
69
+ schemaImports.push(` ${getQuerySchemaName},`);
70
+ if (operations.has('list')) {
71
+ schemaImports.push(` ${constantPrefix}_LIST_QUERY_FIELDS,`);
72
+ schemaImports.push(` ${constantPrefix}_SORTABLE_FIELDS,`);
73
+ }
74
+ if (operations.has('list') || operations.has('get')) {
75
+ schemaImports.push(` ${constantPrefix}_INCLUDABLE_RELATIONS,`);
76
+ }
77
+ if (needsOmitFields)
78
+ schemaImports.push(` ${constantPrefix}_OMIT_FIELDS,`);
79
+ if (needsShapeResponse) {
80
+ schemaImports.push(` API_OMIT_FIELDS_BY_MODEL,`);
81
+ schemaImports.push(` API_RELATION_TARGETS,`);
82
+ }
83
+ const handlerBlocks = [];
84
+ if (operations.has('list')) {
85
+ handlerBlocks.push(this.generateListRoute(clientKey, modelHasPolicies, listQuerySchemaName, constantPrefix));
86
+ }
87
+ if (operations.has('get')) {
88
+ handlerBlocks.push(this.generateGetRoute(clientKey, pathParams, paramSchemaName, getQuerySchemaName, whereFromParams, modelHasPolicies, constantPrefix));
89
+ }
90
+ if (operations.has('create')) {
91
+ handlerBlocks.push(this.generateCreateRoute(clientKey, modelHasPolicies, modelHasHooks, constantPrefix));
92
+ }
93
+ if (operations.has('update')) {
94
+ handlerBlocks.push(this.generateUpdateRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies, modelHasHooks, constantPrefix));
95
+ }
96
+ if (operations.has('delete')) {
97
+ handlerBlocks.push(this.generateDeleteRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies, modelHasHooks, constantPrefix));
98
+ }
99
+ const lines = [
100
+ '// Auto-generated by RouteGenerator. Do not edit manually.',
101
+ "import { Hono } from 'hono';",
102
+ `import type { AppEnv } from '${PACKAGE_NAME}/api/types';`,
103
+ ];
104
+ if (validateImports.length > 0) {
105
+ lines.push(`import { ${validateImports.join(', ')} } from '${PACKAGE_NAME}/api/middleware/validate';`);
106
+ }
107
+ if (needsNotFound) {
108
+ lines.push(`import { notFoundResponse } from '${PACKAGE_NAME}/api/middleware/errors';`);
109
+ }
110
+ if (needsBuildReadQuery) {
111
+ lines.push(`import { buildReadQuery } from '${PACKAGE_NAME}/api/utils/read-query';`);
112
+ }
113
+ if (needsParseInclude) {
114
+ lines.push(`import { parseIncludeQuery } from '${PACKAGE_NAME}/api/utils/include-query';`);
115
+ }
116
+ if (needsOmitFields) {
117
+ lines.push(`import { omitFields } from '${PACKAGE_NAME}/api/utils/omit-fields';`);
118
+ }
119
+ if (needsShapeResponse) {
120
+ lines.push(`import { shapeResponse, shapeResponseMany } from '${PACKAGE_NAME}/api/utils/response-shape';`);
121
+ }
122
+ if (modelHasPolicies) {
123
+ lines.push(`import { assertPolicy, mergeWhere, resolvePolicyWhere } from '${PACKAGE_NAME}/api/auth/policy';`);
124
+ }
125
+ if (modelHasHooks) {
126
+ lines.push(`import { cancelledResponse, createHookContext, runAfterHooks, runBeforeHooks } from '${PACKAGE_NAME}/api/hooks';`);
127
+ }
128
+ if (schemaImports.length > 0) {
129
+ lines.push('import {');
130
+ lines.push(...schemaImports);
131
+ lines.push(`} from '../schemas/validation.js';`);
132
+ }
133
+ if (overlay) {
134
+ lines.push(`import ${overlay.importName}Overlay from '${overlay.routeImportPath}';`);
135
+ }
136
+ lines.push('', 'const router = new Hono<AppEnv>();', '');
137
+ for (const block of handlerBlocks) {
138
+ lines.push(...block, '');
139
+ }
140
+ if (overlay) {
141
+ lines.push(`router.route('/', ${overlay.importName}Overlay);`, '');
142
+ }
143
+ lines.push('export default router;', '');
144
+ return lines.join('\n');
145
+ }
146
+ generateOverlayOnly(overlay) {
30
147
  return [
31
148
  '// Auto-generated by RouteGenerator. Do not edit manually.',
32
149
  "import { Hono } from 'hono';",
33
150
  `import type { AppEnv } from '${PACKAGE_NAME}/api/types';`,
34
- `import { validateJson, validateParam, validateQuery } from '${PACKAGE_NAME}/api/middleware/validate';`,
35
- `import { notFoundResponse } from '${PACKAGE_NAME}/api/middleware/errors';`,
36
- `import { buildReadQuery } from '${PACKAGE_NAME}/api/utils/read-query';`,
37
- `import { parseIncludeQuery } from '${PACKAGE_NAME}/api/utils/include-query';`,
38
- `import { omitFields } from '${PACKAGE_NAME}/api/utils/omit-fields';`,
39
- `import { shapeResponse, shapeResponseMany } from '${PACKAGE_NAME}/api/utils/response-shape';`,
40
- ...(modelHasPolicies
41
- ? [
42
- `import { assertPolicy, mergeWhere, resolvePolicyWhere } from '${PACKAGE_NAME}/api/auth/policy';`,
43
- ]
44
- : []),
45
- ...(modelHasHooks
46
- ? [
47
- `import { cancelledResponse, createHookContext, runAfterHooks, runBeforeHooks } from '${PACKAGE_NAME}/api/hooks';`,
48
- ]
49
- : []),
50
- `import {`,
51
- ` ${this.model.name}CreateSchema,`,
52
- ` ${this.model.name}UpdateSchema,`,
53
- ` ${paramSchemaName},`,
54
- ` ${listQuerySchemaName},`,
55
- ` ${getQuerySchemaName},`,
56
- ` ${constantPrefix}_LIST_QUERY_FIELDS,`,
57
- ` ${constantPrefix}_INCLUDABLE_RELATIONS,`,
58
- ` ${constantPrefix}_OMIT_FIELDS,`,
59
- ` ${constantPrefix}_SORTABLE_FIELDS,`,
60
- ` API_OMIT_FIELDS_BY_MODEL,`,
61
- ` API_RELATION_TARGETS,`,
62
- `} from '../schemas/validation.js';`,
151
+ `import ${overlay.importName}Overlay from '${overlay.routeImportPath}';`,
63
152
  '',
64
153
  'const router = new Hono<AppEnv>();',
65
- '',
66
- ...this.generateListRoute(clientKey, modelHasPolicies, listQuerySchemaName, constantPrefix),
67
- '',
68
- ...this.generateGetRoute(clientKey, pathParams, paramSchemaName, getQuerySchemaName, whereFromParams, modelHasPolicies, constantPrefix),
69
- '',
70
- ...this.generateCreateRoute(clientKey, modelHasPolicies, modelHasHooks, constantPrefix),
71
- '',
72
- ...this.generateUpdateRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies, modelHasHooks, constantPrefix),
73
- '',
74
- ...this.generateDeleteRoute(clientKey, pathParams, paramSchemaName, whereFromParams, modelHasPolicies, modelHasHooks, constantPrefix),
154
+ `router.route('/', ${overlay.importName}Overlay);`,
75
155
  '',
76
156
  'export default router;',
77
157
  '',
@@ -264,16 +344,36 @@ export class RouteGenerator {
264
344
  return toRouteBasePath(this.model.name);
265
345
  }
266
346
  }
267
- export function generateRouteFiles(schema, modelsWithHooks = new Set()) {
347
+ export function generateRouteFiles(schema, modelsWithHooksOrOptions = new Set()) {
348
+ const options = normalizeRouteGeneratorOptions(modelsWithHooksOrOptions);
268
349
  const files = new Map();
269
350
  for (const model of schema.models) {
270
- const generator = new RouteGenerator(model, schema, modelsWithHooks);
271
- files.set(generator.getRouteFileName(), generator.generate());
351
+ const generator = new RouteGenerator(model, schema, options);
352
+ const content = generator.generate();
353
+ if (content !== null) {
354
+ files.set(generator.getRouteFileName(), content);
355
+ }
272
356
  }
273
357
  return files;
274
358
  }
275
- export function getRouteMountEntries(schema) {
276
- return schema.models.map((model) => {
359
+ function normalizeRouteGeneratorOptions(value) {
360
+ if (value instanceof Set) {
361
+ return { modelsWithHooks: value };
362
+ }
363
+ if (value &&
364
+ typeof value === 'object' &&
365
+ 'has' in value &&
366
+ typeof value.has === 'function' &&
367
+ !('overlays' in value) &&
368
+ !('modelsWithHooks' in value)) {
369
+ return { modelsWithHooks: value };
370
+ }
371
+ return value;
372
+ }
373
+ export function getRouteMountEntries(schema, overlays = new Map()) {
374
+ return schema.models
375
+ .filter((model) => isRestEnabled(model) || overlays.has(model.name))
376
+ .map((model) => {
277
377
  const basePath = toRouteBasePath(model.name);
278
378
  const fileName = toRouteFileName(model.name);
279
379
  const importName = toRouteImportName(basePath);
@@ -0,0 +1,9 @@
1
+ import type { Model } from '../../schema-dsl/ast.js';
2
+ export type RestOperation = 'list' | 'get' | 'create' | 'update' | 'delete';
3
+ export declare const REST_OPERATIONS: RestOperation[];
4
+ export interface NormalizedRest {
5
+ operations: Set<RestOperation>;
6
+ }
7
+ export declare function normalizeRest(model: Model): NormalizedRest;
8
+ export declare function isRestEnabled(model: Model): boolean;
9
+ export declare function hasRestOperation(model: Model, operation: RestOperation): boolean;
@@ -0,0 +1,93 @@
1
+ import { assertKeyValueArgs, getOptionalKvPair } from '../../sql-generator/utils/ast-helpers.js';
2
+ export const REST_OPERATIONS = ['list', 'get', 'create', 'update', 'delete'];
3
+ const HTTP_VERB_HINTS = {
4
+ GET: 'list or get',
5
+ POST: 'create',
6
+ PUT: 'update',
7
+ PATCH: 'update',
8
+ DELETE: 'delete',
9
+ };
10
+ export function normalizeRest(model) {
11
+ const fieldRest = model.fields.find((field) => field.attributes.some((attribute) => attribute.name === 'rest'));
12
+ if (fieldRest) {
13
+ throw new Error(`@rest on model ${model.name} must be a model attribute, not a field attribute on "${fieldRest.name}"`);
14
+ }
15
+ const restAttributes = model.attributes.filter((attribute) => attribute.name === 'rest');
16
+ if (restAttributes.length === 0) {
17
+ return { operations: new Set(REST_OPERATIONS) };
18
+ }
19
+ if (restAttributes.length > 1) {
20
+ throw new Error(`Model ${model.name} has duplicate @rest attributes`);
21
+ }
22
+ return normalizeRestAttribute(restAttributes[0], model.name);
23
+ }
24
+ export function isRestEnabled(model) {
25
+ return normalizeRest(model).operations.size > 0;
26
+ }
27
+ export function hasRestOperation(model, operation) {
28
+ return normalizeRest(model).operations.has(operation);
29
+ }
30
+ function normalizeRestAttribute(attribute, modelName) {
31
+ if (!attribute.args) {
32
+ return { operations: new Set() };
33
+ }
34
+ if (attribute.args.kind === 'ExpressionArgs') {
35
+ if (attribute.args.expressions.length === 0) {
36
+ throw new Error(`@rest() on model ${modelName} is empty; use @rest, @rest(false), only, or except`);
37
+ }
38
+ if (attribute.args.expressions.length !== 1) {
39
+ throw new Error(`@rest on model ${modelName} expects a single boolean or key-value args`);
40
+ }
41
+ const expression = attribute.args.expressions[0];
42
+ if (expression.kind === 'BooleanLiteral') {
43
+ if (expression.value === false) {
44
+ return { operations: new Set() };
45
+ }
46
+ return { operations: new Set(REST_OPERATIONS) };
47
+ }
48
+ throw new Error(`@rest on model ${modelName} expects false, only, or except`);
49
+ }
50
+ const args = assertKeyValueArgs(attribute.args);
51
+ const onlyPair = getOptionalKvPair(args, 'only');
52
+ const exceptPair = getOptionalKvPair(args, 'except');
53
+ if (onlyPair && exceptPair) {
54
+ throw new Error(`@rest on model ${modelName} cannot mix only and except`);
55
+ }
56
+ if (!onlyPair && !exceptPair) {
57
+ throw new Error(`@rest on model ${modelName} requires only, except, or false`);
58
+ }
59
+ if (onlyPair) {
60
+ return { operations: new Set(parseRestOperations(onlyPair.value, modelName, 'only')) };
61
+ }
62
+ const excluded = new Set(parseRestOperations(exceptPair.value, modelName, 'except'));
63
+ return {
64
+ operations: new Set(REST_OPERATIONS.filter((operation) => !excluded.has(operation))),
65
+ };
66
+ }
67
+ function parseRestOperations(value, modelName, fieldName) {
68
+ if (value.kind !== 'ArrayLiteral') {
69
+ throw new Error(`@rest ${fieldName} on model ${modelName} must be an array of operations`);
70
+ }
71
+ return value.elements.map((element) => parseRestOperation(element, modelName));
72
+ }
73
+ function parseRestOperation(value, modelName) {
74
+ if (value.kind !== 'Identifier') {
75
+ throw new Error(`@rest operation on model ${modelName} must be an identifier`);
76
+ }
77
+ const raw = value.name;
78
+ const httpHint = HTTP_VERB_HINTS[raw.toUpperCase()];
79
+ if (httpHint && raw === raw.toUpperCase()) {
80
+ throw new Error(`Unknown @rest operation "${raw}" on model ${modelName}; use ${httpHint} instead of HTTP verb "${raw}"`);
81
+ }
82
+ const operation = raw.toLowerCase();
83
+ if (isRestOperation(operation)) {
84
+ return operation;
85
+ }
86
+ if (httpHint) {
87
+ throw new Error(`Unknown @rest operation "${raw}" on model ${modelName}; use ${httpHint} instead of HTTP verb "${raw}"`);
88
+ }
89
+ throw new Error(`Unknown @rest operation "${raw}" on model ${modelName}; expected one of ${REST_OPERATIONS.join(', ')}`);
90
+ }
91
+ function isRestOperation(value) {
92
+ return REST_OPERATIONS.includes(value);
93
+ }
@@ -1,4 +1,5 @@
1
1
  export declare function generateSql(schemaPath?: string): Promise<string>;
2
2
  export declare function generateClient(schemaPath?: string): Promise<void>;
3
3
  export declare function generateApi(schemaPath?: string): Promise<void>;
4
+ export declare function syncGeneratedRouteFiles(routesDir: string, routes: Map<string, string>): Promise<void>;
4
5
  export declare function generateAll(schemaPath?: string): Promise<void>;
@@ -1,4 +1,4 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
1
+ import { mkdir, readdir, readFile, unlink, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { parse } from '../schema-dsl/index.js';
4
4
  import { generateApiFiles } from '../api-generator/index.js';
@@ -38,10 +38,17 @@ export async function generateApi(schemaPath) {
38
38
  await writeFile(path.join(schemasDir, 'validation.ts'), files.validation, 'utf8');
39
39
  await writeFile(path.join(outputDir, 'openapi.ts'), files.openapiTs, 'utf8');
40
40
  await writeFile(path.join(outputDir, 'openapi.json'), files.openapiJson, 'utf8');
41
- for (const [fileName, content] of files.routes) {
41
+ await syncGeneratedRouteFiles(routesDir, files.routes);
42
+ console.log(`Generated API files in ${outputDir}`);
43
+ }
44
+ export async function syncGeneratedRouteFiles(routesDir, routes) {
45
+ for (const [fileName, content] of routes) {
42
46
  await writeFile(path.join(routesDir, fileName), content, 'utf8');
43
47
  }
44
- console.log(`Generated API files in ${outputDir}`);
48
+ const existing = await readdir(routesDir);
49
+ await Promise.all(existing
50
+ .filter((fileName) => fileName.endsWith('.ts') && !routes.has(fileName))
51
+ .map((fileName) => unlink(path.join(routesDir, fileName))));
45
52
  }
46
53
  export async function generateAll(schemaPath) {
47
54
  const resolvedSchemaPath = resolveSchemaPath(schemaPath);
@@ -25,7 +25,7 @@ my-app/
25
25
  ├── app.schema # Source of truth — edit this
26
26
  ├── schema.sql # Generated PostgreSQL DDL (read-only)
27
27
  ├── AGENTS.md # This file
28
- ├── .env # DATABASE_URL, JWT_* settings
28
+ ├── .env # DATABASE_URL, JWT_*, CORS_ORIGIN
29
29
  ├── docker-compose.yml # Local PostgreSQL
30
30
  ├── generated/ # Generated — do not edit
31
31
  │ ├── db.ts # createDbClient(pool)
@@ -59,6 +59,7 @@ npx schematic-pg dev
59
59
  | `JWT_SECRET` | HMAC secret for Bearer JWT auth |
60
60
  | `JWT_ROLE_CLAIM` | JWT claim for role (default `role`) |
61
61
  | `JWT_USER_ID_CLAIM` | JWT claim for user id (default `sub`) |
62
+ | `CORS_ORIGIN` | Allowed browser origins (`*` or comma-separated). Unset disables CORS |
62
63
 
63
64
  ## Schema DSL Essentials
64
65
 
@@ -77,7 +78,8 @@ models {
77
78
 
78
79
  orders: Order[]
79
80
 
80
- @policy(role: USER, allow: [select, update], where: "id = {{auth.user.id}}")
81
+ @rest(except: [create, update, delete])
82
+ @policy(role: USER, allow: [select], where: "id = {{auth.user.id}}")
81
83
  @policy(role: ADMIN, allow: all)
82
84
 
83
85
  @@index(fields: [role])
@@ -93,6 +95,7 @@ models {
93
95
  **Key concepts:**
94
96
 
95
97
  - **Relations:** Put `@relation(fields: [...], references: [...])` on the FK-owning side. The inverse side is inferred.
98
+ - **REST surface:** `@rest(only: [...])`, `@rest(except: [...])`, or `@rest(false)` controls which CRUD handlers are generated (`list`/`get`/`create`/`update`/`delete`).
96
99
  - **Policies:** `@policy(role: ..., allow: [select|insert|update|delete|all], where: "...")` — `where` supports `{{auth.user.id}}`.
97
100
  - **Validation:** `@regex(...)`, `@range(min: ..., max: ...)` flow into generated Zod schemas.
98
101
  - **Indexes / triggers:** `@@index(...)`, `@@trigger { timing, event, level, execute: """...""" }`.
@@ -225,8 +228,9 @@ Also available: `ForeignKeyConstraintError`, `DatabaseError`.
225
228
  | Location | `src/routes/**/*.ts` |
226
229
  | Export | `export default router` (`Hono<AppEnv>`) |
227
230
  | Mount path | File path relative to `src/routes/` |
231
+ | Same-path overlay | `src/routes/users.ts` merges into `generated/routes/users.ts` |
228
232
 
229
- `src/routes/health.ts` → `GET /health`. Regenerate after adding files.
233
+ `src/routes/health.ts` → `GET /health` (standalone). When the file name matches a model route (`users.ts`), it is imported into the generated model router after remaining `@rest` handlers. Regenerate after adding files.
230
234
 
231
235
  ```typescript
232
236
  import { Hono } from 'hono';
@@ -276,6 +280,7 @@ schematic-pg hooks:add [--model X] # scaffold src/hooks/{Model}.ts
276
280
  - Unauthenticated requests default to `{ role: 'PUBLIC' }`.
277
281
  - `@policy` `allow` maps to HTTP: GET→select, POST→insert, PUT→update, DELETE→delete.
278
282
  - Row-level `where` is injected on read/update/delete; POST checks permission only.
283
+ - `@rest` controls which handlers exist; disabled methods return `404` and are omitted from OpenAPI.
279
284
 
280
285
  ## Generated Outputs (Read-Only)
281
286
 
@@ -1,6 +1,6 @@
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 @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
- 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";
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 @rest(except: [create, update, delete])\n @policy(role: USER, allow: [select], where: \"id = {{auth.user.id}}\")\n @policy(role: ADMIN, allow: all)\n }\n}\n";
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\nCORS_ORIGIN=\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";
6
6
  export declare const TSCONFIG_TEMPLATE = "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"NodeNext\",\n \"moduleResolution\": \"NodeNext\",\n \"strict\": true,\n \"esModuleInterop\": true,\n \"skipLibCheck\": true,\n \"outDir\": \"dist\",\n \"rootDir\": \".\"\n },\n \"include\": [\"generated/**/*\", \"src/**/*\"]\n}\n";
@@ -21,7 +21,8 @@ models {
21
21
  passwordHash: VARCHAR(255)? @omit @unfilterable
22
22
  createdAt: TIMESTAMP @default(now())
23
23
 
24
- @policy(role: USER, allow: [select, update], where: "id = {{auth.user.id}}")
24
+ @rest(except: [create, update, delete])
25
+ @policy(role: USER, allow: [select], where: "id = {{auth.user.id}}")
25
26
  @policy(role: ADMIN, allow: all)
26
27
  }
27
28
  }
@@ -32,6 +33,7 @@ AUTH_PEPPER=
32
33
  AUTH_ACCESS_TOKEN_TTL=1h
33
34
  JWT_ROLE_CLAIM=role
34
35
  JWT_USER_ID_CLAIM=sub
36
+ CORS_ORIGIN=
35
37
  `;
36
38
  export const GITIGNORE_TEMPLATE = `node_modules/
37
39
  dist/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schematic-pg",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Single-file backend framework for PostgreSQL and Node.js",
5
5
  "type": "module",
6
6
  "license": "MIT",