schematic-pg 0.1.16 → 0.1.18

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,13 @@ 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 (no cookies), or a comma-separated list (`http://localhost:5173,https://app.example.com`). Concrete origins enable credentialed CORS |
460
+ | `CORS_ALLOW_HEADERS` | — | Extra allowed request headers (comma-separated), merged with `Authorization`, `Content-Type`, `X-CSRF-Token` |
459
461
 
460
462
  Set these in `.env` before running `dev`, `start`, or `db:bootstrap`.
461
463
 
464
+ Browser frontends on another origin need `CORS_ORIGIN`. The generated app reads it at runtime (no regenerate). Preflight `OPTIONS` is handled automatically; concrete origins enable cookies via `credentials: 'include'`. See [CORS](docs/rest-api.md#cors).
465
+
462
466
  ---
463
467
 
464
468
  ## Authentication
@@ -0,0 +1,9 @@
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
+ /** Extra headers from `CORS_ALLOW_HEADERS` (comma-separated). Does not include defaults. */
5
+ export declare function parseCorsAllowHeaders(raw?: string | undefined): string[];
6
+ export declare function createCorsMiddleware(options?: {
7
+ origin?: string | string[] | null;
8
+ allowHeaders?: string[];
9
+ }): MiddlewareHandler<AppEnv>;
@@ -0,0 +1,69 @@
1
+ import { cors } from 'hono/cors';
2
+ const DEFAULT_CORS_ALLOW_HEADERS = ['Authorization', 'Content-Type', 'X-CSRF-Token'];
3
+ const CORS_ALLOW_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'];
4
+ const CORS_MAX_AGE_SECONDS = 86400;
5
+ function parseCommaSeparatedList(raw) {
6
+ const trimmed = raw?.trim() ?? '';
7
+ if (!trimmed) {
8
+ return [];
9
+ }
10
+ return trimmed
11
+ .split(',')
12
+ .map((item) => item.trim())
13
+ .filter((item) => item.length > 0);
14
+ }
15
+ export function parseCorsOrigin(raw = process.env.CORS_ORIGIN) {
16
+ const trimmed = raw?.trim() ?? '';
17
+ if (!trimmed) {
18
+ return null;
19
+ }
20
+ if (trimmed === '*') {
21
+ return '*';
22
+ }
23
+ const origins = parseCommaSeparatedList(trimmed);
24
+ if (origins.length === 0) {
25
+ return null;
26
+ }
27
+ return origins.length === 1 ? origins[0] : origins;
28
+ }
29
+ /** Extra headers from `CORS_ALLOW_HEADERS` (comma-separated). Does not include defaults. */
30
+ export function parseCorsAllowHeaders(raw = process.env.CORS_ALLOW_HEADERS) {
31
+ return parseCommaSeparatedList(raw);
32
+ }
33
+ function resolveAllowHeaders(optionsAllowHeaders) {
34
+ if (optionsAllowHeaders !== undefined) {
35
+ return optionsAllowHeaders;
36
+ }
37
+ const extras = parseCorsAllowHeaders();
38
+ if (extras.length === 0) {
39
+ return DEFAULT_CORS_ALLOW_HEADERS;
40
+ }
41
+ const seen = new Set(DEFAULT_CORS_ALLOW_HEADERS.map((header) => header.toLowerCase()));
42
+ const merged = [...DEFAULT_CORS_ALLOW_HEADERS];
43
+ for (const header of extras) {
44
+ const key = header.toLowerCase();
45
+ if (seen.has(key)) {
46
+ continue;
47
+ }
48
+ seen.add(key);
49
+ merged.push(header);
50
+ }
51
+ return merged;
52
+ }
53
+ export function createCorsMiddleware(options = {}) {
54
+ const origin = options.origin !== undefined ? options.origin : parseCorsOrigin();
55
+ if (origin == null) {
56
+ return async (_c, next) => {
57
+ await next();
58
+ };
59
+ }
60
+ const allowHeaders = resolveAllowHeaders(options.allowHeaders);
61
+ const credentials = origin !== '*';
62
+ return cors({
63
+ origin,
64
+ credentials,
65
+ allowHeaders,
66
+ allowMethods: CORS_ALLOW_METHODS,
67
+ maxAge: CORS_MAX_AGE_SECONDS,
68
+ });
69
+ }
@@ -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());',
@@ -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,8 @@ 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. Concrete origins enable credentialed CORS (cookies); `*` does not |
63
+ | `CORS_ALLOW_HEADERS` | Extra allowed request headers (comma-separated), merged with Authorization, Content-Type, X-CSRF-Token |
62
64
 
63
65
  ## Schema DSL Essentials
64
66
 
@@ -1,6 +1,6 @@
1
1
  export declare const AGENTS_TEMPLATE: string;
2
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\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=\nCORS_ALLOW_HEADERS=\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";
@@ -33,6 +33,8 @@ AUTH_PEPPER=
33
33
  AUTH_ACCESS_TOKEN_TTL=1h
34
34
  JWT_ROLE_CLAIM=role
35
35
  JWT_USER_ID_CLAIM=sub
36
+ CORS_ORIGIN=
37
+ CORS_ALLOW_HEADERS=
36
38
  `;
37
39
  export const GITIGNORE_TEMPLATE = `node_modules/
38
40
  dist/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schematic-pg",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Single-file backend framework for PostgreSQL and Node.js",
5
5
  "type": "module",
6
6
  "license": "MIT",