schematic-pg 0.1.7 → 0.1.10

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 (78) hide show
  1. package/README.md +237 -970
  2. package/dist/api/auth/jwt-crypto.d.ts +10 -0
  3. package/dist/api/auth/jwt-crypto.js +61 -0
  4. package/dist/api/auth/jwt-resolver.js +2 -27
  5. package/dist/api/auth/password/config.d.ts +32 -0
  6. package/dist/api/auth/password/config.js +35 -0
  7. package/dist/api/auth/password/errors.d.ts +8 -0
  8. package/dist/api/auth/password/errors.js +14 -0
  9. package/dist/api/auth/password/index.d.ts +3 -0
  10. package/dist/api/auth/password/index.js +3 -0
  11. package/dist/api/auth/password/password.d.ts +14 -0
  12. package/dist/api/auth/password/password.js +46 -0
  13. package/dist/api/auth/routes.d.ts +23 -0
  14. package/dist/api/auth/routes.js +118 -0
  15. package/dist/api/auth/token/config.d.ts +10 -0
  16. package/dist/api/auth/token/config.js +43 -0
  17. package/dist/api/auth/token/errors.d.ts +6 -0
  18. package/dist/api/auth/token/errors.js +12 -0
  19. package/dist/api/auth/token/index.d.ts +3 -0
  20. package/dist/api/auth/token/index.js +3 -0
  21. package/dist/api/auth/token/token.d.ts +11 -0
  22. package/dist/api/auth/token/token.js +31 -0
  23. package/dist/api/hooks/define.d.ts +10 -0
  24. package/dist/api/hooks/define.js +3 -0
  25. package/dist/api/hooks/index.d.ts +4 -0
  26. package/dist/api/hooks/index.js +2 -0
  27. package/dist/api/hooks/registry.d.ts +8 -0
  28. package/dist/api/hooks/registry.js +94 -0
  29. package/dist/api/hooks/types.d.ts +48 -0
  30. package/dist/api/hooks/types.js +1 -0
  31. package/dist/api/middleware/errors.js +11 -0
  32. package/dist/api-generator/app-generator.js +3 -0
  33. package/dist/api-generator/hook-scanner.d.ts +11 -0
  34. package/dist/api-generator/hook-scanner.js +36 -0
  35. package/dist/api-generator/hooks-generator.d.ts +2 -0
  36. package/dist/api-generator/hooks-generator.js +25 -0
  37. package/dist/api-generator/index.d.ts +2 -0
  38. package/dist/api-generator/index.js +7 -1
  39. package/dist/api-generator/route-generator.d.ts +3 -2
  40. package/dist/api-generator/route-generator.js +85 -64
  41. package/dist/cli/dev.js +5 -36
  42. package/dist/cli/generate.js +1 -0
  43. package/dist/cli/hooks.d.ts +6 -0
  44. package/dist/cli/hooks.js +85 -0
  45. package/dist/cli/init.js +9 -2
  46. package/dist/cli/paths.d.ts +1 -0
  47. package/dist/cli/paths.js +1 -0
  48. package/dist/cli/server.d.ts +5 -0
  49. package/dist/cli/server.js +60 -0
  50. package/dist/cli/start.d.ts +7 -0
  51. package/dist/cli/start.js +35 -0
  52. package/dist/cli/templates/agents.md +290 -0
  53. package/dist/cli/templates.d.ts +6 -3
  54. package/dist/cli/templates.js +58 -6
  55. package/dist/cli/wait-for-database.js +1 -1
  56. package/dist/cli.js +10 -0
  57. package/dist/db/db-client-generator.js +20 -2
  58. package/dist/db/include/executor.d.ts +2 -2
  59. package/dist/db/include/executor.js +7 -7
  60. package/dist/db/include/json-agg.d.ts +2 -2
  61. package/dist/db/include/json-agg.js +4 -4
  62. package/dist/db/include/load.d.ts +3 -3
  63. package/dist/db/include/load.js +8 -8
  64. package/dist/db/index.d.ts +4 -0
  65. package/dist/db/index.js +2 -0
  66. package/dist/db/model-client.d.ts +2 -2
  67. package/dist/db/model-client.js +3 -3
  68. package/dist/db/queryable.d.ts +5 -0
  69. package/dist/db/queryable.js +1 -0
  70. package/dist/db/raw.d.ts +22 -0
  71. package/dist/db/raw.js +35 -0
  72. package/dist/db/transaction.d.ts +2 -0
  73. package/dist/db/transaction.js +24 -0
  74. package/dist/routes/auth.d.ts +3 -0
  75. package/dist/routes/auth.js +5 -0
  76. package/dist/types/generated-db.stub.d.ts +5 -1
  77. package/dist/types/generated-db.stub.js +8 -1
  78. package/package.json +11 -4
package/dist/cli.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { runDbBootstrap, runDbDiff, runDbMigrate, runDbPing } from './cli/db.js';
3
3
  import { runDev } from './cli/dev.js';
4
+ import { runStart } from './cli/start.js';
4
5
  import { generateAll, generateApi, generateClient, generateSql } from './cli/generate.js';
6
+ import { runHooksAdd } from './cli/hooks.js';
5
7
  import { runInit } from './cli/init.js';
6
8
  import { PACKAGE_NAME } from './constants.js';
7
9
  const USAGE = `Usage: ${PACKAGE_NAME} <command> [options]
@@ -12,7 +14,9 @@ Commands:
12
14
  generate:sql [schema] Generate SQL DDL to stdout
13
15
  generate:client [schema] Generate db client files
14
16
  generate:api [schema] Generate API files
17
+ hooks:add [schema] [--model ModelName] Scaffold lifecycle hooks for a model
15
18
  dev [schema] [--no-watch] Generate, bootstrap DB, start server, watch schema
19
+ start [schema] [--no-migrate] Run production server (migrate DB, no generate/watch)
16
20
  db:ping Test database connection
17
21
  db:bootstrap [schema] Apply DDL and snapshot schema state
18
22
  db:diff [schema] Show schema diff (--name <name> to write migration)
@@ -49,9 +53,15 @@ async function main() {
49
53
  case 'generate:api':
50
54
  await generateApi(schemaPath);
51
55
  break;
56
+ case 'hooks:add':
57
+ await runHooksAdd(args);
58
+ break;
52
59
  case 'dev':
53
60
  await runDev(args);
54
61
  break;
62
+ case 'start':
63
+ await runStart(args);
64
+ break;
55
65
  case 'db:ping':
56
66
  await runDbPing();
57
67
  break;
@@ -31,10 +31,13 @@ export class DbClientGenerator {
31
31
  "import type { Pool } from 'pg';",
32
32
  `import { createModelClient } from '${PACKAGE_NAME}/db/model-client';`,
33
33
  `import { hydrateModelMeta } from '${PACKAGE_NAME}/db/model-meta';`,
34
+ `import type { Queryable } from '${PACKAGE_NAME}/db/queryable';`,
35
+ `import { createRawClient } from '${PACKAGE_NAME}/db/raw';`,
36
+ `import { runInTransaction } from '${PACKAGE_NAME}/db/transaction';`,
34
37
  `import type {\n ${imports},\n} from './db-types.js';`,
35
38
  `import {\n ${this.schema.models.map((model) => `${getClientExportName(model.name)}ModelMeta`).join(',\n ')},\n} from './db-model-meta.js';`,
36
39
  '',
37
- 'export function createDbClient(pool: Pool) {',
40
+ 'function buildModels(executor: Queryable) {',
38
41
  ...metaHydrations,
39
42
  ` const modelRegistry = new Map([`,
40
43
  ...registryEntries.map((entry) => ` ${entry},`),
@@ -42,6 +45,21 @@ export class DbClientGenerator {
42
45
  '',
43
46
  ' return {',
44
47
  clientEntries.map((entry) => ` ${entry},`).join('\n'),
48
+ ' ...createRawClient(executor),',
49
+ ' };',
50
+ '}',
51
+ '',
52
+ '/** All model clients scoped to a single transaction. Nested `$transaction` is unsupported. */',
53
+ 'export type TxClient = ReturnType<typeof buildModels>;',
54
+ '',
55
+ 'export function createDbClient(pool: Pool) {',
56
+ ' async function $transaction<T>(fn: (tx: TxClient) => Promise<T>): Promise<T> {',
57
+ ' return runInTransaction(pool, (client) => fn(buildModels(client)));',
58
+ ' }',
59
+ '',
60
+ ' return {',
61
+ ' ...buildModels(pool),',
62
+ ' $transaction,',
45
63
  ' };',
46
64
  '}',
47
65
  '',
@@ -65,7 +83,7 @@ export class DbClientGenerator {
65
83
  }
66
84
  generateClientEntry(modelName) {
67
85
  const clientKey = getClientExportName(modelName);
68
- return `${clientKey}: createModelClient<${modelName}, ${modelName}CreateInput, ${modelName}UpdateInput, ${modelName}WhereInput, ${modelName}OrderByInput>(${clientKey}Meta, pool, modelRegistry)`;
86
+ return `${clientKey}: createModelClient<${modelName}, ${modelName}CreateInput, ${modelName}UpdateInput, ${modelName}WhereInput, ${modelName}OrderByInput>(${clientKey}Meta, executor, modelRegistry)`;
69
87
  }
70
88
  }
71
89
  export function generateDbClientFiles(schema) {
@@ -1,3 +1,3 @@
1
- import type { Pool } from 'pg';
1
+ import type { Queryable } from '../queryable.js';
2
2
  import type { LoadNode } from './planner.js';
3
- export declare function loadIncludes(parentRows: Record<string, unknown>[], plan: LoadNode, pool: Pool): Promise<void>;
3
+ export declare function loadIncludes(parentRows: Record<string, unknown>[], plan: LoadNode, executor: Queryable): Promise<void>;
@@ -2,7 +2,7 @@ import { mapPgError } from '../errors.js';
2
2
  import { mapRows } from '../row-mapper.js';
3
3
  import { WhereTranslator } from '../where-translator.js';
4
4
  import { dedupeKeys, extractParentKeys, stitch } from './hydrator.js';
5
- export async function loadIncludes(parentRows, plan, pool) {
5
+ export async function loadIncludes(parentRows, plan, executor) {
6
6
  for (const childPlan of plan.children) {
7
7
  const relation = childPlan.relation;
8
8
  if (!relation) {
@@ -13,8 +13,8 @@ export async function loadIncludes(parentRows, plan, pool) {
13
13
  assignEmptyRelation(parentRows, relation);
14
14
  continue;
15
15
  }
16
- const childRows = await fetchRelationRows(childPlan, parentKeys, pool);
17
- await loadIncludes(childRows, childPlan, pool);
16
+ const childRows = await fetchRelationRows(childPlan, parentKeys, executor);
17
+ await loadIncludes(childRows, childPlan, executor);
18
18
  stitch(parentRows, childRows, relation);
19
19
  }
20
20
  }
@@ -23,13 +23,13 @@ function assignEmptyRelation(parentRows, relation) {
23
23
  parent[relation.name] = relation.unique ? null : [];
24
24
  }
25
25
  }
26
- async function fetchRelationRows(node, parentKeys, pool) {
26
+ async function fetchRelationRows(node, parentKeys, executor) {
27
27
  const relation = node.relation;
28
28
  if (!relation) {
29
29
  return [];
30
30
  }
31
31
  const query = buildRelationSelect(node, parentKeys);
32
- const rows = await executeQuery(pool, query.sql, query.params, node.model);
32
+ const rows = await executeQuery(executor, query.sql, query.params, node.model);
33
33
  return mapRows(rows, node.model);
34
34
  }
35
35
  function buildRelationSelect(node, parentKeys) {
@@ -79,9 +79,9 @@ function buildOrderByClause(plan) {
79
79
  }
80
80
  return parts.length > 0 ? `ORDER BY ${parts.join(', ')}` : '';
81
81
  }
82
- async function executeQuery(pool, sql, params, model) {
82
+ async function executeQuery(executor, sql, params, model) {
83
83
  try {
84
- const result = await pool.query(sql, params);
84
+ const result = await executor.query(sql, params);
85
85
  return result.rows;
86
86
  }
87
87
  catch (error) {
@@ -1,5 +1,5 @@
1
- import type { Pool } from 'pg';
2
1
  import type { ModelMeta } from '../model-meta.js';
3
2
  import type { FindArgs } from '../query-builder.js';
3
+ import type { Queryable } from '../queryable.js';
4
4
  import type { LoadNode } from './planner.js';
5
- export declare function fetchRootWithJsonAgg<T extends Record<string, unknown>>(model: ModelMeta, plan: LoadNode, args: FindArgs, pool: Pool): Promise<T[]>;
5
+ export declare function fetchRootWithJsonAgg<T extends Record<string, unknown>>(model: ModelMeta, plan: LoadNode, args: FindArgs, executor: Queryable): Promise<T[]>;
@@ -2,9 +2,9 @@ import { mapPgError } from '../errors.js';
2
2
  import { QueryBuilder } from '../query-builder.js';
3
3
  import { mapRow } from '../row-mapper.js';
4
4
  const ROOT_ALIAS = 'root';
5
- export async function fetchRootWithJsonAgg(model, plan, args, pool) {
5
+ export async function fetchRootWithJsonAgg(model, plan, args, executor) {
6
6
  const query = buildJsonAggRootQuery(model, plan, args);
7
- const rows = await executeQuery(pool, query.sql, query.params, model);
7
+ const rows = await executeQuery(executor, query.sql, query.params, model);
8
8
  return rows.map((row) => hydrateJsonAggRow(row, model, plan));
9
9
  }
10
10
  function buildJsonAggRootQuery(model, plan, args) {
@@ -90,9 +90,9 @@ function hydrateJsonObject(value, plan) {
90
90
  }
91
91
  return mapped;
92
92
  }
93
- async function executeQuery(pool, sql, params, model) {
93
+ async function executeQuery(executor, sql, params, model) {
94
94
  try {
95
- const result = await pool.query(sql, params);
95
+ const result = await executor.query(sql, params);
96
96
  return result.rows;
97
97
  }
98
98
  catch (error) {
@@ -1,8 +1,8 @@
1
- import type { Pool } from 'pg';
2
1
  import type { ModelMeta } from '../model-meta.js';
3
2
  import type { FindArgs } from '../query-builder.js';
3
+ import type { Queryable } from '../queryable.js';
4
4
  import type { IncludeInput, IncludeOptions } from './types.js';
5
- export declare function fetchWithIncludes<T extends Record<string, unknown>>(model: ModelMeta, registry: Map<string, ModelMeta>, pool: Pool, rootArgs: FindArgs & {
5
+ export declare function fetchWithIncludes<T extends Record<string, unknown>>(model: ModelMeta, registry: Map<string, ModelMeta>, executor: Queryable, rootArgs: FindArgs & {
6
6
  include?: IncludeInput;
7
7
  }, options?: IncludeOptions): Promise<T[]>;
8
- export declare function attachIncludes<T extends Record<string, unknown>>(model: ModelMeta, registry: Map<string, ModelMeta>, pool: Pool, rootRows: T[], include: IncludeInput, rootArgs: FindArgs, options?: IncludeOptions): Promise<T[]>;
8
+ export declare function attachIncludes<T extends Record<string, unknown>>(model: ModelMeta, registry: Map<string, ModelMeta>, executor: Queryable, rootRows: T[], include: IncludeInput, rootArgs: FindArgs, options?: IncludeOptions): Promise<T[]>;
@@ -4,7 +4,7 @@ import { mapRows } from '../row-mapper.js';
4
4
  import { loadIncludes } from './executor.js';
5
5
  import { fetchRootWithJsonAgg } from './json-agg.js';
6
6
  import { buildLoadPlan } from './planner.js';
7
- export async function fetchWithIncludes(model, registry, pool, rootArgs, options = {}) {
7
+ export async function fetchWithIncludes(model, registry, executor, rootArgs, options = {}) {
8
8
  if (!rootArgs.include) {
9
9
  throw new Error('fetchWithIncludes requires include');
10
10
  }
@@ -14,14 +14,14 @@ export async function fetchWithIncludes(model, registry, pool, rootArgs, options
14
14
  plan.take = rootArgs.take;
15
15
  plan.skip = rootArgs.skip;
16
16
  if (plan.strategy === 'join') {
17
- return fetchRootWithJsonAgg(model, plan, rootArgs, pool);
17
+ return fetchRootWithJsonAgg(model, plan, rootArgs, executor);
18
18
  }
19
- const rows = await executeRootSelect(model, pool, rootArgs);
19
+ const rows = await executeRootSelect(model, executor, rootArgs);
20
20
  const mapped = mapRows(rows, model);
21
- await loadIncludes(mapped, plan, pool);
21
+ await loadIncludes(mapped, plan, executor);
22
22
  return mapped;
23
23
  }
24
- export async function attachIncludes(model, registry, pool, rootRows, include, rootArgs, options = {}) {
24
+ export async function attachIncludes(model, registry, executor, rootRows, include, rootArgs, options = {}) {
25
25
  if (rootRows.length === 0) {
26
26
  return rootRows;
27
27
  }
@@ -30,14 +30,14 @@ export async function attachIncludes(model, registry, pool, rootRows, include, r
30
30
  plan.orderBy = rootArgs.orderBy;
31
31
  plan.take = rootArgs.take;
32
32
  plan.skip = rootArgs.skip;
33
- await loadIncludes(rootRows, plan, pool);
33
+ await loadIncludes(rootRows, plan, executor);
34
34
  return rootRows;
35
35
  }
36
- async function executeRootSelect(model, pool, args) {
36
+ async function executeRootSelect(model, executor, args) {
37
37
  const builder = new QueryBuilder(model);
38
38
  const query = builder.select(args);
39
39
  try {
40
- const result = await pool.query(query.sql, query.params);
40
+ const result = await executor.query(query.sql, query.params);
41
41
  return result.rows;
42
42
  }
43
43
  catch (error) {
@@ -4,6 +4,10 @@ export { getDatabaseUrl } from './config.js';
4
4
  export { QueryBuilder } from './query-builder.js';
5
5
  export { WhereTranslator } from './where-translator.js';
6
6
  export { createModelClient } from './model-client.js';
7
+ export type { Queryable } from './queryable.js';
8
+ export { createRawClient } from './raw.js';
9
+ export type { RawClient } from './raw.js';
10
+ export { runInTransaction } from './transaction.js';
7
11
  export { TypeGenerator, generateDbTypes } from './type-generator.js';
8
12
  export { DbClientGenerator, generateDbClientFiles } from './db-client-generator.js';
9
13
  export { buildModelMeta, buildModelMetaSnapshot, hydrateModelMeta } from './model-meta.js';
package/dist/db/index.js CHANGED
@@ -4,6 +4,8 @@ export { getDatabaseUrl } from './config.js';
4
4
  export { QueryBuilder } from './query-builder.js';
5
5
  export { WhereTranslator } from './where-translator.js';
6
6
  export { createModelClient } from './model-client.js';
7
+ export { createRawClient } from './raw.js';
8
+ export { runInTransaction } from './transaction.js';
7
9
  export { TypeGenerator, generateDbTypes } from './type-generator.js';
8
10
  export { DbClientGenerator, generateDbClientFiles } from './db-client-generator.js';
9
11
  export { buildModelMeta, buildModelMetaSnapshot, hydrateModelMeta } from './model-meta.js';
@@ -1,6 +1,6 @@
1
- import type { Pool } from 'pg';
2
1
  import type { IncludeInput, IncludeOptions } from './include/types.js';
3
2
  import type { ModelMeta } from './model-meta.js';
3
+ import type { Queryable } from './queryable.js';
4
4
  export type ModelRegistry = Map<string, ModelMeta>;
5
5
  export interface SelectArgs<TWhere, TOrderBy> {
6
6
  where?: TWhere;
@@ -35,4 +35,4 @@ export interface ModelClient<T, TCreate, TUpdate, TWhere, TOrderBy> {
35
35
  count: number;
36
36
  }>;
37
37
  }
38
- export declare function createModelClient<T, TCreate, TUpdate, TWhere, TOrderBy>(model: ModelMeta, pool: Pool, registry?: ModelRegistry): ModelClient<T, TCreate, TUpdate, TWhere, TOrderBy>;
38
+ export declare function createModelClient<T, TCreate, TUpdate, TWhere, TOrderBy>(model: ModelMeta, executor: Queryable, registry?: ModelRegistry): ModelClient<T, TCreate, TUpdate, TWhere, TOrderBy>;
@@ -2,11 +2,11 @@ import { mapPgError } from './errors.js';
2
2
  import { fetchWithIncludes } from './include/load.js';
3
3
  import { QueryBuilder } from './query-builder.js';
4
4
  import { mapRow, mapRows } from './row-mapper.js';
5
- export function createModelClient(model, pool, registry) {
5
+ export function createModelClient(model, executor, registry) {
6
6
  const builder = new QueryBuilder(model);
7
7
  async function execute(sql, params) {
8
8
  try {
9
- const result = await pool.query(sql, params);
9
+ const result = await executor.query(sql, params);
10
10
  return result.rows;
11
11
  }
12
12
  catch (error) {
@@ -16,7 +16,7 @@ export function createModelClient(model, pool, registry) {
16
16
  async function selectRows(args = {}) {
17
17
  const findArgs = toFindArgs(args);
18
18
  if (args.include && registry) {
19
- return fetchWithIncludes(model, registry, pool, {
19
+ return fetchWithIncludes(model, registry, executor, {
20
20
  ...findArgs,
21
21
  include: args.include,
22
22
  }, {
@@ -0,0 +1,5 @@
1
+ import type { QueryResult, QueryResultRow } from 'pg';
2
+ /** Minimal executor satisfied by both pg `Pool` and `PoolClient`. */
3
+ export interface Queryable {
4
+ query<T extends QueryResultRow>(sql: string, params?: unknown[]): Promise<QueryResult<T>>;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import type { QueryResultRow } from 'pg';
2
+ import type { Queryable } from './queryable.js';
3
+ export interface RawClient {
4
+ /**
5
+ * Runs an arbitrary SQL query and returns the raw result rows.
6
+ *
7
+ * Rows are returned EXACTLY as `pg` produces them: `snake_case` column names
8
+ * and driver type coercion, with no `camelCase` mapping. Values MUST be passed
9
+ * through the positional `params` array (`$1`, `$2`, …) — never interpolate
10
+ * user input into the `sql` string.
11
+ */
12
+ $queryRaw<T extends QueryResultRow = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
13
+ /**
14
+ * Runs an arbitrary SQL statement and returns the number of affected rows.
15
+ *
16
+ * Values MUST be passed through the positional `params` array (`$1`, `$2`, …)
17
+ * — never interpolate user input into the `sql` string.
18
+ */
19
+ $executeRaw(sql: string, params?: unknown[]): Promise<number>;
20
+ }
21
+ /** Builds the parameterized raw-query escape hatch bound to a single executor. */
22
+ export declare function createRawClient(executor: Queryable): RawClient;
package/dist/db/raw.js ADDED
@@ -0,0 +1,35 @@
1
+ import { mapPgError } from './errors.js';
2
+ /**
3
+ * Label used when surfacing raw-query errors. A raw query has no single owning
4
+ * model, so error mapping runs without a `column -> field` translation map.
5
+ */
6
+ const RAW_QUERY_LABEL = '$queryRaw';
7
+ const EMPTY_COLUMN_TO_FIELD = new Map();
8
+ const NO_PARAMS = [];
9
+ /** Maps a driver error to the project's typed `DatabaseError` subclasses. */
10
+ function wrapRawError(error) {
11
+ return mapPgError(error, RAW_QUERY_LABEL, EMPTY_COLUMN_TO_FIELD);
12
+ }
13
+ /** Builds the parameterized raw-query escape hatch bound to a single executor. */
14
+ export function createRawClient(executor) {
15
+ return {
16
+ async $queryRaw(sql, params = NO_PARAMS) {
17
+ try {
18
+ const result = await executor.query(sql, params);
19
+ return result.rows;
20
+ }
21
+ catch (error) {
22
+ throw wrapRawError(error);
23
+ }
24
+ },
25
+ async $executeRaw(sql, params = NO_PARAMS) {
26
+ try {
27
+ const result = await executor.query(sql, params);
28
+ return result.rowCount ?? 0;
29
+ }
30
+ catch (error) {
31
+ throw wrapRawError(error);
32
+ }
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,2 @@
1
+ import type { Pool, PoolClient } from 'pg';
2
+ export declare function runInTransaction<T>(pool: Pool, fn: (client: PoolClient) => Promise<T>): Promise<T>;
@@ -0,0 +1,24 @@
1
+ const BEGIN = 'BEGIN';
2
+ const COMMIT = 'COMMIT';
3
+ const ROLLBACK = 'ROLLBACK';
4
+ export async function runInTransaction(pool, fn) {
5
+ const client = await pool.connect();
6
+ try {
7
+ await client.query(BEGIN);
8
+ const result = await fn(client);
9
+ await client.query(COMMIT);
10
+ return result;
11
+ }
12
+ catch (error) {
13
+ try {
14
+ await client.query(ROLLBACK);
15
+ }
16
+ catch {
17
+ // ignore rollback failure; surface the original error
18
+ }
19
+ throw error;
20
+ }
21
+ finally {
22
+ client.release();
23
+ }
24
+ }
@@ -0,0 +1,3 @@
1
+ /** Demo User requires balance; register bypasses @policy and writes via db.user.create. */
2
+ declare const _default: import("hono").Hono<import("../api/types.js").AppEnv, import("hono/types").BlankSchema, "/">;
3
+ export default _default;
@@ -0,0 +1,5 @@
1
+ import { createAuthRouter } from 'schematic-pg/api/auth/routes';
2
+ /** Demo User requires balance; register bypasses @policy and writes via db.user.create. */
3
+ export default createAuthRouter({
4
+ defaultCreateFields: { balance: 0 },
5
+ });
@@ -1,2 +1,6 @@
1
- export type DbClient = Record<string, unknown>;
1
+ import type { QueryResultRow } from 'pg';
2
+ export interface DbClient extends Record<string, unknown> {
3
+ $queryRaw<T extends QueryResultRow = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
4
+ $executeRaw(sql: string, params?: unknown[]): Promise<number>;
5
+ }
2
6
  export declare function createDbClient(_pool: unknown): DbClient;
@@ -1,3 +1,10 @@
1
1
  export function createDbClient(_pool) {
2
- return {};
2
+ return {
3
+ async $queryRaw() {
4
+ return [];
5
+ },
6
+ async $executeRaw() {
7
+ return 0;
8
+ },
9
+ };
3
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schematic-pg",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
4
4
  "description": "Single-file backend framework for PostgreSQL and Node.js",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "git+https://github.com/tawsbob/postgrest.js.git"
12
+ "url": "git+https://github.com/tawsbob/schematic-pg.git"
13
13
  },
14
14
  "main": "./dist/index.js",
15
15
  "types": "./dist/index.d.ts",
@@ -34,6 +34,10 @@
34
34
  "types": "./dist/schema-dsl/index.d.ts",
35
35
  "import": "./dist/schema-dsl/index.js"
36
36
  },
37
+ "./api/hooks": {
38
+ "types": "./dist/api/hooks/index.d.ts",
39
+ "import": "./dist/api/hooks/index.js"
40
+ },
37
41
  "./api/*": {
38
42
  "types": "./dist/api/*.d.ts",
39
43
  "import": "./dist/api/*.js"
@@ -44,13 +48,14 @@
44
48
  }
45
49
  },
46
50
  "scripts": {
47
- "build": "tsc -p tsconfig.build.json",
51
+ "build": "tsc -p tsconfig.build.json && mkdir -p dist/cli/templates && cp src/cli/templates/agents.md dist/cli/templates/",
48
52
  "parse": "tsx src/schema-dsl/cli.ts",
49
53
  "tokenize": "tsx src/schema-dsl/cli.ts --tokens",
50
54
  "generate": "tsx src/cli.ts generate",
51
55
  "generate:client": "tsx src/cli.ts generate:client",
52
56
  "generate:api": "tsx src/cli.ts generate:api",
53
57
  "dev:api": "tsx src/cli.ts dev",
58
+ "start": "tsx src/cli.ts start",
54
59
  "setup:env": "test -f .env || cp .env.example .env",
55
60
  "db:ping": "tsx src/cli.ts db:ping",
56
61
  "db:bootstrap": "tsx src/cli.ts db:bootstrap",
@@ -60,7 +65,7 @@
60
65
  "build:lsp": "npm run build --workspace=@schematic-pg/schema-dsl-language-server",
61
66
  "build:extension": "npm run vscode:prepublish --workspace=schematic-pg-schema-dsl-vscode && npm run package --workspace=schematic-pg-schema-dsl-vscode",
62
67
  "test": "node --import tsx --test $(find src editors/language-server -path '*/__tests__/*.test.ts' ! -name '*.integration.test.ts')",
63
- "test:integration": "npm run setup:env && docker compose up -d --wait && npm run generate:client && npm run generate:api && JWT_SECRET=integration-test-secret node --import tsx --test --test-concurrency=1 'src/**/__tests__/**/*.integration.test.ts'",
68
+ "test:integration": "npm run setup:env && docker compose up -d --wait && npm run generate:client && npm run generate:api && JWT_SECRET=integration-test-secret AUTH_PEPPER=integration-test-pepper node --import tsx --test --test-concurrency=1 'src/**/__tests__/**/*.integration.test.ts'",
64
69
  "docker:up": "docker compose up -d",
65
70
  "docker:down": "docker compose down",
66
71
  "docker:logs": "docker compose logs -f postgres",
@@ -69,6 +74,8 @@
69
74
  "dependencies": {
70
75
  "@hono/node-server": "^2.0.6",
71
76
  "@hono/zod-validator": "^0.8.0",
77
+ "@inquirer/prompts": "^8.5.2",
78
+ "argon2": "^0.44.0",
72
79
  "hono": "^4.12.27",
73
80
  "pg": "^8.22.0",
74
81
  "tsx": "^4.19.4",