schematic-pg 0.1.0

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.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1019 -0
  3. package/dist/api/auth/errors.d.ts +6 -0
  4. package/dist/api/auth/errors.js +12 -0
  5. package/dist/api/auth/jwt-resolver.d.ts +7 -0
  6. package/dist/api/auth/jwt-resolver.js +59 -0
  7. package/dist/api/auth/middleware.d.ts +4 -0
  8. package/dist/api/auth/middleware.js +10 -0
  9. package/dist/api/auth/policy.d.ts +7 -0
  10. package/dist/api/auth/policy.js +95 -0
  11. package/dist/api/auth/template.d.ts +2 -0
  12. package/dist/api/auth/template.js +24 -0
  13. package/dist/api/auth/types.d.ts +12 -0
  14. package/dist/api/auth/types.js +1 -0
  15. package/dist/api/middleware/db.d.ts +8 -0
  16. package/dist/api/middleware/db.js +12 -0
  17. package/dist/api/middleware/errors.d.ts +5 -0
  18. package/dist/api/middleware/errors.js +27 -0
  19. package/dist/api/middleware/validate.d.ts +23 -0
  20. package/dist/api/middleware/validate.js +13 -0
  21. package/dist/api/types.d.ts +8 -0
  22. package/dist/api/types.js +1 -0
  23. package/dist/api/utils/route-naming.d.ts +3 -0
  24. package/dist/api/utils/route-naming.js +25 -0
  25. package/dist/api-generator/app-generator.d.ts +11 -0
  26. package/dist/api-generator/app-generator.js +79 -0
  27. package/dist/api-generator/custom-route-scanner.d.ts +6 -0
  28. package/dist/api-generator/custom-route-scanner.js +42 -0
  29. package/dist/api-generator/generate-api-cli.d.ts +1 -0
  30. package/dist/api-generator/generate-api-cli.js +28 -0
  31. package/dist/api-generator/index.d.ts +11 -0
  32. package/dist/api-generator/index.js +15 -0
  33. package/dist/api-generator/policy-generator.d.ts +9 -0
  34. package/dist/api-generator/policy-generator.js +33 -0
  35. package/dist/api-generator/route-generator.d.ts +20 -0
  36. package/dist/api-generator/route-generator.js +198 -0
  37. package/dist/api-generator/utils/policy.d.ts +18 -0
  38. package/dist/api-generator/utils/policy.js +72 -0
  39. package/dist/api-generator/zod-schema-generator.d.ts +16 -0
  40. package/dist/api-generator/zod-schema-generator.js +145 -0
  41. package/dist/cli/db.d.ts +4 -0
  42. package/dist/cli/db.js +144 -0
  43. package/dist/cli/dev.d.ts +1 -0
  44. package/dist/cli/dev.js +10 -0
  45. package/dist/cli/generate.d.ts +4 -0
  46. package/dist/cli/generate.js +50 -0
  47. package/dist/cli/init.d.ts +1 -0
  48. package/dist/cli/init.js +57 -0
  49. package/dist/cli/paths.d.ts +5 -0
  50. package/dist/cli/paths.js +10 -0
  51. package/dist/cli/templates.d.ts +6 -0
  52. package/dist/cli/templates.js +85 -0
  53. package/dist/cli.d.ts +2 -0
  54. package/dist/cli.js +81 -0
  55. package/dist/constants.d.ts +1 -0
  56. package/dist/constants.js +1 -0
  57. package/dist/db/bootstrap-cli.d.ts +1 -0
  58. package/dist/db/bootstrap-cli.js +17 -0
  59. package/dist/db/bootstrap.d.ts +3 -0
  60. package/dist/db/bootstrap.js +16 -0
  61. package/dist/db/cli.d.ts +1 -0
  62. package/dist/db/cli.js +20 -0
  63. package/dist/db/client.d.ts +8 -0
  64. package/dist/db/client.js +23 -0
  65. package/dist/db/config.d.ts +1 -0
  66. package/dist/db/config.js +10 -0
  67. package/dist/db/db-client-generator.d.ts +13 -0
  68. package/dist/db/db-client-generator.js +70 -0
  69. package/dist/db/diff-cli.d.ts +1 -0
  70. package/dist/db/diff-cli.js +46 -0
  71. package/dist/db/diff.d.ts +9 -0
  72. package/dist/db/diff.js +30 -0
  73. package/dist/db/errors.d.ts +34 -0
  74. package/dist/db/errors.js +88 -0
  75. package/dist/db/generate-client-cli.d.ts +1 -0
  76. package/dist/db/generate-client-cli.js +21 -0
  77. package/dist/db/index.d.ts +19 -0
  78. package/dist/db/index.js +17 -0
  79. package/dist/db/load-env.d.ts +3 -0
  80. package/dist/db/load-env.js +19 -0
  81. package/dist/db/migrate-cli.d.ts +1 -0
  82. package/dist/db/migrate-cli.js +88 -0
  83. package/dist/db/migrate.d.ts +3 -0
  84. package/dist/db/migrate.js +32 -0
  85. package/dist/db/migrations.d.ts +17 -0
  86. package/dist/db/migrations.js +81 -0
  87. package/dist/db/model-client.d.ts +36 -0
  88. package/dist/db/model-client.js +83 -0
  89. package/dist/db/model-meta.d.ts +36 -0
  90. package/dist/db/model-meta.js +57 -0
  91. package/dist/db/query-builder.d.ts +33 -0
  92. package/dist/db/query-builder.js +97 -0
  93. package/dist/db/reset-database.d.ts +4 -0
  94. package/dist/db/reset-database.js +9 -0
  95. package/dist/db/row-mapper.d.ts +3 -0
  96. package/dist/db/row-mapper.js +41 -0
  97. package/dist/db/schema-state.d.ts +7 -0
  98. package/dist/db/schema-state.js +32 -0
  99. package/dist/db/type-generator.d.ts +19 -0
  100. package/dist/db/type-generator.js +136 -0
  101. package/dist/db/utils/naming.d.ts +6 -0
  102. package/dist/db/utils/naming.js +23 -0
  103. package/dist/db/where-translator.d.ts +20 -0
  104. package/dist/db/where-translator.js +141 -0
  105. package/dist/index.d.ts +1 -0
  106. package/dist/index.js +1 -0
  107. package/dist/routes/health.d.ts +4 -0
  108. package/dist/routes/health.js +4 -0
  109. package/dist/schema-dsl/ast.d.ts +108 -0
  110. package/dist/schema-dsl/ast.js +1 -0
  111. package/dist/schema-dsl/cli.d.ts +1 -0
  112. package/dist/schema-dsl/cli.js +25 -0
  113. package/dist/schema-dsl/index.d.ts +8 -0
  114. package/dist/schema-dsl/index.js +16 -0
  115. package/dist/schema-dsl/inspect.d.ts +1 -0
  116. package/dist/schema-dsl/inspect.js +9 -0
  117. package/dist/schema-dsl/lexer.d.ts +31 -0
  118. package/dist/schema-dsl/lexer.js +216 -0
  119. package/dist/schema-dsl/parser.d.ts +49 -0
  120. package/dist/schema-dsl/parser.js +372 -0
  121. package/dist/schema-dsl/tokens.d.ts +30 -0
  122. package/dist/schema-dsl/tokens.js +35 -0
  123. package/dist/sql-generator/cli.d.ts +1 -0
  124. package/dist/sql-generator/cli.js +7 -0
  125. package/dist/sql-generator/generators/drop-tables.d.ts +2 -0
  126. package/dist/sql-generator/generators/drop-tables.js +8 -0
  127. package/dist/sql-generator/generators/enums.d.ts +4 -0
  128. package/dist/sql-generator/generators/enums.js +16 -0
  129. package/dist/sql-generator/generators/extensions.d.ts +4 -0
  130. package/dist/sql-generator/generators/extensions.js +11 -0
  131. package/dist/sql-generator/generators/foreign-keys.d.ts +4 -0
  132. package/dist/sql-generator/generators/foreign-keys.js +23 -0
  133. package/dist/sql-generator/generators/indexes.d.ts +13 -0
  134. package/dist/sql-generator/generators/indexes.js +39 -0
  135. package/dist/sql-generator/generators/tables.d.ts +4 -0
  136. package/dist/sql-generator/generators/tables.js +65 -0
  137. package/dist/sql-generator/generators/triggers.d.ts +6 -0
  138. package/dist/sql-generator/generators/triggers.js +47 -0
  139. package/dist/sql-generator/index.d.ts +5 -0
  140. package/dist/sql-generator/index.js +3 -0
  141. package/dist/sql-generator/migration-planner.d.ts +15 -0
  142. package/dist/sql-generator/migration-planner.js +207 -0
  143. package/dist/sql-generator/migration-sql-generator.d.ts +9 -0
  144. package/dist/sql-generator/migration-sql-generator.js +181 -0
  145. package/dist/sql-generator/migration-types.d.ts +86 -0
  146. package/dist/sql-generator/migration-types.js +1 -0
  147. package/dist/sql-generator/sql-generator.d.ts +6 -0
  148. package/dist/sql-generator/sql-generator.js +26 -0
  149. package/dist/sql-generator/utils/ast-helpers.d.ts +58 -0
  150. package/dist/sql-generator/utils/ast-helpers.js +252 -0
  151. package/dist/sql-generator/utils/format.d.ts +2 -0
  152. package/dist/sql-generator/utils/format.js +21 -0
  153. package/dist/sql-generator/utils/snake-case.d.ts +3 -0
  154. package/dist/sql-generator/utils/snake-case.js +96 -0
  155. package/dist/sql-generator/utils/type-mapper.d.ts +2 -0
  156. package/dist/sql-generator/utils/type-mapper.js +39 -0
  157. package/dist/sql-generator/utils/value-formatter.d.ts +4 -0
  158. package/dist/sql-generator/utils/value-formatter.js +41 -0
  159. package/dist/types/generated-db.stub.d.ts +2 -0
  160. package/dist/types/generated-db.stub.js +3 -0
  161. package/dist/types/generated-policies.stub.d.ts +7 -0
  162. package/dist/types/generated-policies.stub.js +1 -0
  163. package/package.json +86 -0
package/dist/cli/db.js ADDED
@@ -0,0 +1,144 @@
1
+ import { bootstrapDatabase } from '../db/bootstrap.js';
2
+ import { DatabaseClient } from '../db/client.js';
3
+ import { generateSchemaDiff, summarizeMigrations } from '../db/diff.js';
4
+ import { applyPendingMigrations } from '../db/migrate.js';
5
+ import { createMigration, getAppliedMigrationFilenames, listMigrationFiles } from '../db/migrations.js';
6
+ import { snapshotExists } from '../db/schema-state.js';
7
+ import { resolveSchemaPath } from './paths.js';
8
+ function parseDiffArgs(args) {
9
+ let schemaPath = resolveSchemaPath();
10
+ let name;
11
+ let print = false;
12
+ for (let index = 0; index < args.length; index++) {
13
+ const arg = args[index];
14
+ if (arg === '--print') {
15
+ print = true;
16
+ continue;
17
+ }
18
+ if (arg === '--name') {
19
+ name = args[index + 1];
20
+ index++;
21
+ continue;
22
+ }
23
+ if (!arg.startsWith('--')) {
24
+ schemaPath = resolveSchemaPath(arg);
25
+ }
26
+ }
27
+ return { schemaPath, name, print };
28
+ }
29
+ function parseMigrateArgs(args) {
30
+ const command = args[0] === 'status' ? 'status' : 'migrate';
31
+ const schemaArg = args.find((arg) => !arg.startsWith('--') && arg !== 'status');
32
+ const schemaPath = resolveSchemaPath(schemaArg);
33
+ return { command, schemaPath };
34
+ }
35
+ export async function runDbPing() {
36
+ const client = new DatabaseClient();
37
+ try {
38
+ const result = await client.query('SELECT 1 AS ok');
39
+ const ok = result.rows[0]?.ok;
40
+ if (ok !== 1) {
41
+ throw new Error(`Unexpected ping result: ${String(ok)}`);
42
+ }
43
+ process.stdout.write('Database connection OK (SELECT 1)\n');
44
+ }
45
+ finally {
46
+ await client.close();
47
+ }
48
+ }
49
+ export async function runDbBootstrap(schemaPath) {
50
+ const resolvedSchemaPath = resolveSchemaPath(schemaPath);
51
+ const client = new DatabaseClient();
52
+ try {
53
+ await bootstrapDatabase(resolvedSchemaPath, client);
54
+ process.stdout.write(`Database bootstrapped from ${resolvedSchemaPath}\n`);
55
+ }
56
+ finally {
57
+ await client.close();
58
+ }
59
+ }
60
+ export async function runDbDiff(args) {
61
+ const { schemaPath, name, print } = parseDiffArgs(args);
62
+ const diff = generateSchemaDiff(schemaPath);
63
+ if (diff.migrations.length === 0) {
64
+ process.stdout.write('No schema changes detected.\n');
65
+ return;
66
+ }
67
+ if (diff.hasDestructiveChanges) {
68
+ process.stderr.write('Warning: migration includes destructive changes (drops).\n');
69
+ }
70
+ if (print || !name) {
71
+ process.stdout.write(diff.sql);
72
+ return;
73
+ }
74
+ const migration = createMigration(name, diff.sql);
75
+ process.stdout.write(`Migration written to ${migration.path}\n`);
76
+ }
77
+ async function showMigrationStatus(schemaPath, client) {
78
+ if (!snapshotExists()) {
79
+ process.stdout.write('Snapshot: missing (.schema-state/app.schema)\n');
80
+ process.stdout.write('\nPending schema changes (snapshot vs app.schema):\n');
81
+ process.stdout.write(' (snapshot not initialized — run db:bootstrap first)\n');
82
+ }
83
+ else {
84
+ process.stdout.write('Snapshot: present\n');
85
+ try {
86
+ const diff = generateSchemaDiff(schemaPath);
87
+ const counts = summarizeMigrations(diff.migrations);
88
+ process.stdout.write('\nPending schema changes (snapshot vs app.schema):\n');
89
+ if (counts.size === 0) {
90
+ process.stdout.write(' (none)\n');
91
+ }
92
+ else {
93
+ for (const [kind, count] of [...counts.entries()].sort()) {
94
+ process.stdout.write(` ${kind}: ${count}\n`);
95
+ }
96
+ if (diff.hasDestructiveChanges) {
97
+ process.stdout.write(' Warning: includes destructive changes\n');
98
+ }
99
+ }
100
+ }
101
+ catch (error) {
102
+ const message = error instanceof Error ? error.message : String(error);
103
+ process.stdout.write(`\nPending schema changes: error — ${message}\n`);
104
+ }
105
+ }
106
+ const applied = await client.withClient(async (pgClient) => getAppliedMigrationFilenames(pgClient));
107
+ const files = listMigrationFiles();
108
+ const pendingFiles = files.filter((migration) => !applied.has(migration.filename));
109
+ process.stdout.write('\nMigration files:\n');
110
+ if (files.length === 0) {
111
+ process.stdout.write(' (none)\n');
112
+ }
113
+ else {
114
+ for (const migration of files) {
115
+ const state = applied.has(migration.filename) ? 'applied' : 'pending';
116
+ process.stdout.write(` [${state}] ${migration.filename}\n`);
117
+ }
118
+ }
119
+ if (pendingFiles.length > 0) {
120
+ process.stdout.write(`\n${pendingFiles.length} migration file(s) pending apply.\n`);
121
+ }
122
+ }
123
+ export async function runDbMigrate(args) {
124
+ const { command, schemaPath } = parseMigrateArgs(args);
125
+ const client = new DatabaseClient();
126
+ try {
127
+ if (command === 'status') {
128
+ await showMigrationStatus(schemaPath, client);
129
+ return;
130
+ }
131
+ const applied = await applyPendingMigrations(schemaPath, client);
132
+ if (applied.length === 0) {
133
+ process.stdout.write('No pending migrations to apply.\n');
134
+ return;
135
+ }
136
+ for (const migration of applied) {
137
+ process.stdout.write(`Applied ${migration.filename}\n`);
138
+ }
139
+ process.stdout.write(`Snapshot updated from ${schemaPath}\n`);
140
+ }
141
+ finally {
142
+ await client.close();
143
+ }
144
+ }
@@ -0,0 +1 @@
1
+ export declare function runDev(schemaPath?: string): Promise<void>;
@@ -0,0 +1,10 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { generateApi, generateClient } from './generate.js';
4
+ import { DEFAULT_OUTPUT_DIR } from './paths.js';
5
+ export async function runDev(schemaPath) {
6
+ await generateClient(schemaPath);
7
+ await generateApi(schemaPath);
8
+ const appPath = path.resolve(DEFAULT_OUTPUT_DIR, 'app.ts');
9
+ execFileSync(process.execPath, ['--import', 'tsx', appPath], { stdio: 'inherit', cwd: process.cwd() });
10
+ }
@@ -0,0 +1,4 @@
1
+ export declare function generateSql(schemaPath?: string): Promise<string>;
2
+ export declare function generateClient(schemaPath?: string): Promise<void>;
3
+ export declare function generateApi(schemaPath?: string): Promise<void>;
4
+ export declare function generateAll(schemaPath?: string): Promise<void>;
@@ -0,0 +1,50 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { parse } from '../schema-dsl/index.js';
4
+ import { generateApiFiles } from '../api-generator/index.js';
5
+ import { generateDbClientFiles } from '../db/db-client-generator.js';
6
+ import { SqlGenerator } from '../sql-generator/sql-generator.js';
7
+ import { DEFAULT_CUSTOM_ROUTES_DIR, DEFAULT_OUTPUT_DIR, resolveSchemaPath, } from './paths.js';
8
+ export async function generateSql(schemaPath) {
9
+ const resolvedSchemaPath = resolveSchemaPath(schemaPath);
10
+ const source = await readFile(resolvedSchemaPath, 'utf8');
11
+ return new SqlGenerator().generateFromSource(source);
12
+ }
13
+ export async function generateClient(schemaPath) {
14
+ const resolvedSchemaPath = resolveSchemaPath(schemaPath);
15
+ const outputDir = path.resolve(DEFAULT_OUTPUT_DIR);
16
+ const source = await readFile(resolvedSchemaPath, 'utf8');
17
+ const schema = parse(source);
18
+ const files = generateDbClientFiles(schema);
19
+ await mkdir(outputDir, { recursive: true });
20
+ await writeFile(path.join(outputDir, 'db-types.ts'), files.dbTypes, 'utf8');
21
+ await writeFile(path.join(outputDir, 'db-model-meta.ts'), files.modelMeta, 'utf8');
22
+ await writeFile(path.join(outputDir, 'db.ts'), files.dbClient, 'utf8');
23
+ console.log(`Generated db client files in ${outputDir}`);
24
+ }
25
+ export async function generateApi(schemaPath) {
26
+ const resolvedSchemaPath = resolveSchemaPath(schemaPath);
27
+ const outputDir = path.resolve(DEFAULT_OUTPUT_DIR);
28
+ const routesDir = path.join(outputDir, 'routes');
29
+ const schemasDir = path.join(outputDir, 'schemas');
30
+ const source = await readFile(resolvedSchemaPath, 'utf8');
31
+ const schema = parse(source);
32
+ const files = generateApiFiles(schema, { customRoutesDir: DEFAULT_CUSTOM_ROUTES_DIR });
33
+ await mkdir(routesDir, { recursive: true });
34
+ await mkdir(schemasDir, { recursive: true });
35
+ await writeFile(path.join(outputDir, 'app.ts'), files.app, 'utf8');
36
+ await writeFile(path.join(outputDir, 'policies.ts'), files.policies, 'utf8');
37
+ await writeFile(path.join(schemasDir, 'validation.ts'), files.validation, 'utf8');
38
+ for (const [fileName, content] of files.routes) {
39
+ await writeFile(path.join(routesDir, fileName), content, 'utf8');
40
+ }
41
+ console.log(`Generated API files in ${outputDir}`);
42
+ }
43
+ export async function generateAll(schemaPath) {
44
+ const resolvedSchemaPath = resolveSchemaPath(schemaPath);
45
+ const sql = await generateSql(resolvedSchemaPath);
46
+ await writeFile(path.resolve('schema.sql'), sql, 'utf8');
47
+ console.log('Generated schema.sql');
48
+ await generateClient(resolvedSchemaPath);
49
+ await generateApi(resolvedSchemaPath);
50
+ }
@@ -0,0 +1 @@
1
+ export declare function runInit(args: string[]): Promise<void>;
@@ -0,0 +1,57 @@
1
+ import { PACKAGE_NAME } from '../constants.js';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, readdir, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { APP_SCHEMA_TEMPLATE, createPackageJsonTemplate, DOCKER_COMPOSE_TEMPLATE, ENV_TEMPLATE, HEALTH_ROUTE_TEMPLATE, TSCONFIG_TEMPLATE, } from './templates.js';
6
+ const INIT_FILES = [
7
+ { relativePath: 'app.schema', content: APP_SCHEMA_TEMPLATE },
8
+ { relativePath: '.env', content: ENV_TEMPLATE },
9
+ { relativePath: 'docker-compose.yml', content: DOCKER_COMPOSE_TEMPLATE },
10
+ { relativePath: 'tsconfig.json', content: TSCONFIG_TEMPLATE },
11
+ { relativePath: 'src/routes/health.ts', content: HEALTH_ROUTE_TEMPLATE },
12
+ ];
13
+ function resolveTargetDir(args) {
14
+ const targetArg = args.find((arg) => !arg.startsWith('--'));
15
+ return path.resolve(targetArg ?? '.');
16
+ }
17
+ function resolveProjectName(targetDir) {
18
+ return path.basename(targetDir) || `${PACKAGE_NAME}-app`;
19
+ }
20
+ export async function runInit(args) {
21
+ const targetDir = resolveTargetDir(args);
22
+ const projectName = resolveProjectName(targetDir);
23
+ if (targetDir !== process.cwd() && existsSync(targetDir)) {
24
+ const entries = await readdir(targetDir);
25
+ if (entries.length > 0) {
26
+ throw new Error(`Target directory is not empty: ${targetDir}`);
27
+ }
28
+ }
29
+ await mkdir(targetDir, { recursive: true });
30
+ await mkdir(path.join(targetDir, 'src/routes'), { recursive: true });
31
+ for (const file of INIT_FILES) {
32
+ const filePath = path.join(targetDir, file.relativePath);
33
+ if (existsSync(filePath)) {
34
+ console.log(`Skipped existing file: ${file.relativePath}`);
35
+ continue;
36
+ }
37
+ await writeFile(filePath, file.content, 'utf8');
38
+ console.log(`Created ${file.relativePath}`);
39
+ }
40
+ const packageJsonPath = path.join(targetDir, 'package.json');
41
+ if (!existsSync(packageJsonPath)) {
42
+ await writeFile(packageJsonPath, `${createPackageJsonTemplate(projectName)}\n`, 'utf8');
43
+ console.log('Created package.json');
44
+ }
45
+ else {
46
+ console.log('Skipped existing file: package.json');
47
+ }
48
+ console.log('\nNext steps:');
49
+ if (targetDir !== process.cwd()) {
50
+ console.log(` cd ${targetDir}`);
51
+ }
52
+ console.log(' npm install');
53
+ console.log(' docker compose up -d');
54
+ console.log(` npx ${PACKAGE_NAME} generate`);
55
+ console.log(` npx ${PACKAGE_NAME} db:bootstrap`);
56
+ console.log(` npx ${PACKAGE_NAME} dev`);
57
+ }
@@ -0,0 +1,5 @@
1
+ export declare const DEFAULT_SCHEMA_FILE = "app.schema";
2
+ export declare const DEFAULT_OUTPUT_DIR = "generated";
3
+ export declare const DEFAULT_CUSTOM_ROUTES_DIR: string;
4
+ export declare function resolveSchemaPath(schemaArg?: string): string;
5
+ export declare function resolveOutputDir(): string;
@@ -0,0 +1,10 @@
1
+ import path from 'node:path';
2
+ export const DEFAULT_SCHEMA_FILE = 'app.schema';
3
+ export const DEFAULT_OUTPUT_DIR = 'generated';
4
+ export const DEFAULT_CUSTOM_ROUTES_DIR = path.resolve('src/routes');
5
+ export function resolveSchemaPath(schemaArg) {
6
+ return path.resolve(schemaArg ?? DEFAULT_SCHEMA_FILE);
7
+ }
8
+ export function resolveOutputDir() {
9
+ return path.resolve(DEFAULT_OUTPUT_DIR);
10
+ }
@@ -0,0 +1,6 @@
1
+ export declare const APP_SCHEMA_TEMPLATE = "models {\n model User {\n id: UUID @id @default(gen_random_uuid())\n email: VARCHAR(255) @unique\n name: VARCHAR(150)\n createdAt: TIMESTAMP @default(now())\n }\n}\n";
2
+ export declare const ENV_TEMPLATE = "DATABASE_URL=postgresql://postgrest:postgrest@localhost:5432/postgrest\nJWT_SECRET=\nJWT_ROLE_CLAIM=role\nJWT_USER_ID_CLAIM=sub\n";
3
+ export declare const DOCKER_COMPOSE_TEMPLATE = "services:\n postgres:\n image: postgis/postgis:16-3.4\n container_name: schematic-pg-postgres\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/data\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U postgrest -d postgrest\"]\n interval: 5s\n timeout: 5s\n retries: 5\n";
4
+ 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";
5
+ export declare const HEALTH_ROUTE_TEMPLATE = "import { Hono } from 'hono';\nimport type { AppEnv } from 'schematic-pg/api/types';\n\nconst router = new Hono<AppEnv>();\nrouter.get('/', (c) => c.json({ ok: true }));\nexport default router;\n";
6
+ export declare function createPackageJsonTemplate(projectName: string): string;
@@ -0,0 +1,85 @@
1
+ import { PACKAGE_NAME } from '../constants.js';
2
+ export const APP_SCHEMA_TEMPLATE = `models {
3
+ model User {
4
+ id: UUID @id @default(gen_random_uuid())
5
+ email: VARCHAR(255) @unique
6
+ name: VARCHAR(150)
7
+ createdAt: TIMESTAMP @default(now())
8
+ }
9
+ }
10
+ `;
11
+ export const ENV_TEMPLATE = `DATABASE_URL=postgresql://postgrest:postgrest@localhost:5432/postgrest
12
+ JWT_SECRET=
13
+ JWT_ROLE_CLAIM=role
14
+ JWT_USER_ID_CLAIM=sub
15
+ `;
16
+ export const DOCKER_COMPOSE_TEMPLATE = `services:
17
+ postgres:
18
+ image: postgis/postgis:16-3.4
19
+ container_name: schematic-pg-postgres
20
+ restart: unless-stopped
21
+ ports:
22
+ - "5432:5432"
23
+ environment:
24
+ POSTGRES_USER: postgrest
25
+ POSTGRES_PASSWORD: postgrest
26
+ POSTGRES_DB: postgrest
27
+ volumes:
28
+ - ./docker_data/postgres:/var/lib/postgresql/data
29
+ healthcheck:
30
+ test: ["CMD-SHELL", "pg_isready -U postgrest -d postgrest"]
31
+ interval: 5s
32
+ timeout: 5s
33
+ retries: 5
34
+ `;
35
+ export const TSCONFIG_TEMPLATE = `{
36
+ "compilerOptions": {
37
+ "target": "ES2022",
38
+ "module": "NodeNext",
39
+ "moduleResolution": "NodeNext",
40
+ "strict": true,
41
+ "esModuleInterop": true,
42
+ "skipLibCheck": true,
43
+ "outDir": "dist",
44
+ "rootDir": "."
45
+ },
46
+ "include": ["generated/**/*", "src/**/*"]
47
+ }
48
+ `;
49
+ export const HEALTH_ROUTE_TEMPLATE = `import { Hono } from 'hono';
50
+ import type { AppEnv } from '${PACKAGE_NAME}/api/types';
51
+
52
+ const router = new Hono<AppEnv>();
53
+ router.get('/', (c) => c.json({ ok: true }));
54
+ export default router;
55
+ `;
56
+ export function createPackageJsonTemplate(projectName) {
57
+ return JSON.stringify({
58
+ name: projectName,
59
+ version: '0.1.0',
60
+ private: true,
61
+ type: 'module',
62
+ imports: {
63
+ 'generated/db.js': './generated/db.js',
64
+ 'generated/policies.js': './generated/policies.js',
65
+ },
66
+ scripts: {
67
+ dev: `${PACKAGE_NAME} dev`,
68
+ generate: `${PACKAGE_NAME} generate`,
69
+ 'db:bootstrap': `${PACKAGE_NAME} db:bootstrap`,
70
+ 'db:migrate': `${PACKAGE_NAME} db:migrate`,
71
+ },
72
+ dependencies: {
73
+ '@hono/node-server': '^2.0.6',
74
+ '@hono/zod-validator': '^0.8.0',
75
+ hono: '^4.12.27',
76
+ pg: '^8.22.0',
77
+ [PACKAGE_NAME]: '^0.1.0',
78
+ zod: '^4.4.3',
79
+ },
80
+ devDependencies: {
81
+ '@types/node': '^22.15.21',
82
+ typescript: '^5.8.3',
83
+ },
84
+ }, null, 2);
85
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ import { runDbBootstrap, runDbDiff, runDbMigrate, runDbPing } from './cli/db.js';
3
+ import { runDev } from './cli/dev.js';
4
+ import { generateAll, generateApi, generateClient, generateSql } from './cli/generate.js';
5
+ import { runInit } from './cli/init.js';
6
+ import { PACKAGE_NAME } from './constants.js';
7
+ const USAGE = `Usage: ${PACKAGE_NAME} <command> [options]
8
+
9
+ Commands:
10
+ init [dir] Scaffold a new project
11
+ generate [schema] Generate schema.sql, db client, and API
12
+ generate:sql [schema] Generate SQL DDL to stdout
13
+ generate:client [schema] Generate db client files
14
+ generate:api [schema] Generate API files
15
+ dev [schema] Regenerate client + API and start server
16
+ db:ping Test database connection
17
+ db:bootstrap [schema] Apply DDL and snapshot schema state
18
+ db:diff [schema] Show schema diff (--name <name> to write migration)
19
+ db:migrate [schema] Apply pending migrations
20
+ db:migrate:status [schema] Show migration status
21
+ `;
22
+ function getPositionalArgs(args) {
23
+ return args.filter((arg) => !arg.startsWith('--'));
24
+ }
25
+ async function main() {
26
+ const [command, ...args] = process.argv.slice(2);
27
+ if (!command || command === '--help' || command === '-h') {
28
+ process.stdout.write(USAGE);
29
+ return;
30
+ }
31
+ const positionalArgs = getPositionalArgs(args);
32
+ const schemaPath = positionalArgs[0];
33
+ try {
34
+ switch (command) {
35
+ case 'init':
36
+ await runInit(args);
37
+ break;
38
+ case 'generate':
39
+ await generateAll(schemaPath);
40
+ break;
41
+ case 'generate:sql': {
42
+ const sql = await generateSql(schemaPath);
43
+ process.stdout.write(sql);
44
+ break;
45
+ }
46
+ case 'generate:client':
47
+ await generateClient(schemaPath);
48
+ break;
49
+ case 'generate:api':
50
+ await generateApi(schemaPath);
51
+ break;
52
+ case 'dev':
53
+ await runDev(schemaPath);
54
+ break;
55
+ case 'db:ping':
56
+ await runDbPing();
57
+ break;
58
+ case 'db:bootstrap':
59
+ await runDbBootstrap(schemaPath);
60
+ break;
61
+ case 'db:diff':
62
+ await runDbDiff(args);
63
+ break;
64
+ case 'db:migrate':
65
+ await runDbMigrate(args);
66
+ break;
67
+ case 'db:migrate:status':
68
+ await runDbMigrate(['status', ...args]);
69
+ break;
70
+ default:
71
+ process.stderr.write(`Unknown command: ${command}\n\n${USAGE}`);
72
+ process.exitCode = 1;
73
+ }
74
+ }
75
+ catch (error) {
76
+ const message = error instanceof Error ? error.message : String(error);
77
+ process.stderr.write(`${message}\n`);
78
+ process.exitCode = 1;
79
+ }
80
+ }
81
+ main();
@@ -0,0 +1 @@
1
+ export declare const PACKAGE_NAME = "schematic-pg";
@@ -0,0 +1 @@
1
+ export const PACKAGE_NAME = 'schematic-pg';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { join } from 'node:path';
2
+ import { bootstrapDatabase } from './bootstrap.js';
3
+ import { DatabaseClient } from './client.js';
4
+ const schemaPath = process.argv[2] ?? join(process.cwd(), 'app.schema');
5
+ const client = new DatabaseClient();
6
+ bootstrapDatabase(schemaPath, client)
7
+ .then(() => {
8
+ process.stdout.write(`Database bootstrapped from ${schemaPath}\n`);
9
+ })
10
+ .catch((error) => {
11
+ const message = error instanceof Error ? error.message : String(error);
12
+ process.stderr.write(`Database bootstrap failed: ${message}\n`);
13
+ process.exitCode = 1;
14
+ })
15
+ .finally(async () => {
16
+ await client.close();
17
+ });
@@ -0,0 +1,3 @@
1
+ import { DatabaseClient } from './client.js';
2
+ export declare function generateBootstrapSql(schemaPath: string): string;
3
+ export declare function bootstrapDatabase(schemaPath?: string, client?: Pick<DatabaseClient, 'withClient'>): Promise<void>;
@@ -0,0 +1,16 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { SqlGenerator } from '../sql-generator/sql-generator.js';
4
+ import { DatabaseClient } from './client.js';
5
+ import { writeSnapshot } from './schema-state.js';
6
+ export function generateBootstrapSql(schemaPath) {
7
+ const source = readFileSync(schemaPath, 'utf8');
8
+ return new SqlGenerator().generateFromSource(source);
9
+ }
10
+ export async function bootstrapDatabase(schemaPath = join(process.cwd(), 'app.schema'), client = new DatabaseClient()) {
11
+ const sql = generateBootstrapSql(schemaPath);
12
+ await client.withClient(async (pgClient) => {
13
+ await pgClient.query(sql);
14
+ });
15
+ writeSnapshot(schemaPath);
16
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/db/cli.js ADDED
@@ -0,0 +1,20 @@
1
+ import { DatabaseClient } from './client.js';
2
+ async function main() {
3
+ const client = new DatabaseClient();
4
+ try {
5
+ const result = await client.query('SELECT 1 AS ok');
6
+ const ok = result.rows[0]?.ok;
7
+ if (ok !== 1) {
8
+ throw new Error(`Unexpected ping result: ${String(ok)}`);
9
+ }
10
+ process.stdout.write('Database connection OK (SELECT 1)\n');
11
+ }
12
+ finally {
13
+ await client.close();
14
+ }
15
+ }
16
+ main().catch((error) => {
17
+ const message = error instanceof Error ? error.message : String(error);
18
+ process.stderr.write(`Database connection failed: ${message}\n`);
19
+ process.exitCode = 1;
20
+ });
@@ -0,0 +1,8 @@
1
+ import { type PoolClient, type QueryResult, type QueryResultRow } from 'pg';
2
+ export declare class DatabaseClient {
3
+ private readonly pool;
4
+ constructor(connectionString?: string);
5
+ query<T extends QueryResultRow>(sql: string, params?: unknown[]): Promise<QueryResult<T>>;
6
+ withClient<T>(fn: (client: PoolClient) => Promise<T>): Promise<T>;
7
+ close(): Promise<void>;
8
+ }
@@ -0,0 +1,23 @@
1
+ import { Pool } from 'pg';
2
+ import { getDatabaseUrl } from './config.js';
3
+ export class DatabaseClient {
4
+ pool;
5
+ constructor(connectionString = getDatabaseUrl()) {
6
+ this.pool = new Pool({ connectionString });
7
+ }
8
+ async query(sql, params = []) {
9
+ return this.pool.query(sql, params);
10
+ }
11
+ async withClient(fn) {
12
+ const client = await this.pool.connect();
13
+ try {
14
+ return await fn(client);
15
+ }
16
+ finally {
17
+ client.release();
18
+ }
19
+ }
20
+ async close() {
21
+ await this.pool.end();
22
+ }
23
+ }
@@ -0,0 +1 @@
1
+ export declare function getDatabaseUrl(): string;
@@ -0,0 +1,10 @@
1
+ import { loadEnv } from './load-env.js';
2
+ const missingDatabaseUrlMessage = 'DATABASE_URL is not set. Copy .env.example to .env and configure your connection string.';
3
+ export function getDatabaseUrl() {
4
+ loadEnv();
5
+ const databaseUrl = process.env.DATABASE_URL;
6
+ if (!databaseUrl) {
7
+ throw new Error(missingDatabaseUrlMessage);
8
+ }
9
+ return databaseUrl;
10
+ }
@@ -0,0 +1,13 @@
1
+ import type { Schema } from '../schema-dsl/ast.js';
2
+ export declare class DbClientGenerator {
3
+ private readonly schema;
4
+ constructor(schema: Schema);
5
+ generate(): string;
6
+ generateModelMetaModule(): string;
7
+ private generateClientEntry;
8
+ }
9
+ export declare function generateDbClientFiles(schema: Schema): {
10
+ dbTypes: string;
11
+ dbClient: string;
12
+ modelMeta: string;
13
+ };