@kequtech/tenon-orm 0.4.2 → 0.4.4

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 (45) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +16 -5
  3. package/dist/check.js +4 -2
  4. package/dist/cli.js +106 -28
  5. package/dist/config/connection.d.ts +3 -0
  6. package/dist/config/connection.js +22 -0
  7. package/dist/config/init.d.ts +2 -0
  8. package/dist/config/init.js +31 -0
  9. package/dist/config/load.d.ts +7 -0
  10. package/dist/config/load.js +152 -0
  11. package/dist/config/validate.d.ts +3 -0
  12. package/dist/config/validate.js +28 -0
  13. package/dist/config.d.ts +30 -0
  14. package/dist/config.js +1 -0
  15. package/dist/connection.d.ts +8 -0
  16. package/dist/connection.js +1 -1
  17. package/dist/database.d.ts +3 -7
  18. package/dist/database.js +22 -19
  19. package/dist/describe.js +3 -1
  20. package/dist/generate.d.ts +0 -11
  21. package/dist/generate.js +1 -26
  22. package/dist/generation/output.js +2 -2
  23. package/dist/index.d.ts +3 -3
  24. package/dist/inspection/cli.d.ts +3 -4
  25. package/dist/inspection/cli.js +3 -13
  26. package/dist/inspection/connection.js +23 -19
  27. package/dist/inspection/options.d.ts +5 -7
  28. package/dist/inspection/options.js +39 -22
  29. package/dist/inspection/types.d.ts +3 -2
  30. package/dist/migrations/history.d.ts +1 -1
  31. package/dist/migrations/history.js +3 -2
  32. package/dist/types.d.ts +1 -4
  33. package/dist/validation.d.ts +2 -0
  34. package/dist/validation.js +6 -0
  35. package/docs/adoption-guide.md +9 -3
  36. package/docs/agent-guide.md +13 -16
  37. package/docs/configuration.md +211 -0
  38. package/docs/connections.md +51 -27
  39. package/docs/human-guide.md +44 -25
  40. package/docs/migrations.md +13 -6
  41. package/docs/query-checking.md +14 -11
  42. package/docs/query-inspection.md +10 -7
  43. package/docs/reference.md +72 -49
  44. package/docs/schema-types.md +35 -27
  45. package/package.json +1 -1
package/dist/database.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { createClient } from './client.js';
2
- import { createConnection, executeSql } from './connection.js';
2
+ import { createConnection, executeSql, isPool } from './connection.js';
3
+ import { configConnection } from './config/connection.js';
4
+ import { configObject } from './config/validate.js';
3
5
  import { withTransaction } from './transaction.js';
4
6
  import { plainObject } from './validation.js';
5
- import { generateTypes } from './generate.js';
6
7
  function closeTimeout(options) {
7
8
  const settings = plainObject(options === undefined ? {} : options, 'Close options');
8
9
  for (const key of Object.keys(settings)) {
@@ -18,38 +19,40 @@ function closeTimeout(options) {
18
19
  }
19
20
  return timeoutMs;
20
21
  }
21
- export function createTenon(options) {
22
- for (const key of Object.keys(plainObject(options, 'Tenon options'))) {
23
- if (key !== 'connection' && key !== 'onQuery') {
24
- throw new Error(`Unknown Tenon option: ${key}`);
25
- }
26
- }
27
- if (options.onQuery !== undefined && typeof options.onQuery !== 'function') {
28
- throw new Error('onQuery must be a function.');
29
- }
30
- const { pool, close } = createConnection(options.connection);
22
+ export function createTenon(config) {
23
+ configObject(config);
24
+ const onQuery = config.onQuery;
25
+ let connection;
31
26
  let closing;
32
27
  function checkOpen() {
33
28
  if (closing) {
34
29
  throw new Error('This database instance is closed.');
35
30
  }
36
31
  }
32
+ function currentConnection() {
33
+ if (!connection) {
34
+ checkOpen();
35
+ const value = config.connection;
36
+ connection = createConnection(isPool(value) ? value : configConnection(value));
37
+ }
38
+ return connection;
39
+ }
37
40
  const query = (text, input, readResult, guarded) => {
38
41
  checkOpen();
42
+ const { pool } = currentConnection();
39
43
  if (guarded)
40
44
  return withTransaction(pool, (query) => query(text, input, readResult, false));
41
45
  return executeSql(pool, text, input).then(readResult);
42
46
  };
43
47
  return {
44
- ...createClient(query, options.onQuery),
45
- native: pool,
46
- async generateTypes(options) {
47
- checkOpen();
48
- await generateTypes(pool, options);
48
+ ...createClient(query, onQuery),
49
+ get native() {
50
+ return currentConnection().pool;
49
51
  },
50
52
  async transaction(callback) {
51
53
  checkOpen();
52
- return withTransaction(pool, (query) => callback(createClient(query, options.onQuery)));
54
+ const { pool } = currentConnection();
55
+ return withTransaction(pool, (query) => callback(createClient(query, onQuery)));
53
56
  },
54
57
  close(options) {
55
58
  if (closing)
@@ -57,7 +60,7 @@ export function createTenon(options) {
57
60
  try {
58
61
  const timeoutMs = closeTimeout(options);
59
62
  // Cache before invoking driver callbacks, so reentrant/concurrent callers share shutdown.
60
- closing = Promise.resolve().then(() => close(timeoutMs));
63
+ closing = Promise.resolve().then(() => connection?.close(timeoutMs));
61
64
  return closing;
62
65
  }
63
66
  catch (error) {
package/dist/describe.js CHANGED
@@ -23,6 +23,8 @@ export async function describeQuery(options) {
23
23
  ? { parameterTypes: [...options.query.parameterTypes] }
24
24
  : {}),
25
25
  };
26
- const codecs = options.codecs ?? defaultCodecs;
26
+ const codecs = options.codecs ??
27
+ (typeof options.connection === 'object' ? options.connection?.codecs : undefined) ??
28
+ defaultCodecs;
27
29
  return withInspectionConnection(options, async (client, context) => queryDescription(await readStatementMetadata(client, query), context, codecs));
28
30
  }
@@ -4,16 +4,5 @@ export interface GenerateOptions {
4
4
  readonly namespace?: string;
5
5
  readonly onWarning?: (message: string) => void | Promise<void>;
6
6
  }
7
- export interface GenerateTypesOptions {
8
- /** Output path, resolved from the working directory. */
9
- readonly out: string;
10
- /** Schema to inspect; defaults to public. */
11
- readonly namespace?: string;
12
- /** Reject missing or stale output without writing. Defaults to false. */
13
- readonly check?: boolean;
14
- readonly onWarning?: (message: string) => void | Promise<void>;
15
- }
16
- /** Generate with the owning database's connection and preserve exact file freshness checks. */
17
- export declare function generateTypes(connection: Connection, options: GenerateTypesOptions): Promise<void>;
18
7
  /** Read one schema snapshot with default catalog codecs and the selected application read mappings. */
19
8
  export declare function generateDefinitions(options: GenerateOptions): Promise<string>;
package/dist/generate.js CHANGED
@@ -4,32 +4,7 @@ import { renderDatabase } from './generation/render.js';
4
4
  import { domainSummaries, schemaSummary, tableSummary } from './generation/schema-summary.js';
5
5
  import { withMigrationLock } from './migrations/connection.js';
6
6
  import { migrationTable } from './migrations/history.js';
7
- import { quote, validateName } from './sql.js';
8
- import { plainObject } from './validation.js';
9
- import { writeDefinitions } from './generation/output.js';
10
- /** Generate with the owning database's connection and preserve exact file freshness checks. */
11
- export async function generateTypes(connection, options) {
12
- const config = plainObject(options, 'Generation options');
13
- for (const key of Object.keys(config)) {
14
- if (!['out', 'namespace', 'check', 'onWarning'].includes(key)) {
15
- throw new Error(`Unknown generation option: ${key}`);
16
- }
17
- }
18
- if (typeof config.out !== 'string' || !config.out.trim() || config.out.includes('\0')) {
19
- throw new Error('Generation out must be a nonempty file path.');
20
- }
21
- if (config.check !== undefined && typeof config.check !== 'boolean') {
22
- throw new Error('Generation check must be a boolean.');
23
- }
24
- if (options.namespace !== undefined)
25
- validateName(options.namespace);
26
- const source = await generateDefinitions({
27
- connection,
28
- ...(options.namespace !== undefined && { namespace: options.namespace }),
29
- ...(options.onWarning !== undefined && { onWarning: options.onWarning }),
30
- });
31
- await writeDefinitions(source, config.out, config.check === true);
32
- }
7
+ import { quote } from './sql.js';
33
8
  /** Read one schema snapshot with default catalog codecs and the selected application read mappings. */
34
9
  export async function generateDefinitions(options) {
35
10
  if (options.onWarning !== undefined && typeof options.onWarning !== 'function') {
@@ -14,10 +14,10 @@ export async function writeDefinitions(source, filename, check) {
14
14
  if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) {
15
15
  throw error;
16
16
  }
17
- throw new Error(`Generated definitions file is missing: ${output}\nRun db.generateTypes with the same options and check: false, then commit the result.`);
17
+ throw new Error(`Generated definitions file is missing: ${output}\nRun tenon generate with the same options, omitting --check, then commit the result.`);
18
18
  }
19
19
  if (existing !== source) {
20
- throw new Error(`Generated definitions differ: ${output}\n${definitionDiff(existing, source)}\nRun db.generateTypes with the same options and check: false, then commit the result.`);
20
+ throw new Error(`Generated definitions differ: ${output}\n${definitionDiff(existing, source)}\nRun tenon generate with the same options, omitting --check, then commit the result.`);
21
21
  }
22
22
  return;
23
23
  }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  export { builtin, createTemporalCodecs, defaultCodecs, json, param } from '@kequtech/pgclear';
2
2
  export type { Codec, CodecRegistry, Parameter, PgArray, TemporalConstructors, TemporalInput, } from '@kequtech/pgclear';
3
- export type { Connection, ConnectionOptions, QueryResult } from './connection.js';
3
+ export type { Connection, ConnectionConfig, ConnectionOptions, QueryResult } from './connection.js';
4
4
  export type { AffectedRowsConstraint } from './affected-rows.js';
5
5
  export { createTenon } from './database.js';
6
- export type { TenonOptions } from './database.js';
6
+ export type { TenonConfig } from './config.js';
7
7
  export { AffectedRowsError, CardinalityError } from './errors.js';
8
8
  export type { QueryOptions, QueryOutput, QueryReturnMode } from './query-options.js';
9
9
  export { identifier, sql, unsafeSql } from './query.js';
@@ -14,7 +14,7 @@ export type { DescribeOptions, DescribedColumn, DescribedType, DescriptionIssue,
14
14
  export { adoptBaseline, captureBaseline } from './baseline.js';
15
15
  export type { CaptureBaselineOptions, CapturedBaseline } from './baseline.js';
16
16
  export { generateDefinitions } from './generate.js';
17
- export type { GenerateOptions, GenerateTypesOptions } from './generate.js';
17
+ export type { GenerateOptions } from './generate.js';
18
18
  export { migrate, migrationStatus, rollback } from './migrations.js';
19
19
  export type { MigrationStatus } from './migrations.js';
20
20
  export type { DatabaseToolOptions } from './migrations/connection.js';
@@ -21,9 +21,9 @@ export declare const describeFlags: {
21
21
  /** Translate CLI flags; validation and defaults belong to the public API. */
22
22
  export declare function inspectionSettings(values: Partial<Record<keyof typeof describeFlags, string | undefined>>): {
23
23
  session: {
24
- role: string | undefined;
25
- searchPath: string | undefined;
26
- timeZone: string | undefined;
24
+ timeZone?: string;
25
+ searchPath?: string;
26
+ role?: string;
27
27
  };
28
28
  timeouts: {
29
29
  lockMs?: number;
@@ -31,4 +31,3 @@ export declare function inspectionSettings(values: Partial<Record<keyof typeof d
31
31
  connectMs?: number;
32
32
  };
33
33
  };
34
- export declare function runDescribe(filename: string, connection: string, values: Partial<Record<keyof typeof describeFlags, string | undefined>>): Promise<void>;
@@ -1,5 +1,3 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { describeQuery } from '../describe.js';
3
1
  export const describeFlags = {
4
2
  role: { type: 'string' },
5
3
  'search-path': { type: 'string' },
@@ -12,9 +10,9 @@ export const describeFlags = {
12
10
  export function inspectionSettings(values) {
13
11
  return {
14
12
  session: {
15
- role: values.role,
16
- searchPath: values['search-path'],
17
- timeZone: values['time-zone'],
13
+ ...(values.role !== undefined && { role: values.role }),
14
+ ...(values['search-path'] !== undefined && { searchPath: values['search-path'] }),
15
+ ...(values['time-zone'] !== undefined && { timeZone: values['time-zone'] }),
18
16
  },
19
17
  timeouts: {
20
18
  ...(values['connect-timeout-ms'] !== undefined
@@ -29,11 +27,3 @@ export function inspectionSettings(values) {
29
27
  },
30
28
  };
31
29
  }
32
- export async function runDescribe(filename, connection, values) {
33
- const result = await describeQuery({
34
- connection,
35
- query: await readFile(filename, 'utf8'),
36
- ...inspectionSettings(values),
37
- });
38
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
39
- }
@@ -1,27 +1,30 @@
1
- import { connect } from '@kequtech/pgclear';
1
+ import { createPool, defaultCodecs } from '@kequtech/pgclear';
2
2
  import { inspectionOptions } from './options.js';
3
3
  export async function withInspectionConnection(options, work) {
4
- const { url, limits, session } = inspectionOptions(options);
5
- const connection = await connect({
6
- connectionString: url.toString(),
4
+ const { connection, limits, session } = inspectionOptions(options);
5
+ const pool = createPool({
6
+ ...connection,
7
+ codecs: defaultCodecs,
8
+ maxConnections: 1,
7
9
  connectTimeoutMs: limits.connectMs,
8
10
  startupParameters: {
11
+ ...Object.fromEntries(Object.entries(connection.startupParameters ?? {}).filter(([name]) => !['statement_timeout', 'lock_timeout'].includes(name.toLowerCase()))),
9
12
  statement_timeout: String(limits.statementMs),
10
13
  lock_timeout: String(limits.lockMs),
11
14
  },
12
15
  });
13
- const client = connection;
14
16
  try {
15
- for (const [key, name] of [
16
- ['role', 'role'],
17
- ['searchPath', 'search_path'],
18
- ['timeZone', 'TimeZone'],
19
- ]) {
20
- const value = session[key];
21
- if (value !== undefined)
22
- await client.queryClassic('SELECT pg_catalog.set_config($1, $2, false)', [name, value]);
23
- }
24
- const { rows: [context], } = await client.queryClassic(`
17
+ return await pool.withSession(async (client) => {
18
+ for (const [key, name] of [
19
+ ['role', 'role'],
20
+ ['searchPath', 'search_path'],
21
+ ['timeZone', 'TimeZone'],
22
+ ]) {
23
+ const value = session[key];
24
+ if (value !== undefined)
25
+ await client.queryClassic('SELECT pg_catalog.set_config($1, $2, false)', [name, value]);
26
+ }
27
+ const { rows: [context], } = await client.queryClassic(`
25
28
  SELECT pg_catalog.current_database() AS database,
26
29
  session_user AS "sessionUser", current_user AS "currentUser",
27
30
  pg_catalog.current_setting('search_path') AS "searchPath",
@@ -32,11 +35,12 @@ export async function withInspectionConnection(options, work) {
32
35
  (SELECT setting::integer FROM pg_catalog.pg_settings
33
36
  WHERE name = 'lock_timeout') AS "lockTimeoutMs"
34
37
  `, []);
35
- if (!context)
36
- throw new Error('Inspection context was not returned.');
37
- return await work(client, { mode: 'separate-session', readMappings: 'pgclear', ...context });
38
+ if (!context)
39
+ throw new Error('Inspection context was not returned.');
40
+ return await work(client, { mode: 'separate-session', readMappings: 'pgclear', ...context });
41
+ });
38
42
  }
39
43
  finally {
40
- await connection.close();
44
+ await pool.close({ timeoutMs: limits.connectMs });
41
45
  }
42
46
  }
@@ -1,15 +1,13 @@
1
- import type { DescribeOptions } from './types.js';
1
+ import type { DescribeOptions, InspectionSession, InspectionTimeouts } from './types.js';
2
+ export declare function sessionOptions(value: unknown): InspectionSession;
3
+ export declare function timeoutOptions(value: unknown): InspectionTimeouts;
2
4
  /** Validate and capture caller-owned settings before opening a connection. */
3
5
  export declare function inspectionOptions(options: Pick<DescribeOptions, 'connection' | 'session' | 'timeouts'>): {
4
- url: URL;
6
+ connection: import("@kequtech/pgclear").PoolOptions;
5
7
  limits: {
6
8
  connectMs: number;
7
9
  statementMs: number;
8
10
  lockMs: number;
9
11
  };
10
- session: {
11
- role?: string | undefined;
12
- searchPath?: string | undefined;
13
- timeZone?: string | undefined;
14
- };
12
+ session: InspectionSession;
15
13
  };
@@ -1,11 +1,9 @@
1
1
  import { plainObject } from '../validation.js';
2
- /** Validate and capture caller-owned settings before opening a connection. */
3
- export function inspectionOptions(options) {
4
- if (typeof options.connection !== 'string' || !options.connection.trim()) {
5
- throw new Error('Describe requires an explicit nonempty connection string.');
6
- }
7
- plainObject(options.session ?? {}, 'Inspection session');
8
- const session = { ...options.session };
2
+ import { configConnection } from '../config/connection.js';
3
+ const defaults = { connectMs: 3000, statementMs: 5000, lockMs: 1000 };
4
+ export function sessionOptions(value) {
5
+ const input = plainObject(value, 'Inspection session');
6
+ const session = { ...input };
9
7
  for (const [key, value] of Object.entries(session)) {
10
8
  if (!['role', 'searchPath', 'timeZone'].includes(key)) {
11
9
  throw new Error(`Unknown inspection session option: ${key}`);
@@ -16,9 +14,11 @@ export function inspectionOptions(options) {
16
14
  throw new Error(`Inspection session ${key} requires a nonempty string without NUL.`);
17
15
  }
18
16
  }
19
- plainObject(options.timeouts ?? {}, 'Inspection timeouts');
20
- const timeouts = { ...options.timeouts };
21
- const defaults = { connectMs: 3000, statementMs: 5000, lockMs: 1000 };
17
+ return session;
18
+ }
19
+ export function timeoutOptions(value) {
20
+ const input = plainObject(value, 'Inspection timeouts');
21
+ const timeouts = { ...input };
22
22
  for (const [key, value] of Object.entries(timeouts)) {
23
23
  if (!Object.hasOwn(defaults, key))
24
24
  throw new Error(`Unknown inspection timeout: ${key}`);
@@ -28,24 +28,41 @@ export function inspectionOptions(options) {
28
28
  throw new Error(`Inspection timeout ${key} must be a positive integer in milliseconds.`);
29
29
  }
30
30
  }
31
+ return timeouts;
32
+ }
33
+ /** Validate and capture caller-owned settings before opening a connection. */
34
+ export function inspectionOptions(options) {
35
+ const selected = configConnection(options.connection);
36
+ let connection = typeof selected === 'string' ? { connectionString: selected } : { ...selected };
37
+ if (connection.startupParameters !== undefined) {
38
+ plainObject(connection.startupParameters, 'Startup parameters');
39
+ }
40
+ const session = sessionOptions(options.session ?? {});
41
+ const timeouts = timeoutOptions(options.timeouts ?? {});
31
42
  const limits = {
32
43
  connectMs: timeouts.connectMs ?? defaults.connectMs,
33
44
  statementMs: timeouts.statementMs ?? defaults.statementMs,
34
45
  lockMs: timeouts.lockMs ?? defaults.lockMs,
35
46
  };
36
47
  // URL startup options must not disable the inspection's server-side bounds.
37
- let url;
38
- try {
39
- url = new URL(options.connection);
40
- }
41
- catch {
42
- throw new Error('Describe requires a PostgreSQL connection URI.');
43
- }
44
- if (!['postgres:', 'postgresql:'].includes(url.protocol)) {
45
- throw new Error('Describe requires a PostgreSQL connection URI.');
48
+ if (connection.connectionString !== undefined) {
49
+ let url;
50
+ try {
51
+ url = new URL(connection.connectionString);
52
+ }
53
+ catch {
54
+ throw new Error('Describe requires a PostgreSQL connection URI.');
55
+ }
56
+ if (!['postgres:', 'postgresql:'].includes(url.protocol)) {
57
+ throw new Error('Describe requires a PostgreSQL connection URI.');
58
+ }
59
+ if (!url.searchParams.has('application_name') && connection.applicationName === undefined) {
60
+ url.searchParams.set('application_name', 'tenon-describe');
61
+ }
62
+ connection = { ...connection, connectionString: url.toString() };
46
63
  }
47
- if (!url.searchParams.has('application_name')) {
48
- url.searchParams.set('application_name', 'tenon-describe');
64
+ else if (connection.applicationName === undefined) {
65
+ connection = { ...connection, applicationName: 'tenon-describe' };
49
66
  }
50
- return { url, limits, session };
67
+ return { connection, limits, session };
51
68
  }
@@ -1,4 +1,5 @@
1
1
  import type { CodecRegistry, DescribeStatement } from '@kequtech/pgclear';
2
+ import type { ConnectionConfig } from '../connection.js';
2
3
  export interface InspectionSession {
3
4
  readonly role?: string | undefined;
4
5
  /** PostgreSQL's search_path setting, as used in the application's connection options. */
@@ -23,8 +24,8 @@ export interface InspectionContext {
23
24
  readonly readMappings: 'pgclear';
24
25
  }
25
26
  export interface DescribeOptions {
26
- /** PostgreSQL connection URI. Inspection owns a separate, short-lived connection. */
27
- readonly connection: string;
27
+ /** URI or pool configuration values. Inspection owns a separate, short-lived connection. */
28
+ readonly connection: ConnectionConfig | undefined;
28
29
  /** Finished SQL; query values are neither bound nor executed. */
29
30
  readonly query: string | DescribeStatement;
30
31
  /** Use the same registry as execution; custom decoder types can remain unresolved. */
@@ -1,4 +1,4 @@
1
- import type { Session } from '@kequtech/pgclear';
1
+ import { type Session } from '@kequtech/pgclear';
2
2
  import type { SqlMigration } from './files.js';
3
3
  export declare const migrationTable = "__tenon_migrations";
4
4
  interface AppliedMigration {
@@ -1,11 +1,12 @@
1
+ import { defaultCodecs } from '@kequtech/pgclear';
1
2
  import { qualified } from '../sql.js';
2
3
  export const migrationTable = '__tenon_migrations';
3
4
  export async function migrationState(client, namespace, migrations) {
4
- const exists = await client.queryClassic('SELECT to_regclass($1)::text AS name', [qualified(namespace, migrationTable)]);
5
+ const exists = await client.queryClassic('SELECT pg_catalog.to_regclass($1)::text AS name', [qualified(namespace, migrationTable)], { codecs: defaultCodecs });
5
6
  if (exists.rows[0].name === null) {
6
7
  return [];
7
8
  }
8
- const { rows: applied } = await client.queryClassic(`SELECT id, name, checksum FROM ${qualified(namespace, migrationTable)} ORDER BY id::numeric`, []);
9
+ const { rows: applied } = await client.queryClassic(`SELECT id, name, checksum FROM ${qualified(namespace, migrationTable)} ORDER BY id::numeric`, [], { codecs: defaultCodecs });
9
10
  for (const [i, record] of applied.entries()) {
10
11
  const file = migrations[i];
11
12
  if (!file ||
package/dist/types.d.ts CHANGED
@@ -2,7 +2,6 @@ import type { Pool } from '@kequtech/pgclear';
2
2
  import type { QueryResult } from './connection.js';
3
3
  import type { QueryOptions, QueryOutput, QueryReturnMode } from './query-options.js';
4
4
  import type { SqlQuery } from './query.js';
5
- import type { GenerateTypesOptions } from './generate.js';
6
5
  export interface QueryClient {
7
6
  query<Row extends object>(query: SqlQuery<Row>, options?: QueryOptions & {
8
7
  readonly returns?: 'native';
@@ -18,10 +17,8 @@ export interface CloseOptions {
18
17
  readonly timeoutMs?: number | undefined;
19
18
  }
20
19
  export type Database = Transaction & {
21
- /** The underlying pool-level driver client; native calls bypass Tenon's query and transaction guards. */
20
+ /** The underlying pool; access initializes shared config. Native calls bypass Tenon's guards. */
22
21
  readonly native: Pool;
23
- /** Write or check schema declarations using this instance's configured codecs. */
24
- generateTypes(options: GenerateTypesOptions): Promise<void>;
25
22
  /** Await all work inside the callback. Commit on success; rollback on an escaping error. */
26
23
  transaction<T>(callback: (tx: Transaction) => Promise<T>): Promise<T>;
27
24
  close(options?: CloseOptions): Promise<void>;
@@ -1 +1,3 @@
1
1
  export declare function plainObject(value: unknown, name: string): Record<string, unknown>;
2
+ /** Validate before resolving paths so an empty override cannot become the working directory. */
3
+ export declare function validatePath(value: unknown, label: string): asserts value is string;
@@ -8,3 +8,9 @@ export function plainObject(value, name) {
8
8
  }
9
9
  return value;
10
10
  }
11
+ /** Validate before resolving paths so an empty override cannot become the working directory. */
12
+ export function validatePath(value, label) {
13
+ if (typeof value !== 'string' || !value.trim() || value.includes('\0')) {
14
+ throw new Error(`${label} must be a nonempty path without NUL.`);
15
+ }
16
+ }
@@ -15,10 +15,12 @@ Before replacing an ORM, identify behavior it currently supplies: IDs/defaults,
15
15
  validation, hooks, serialization, relationship loading, and transaction boundaries. Preserve needed
16
16
  behavior in application code, constraints, or SQL. Tenon has no model lifecycle or implicit loading.
17
17
 
18
+ Use [shared configuration](configuration.md) to reuse connection settings and codecs across runtime,
19
+ generation, checking, inspection, and migrations. Keep credentials supplied by the deployment.
20
+
18
21
  ## Replace queries
19
22
 
20
- 1. Optionally generate persistence types with
21
- `await db.generateTypes({ out: 'src/generated-schema.d.ts', namespace: 'public' })`.
23
+ 1. Optionally run `tenon generate` with the application's shared configuration.
22
24
  Use the application's schema and codec registry; no Tenon migration history is required.
23
25
  2. Replace one workflow at a time with parameterized SQL in ordinary functions. Accept
24
26
  `QueryClient` when callers may use either `db` or `tx`. Preserve selected columns, ordering,
@@ -48,9 +50,13 @@ reference database, preferably a disposable replay of the old migrations.
48
50
  For an application owning `app` in a shared database:
49
51
 
50
52
  ```sh
51
- npx tenon baseline capture --directory migrations --namespace app --schema app
53
+ npx tenon baseline capture --directory migrations --namespace app --schema app --connection "$DATABASE_URL"
52
54
  ```
53
55
 
56
+ This example supplies the native libpq URI for the same database through `DATABASE_URL`.
57
+ The native connection can instead be stored in `baseline.connection`, and the captured schemas in
58
+ `baseline.schemas`. A string main connection can be used directly. Preserve required native authentication and TLS settings.
59
+
54
60
  The directory must contain no SQL files. Capture uses native
55
61
  `pg_dump --schema-only --no-owner --no-privileges` and writes `0001_baseline.up.sql` without a down
56
62
  file. Install compatible PostgreSQL client tools on PATH, preferably matching the server's major
@@ -5,8 +5,8 @@ Use this as a short workflow for changing an existing Tenon application. For fir
5
5
 
6
6
  ## Read the project first
7
7
 
8
- Inspect the database module, nearby query functions, migration runner, generated types, and check
9
- commands. Reuse the application's package manager, connection configuration, namespace, and
8
+ Inspect `tenon.config.ts` (or its JavaScript equivalent), the database module, nearby query
9
+ functions, migration runner, generated types, and check commands. Reuse the application's package manager, connection configuration, namespace, and
10
10
  conventions. Read the docs shipped with the installed Tenon version.
11
11
 
12
12
  For an upgrade, find the previous resolved version in the lockfile and read the
@@ -52,8 +52,9 @@ Plain bigint, valid Date, and supported Temporal inputs are accepted; inputs do
52
52
  decoders. Plain arrays/objects require `json`, `param`, or encoded text. Direct undefined rejects.
53
53
  JSON encoding still rejects bigint. Import parameter/codec helpers from Tenon.
54
54
 
55
- Use the application's registry for queries and generation, and pass it explicitly to programmatic
56
- inspection/checking. Retain application validation, authorization, domain conversion, and retry
55
+ Keep the registry in shared `connection.codecs` so queries and CLI tools agree. Programmatic
56
+ inspection/checking inherit codecs from connection-option values; pass the registry explicitly
57
+ when using a URI. Retain application validation, authorization, domain conversion, and retry
57
58
  policy. See [schema types and values](schema-types.md).
58
59
 
59
60
  ## Change data deliberately
@@ -83,16 +84,11 @@ files or omit an irreversible down file, and apply it. Never change applied SQL;
83
84
  both directions. Each file commits separately. Use direct or session pooling for tooling advisory
84
85
  locks. See [migrations](migrations.md).
85
86
 
86
- Generate with the configured database instance, after migrations commit:
87
-
88
- ```ts
89
- await db.generateTypes({ out: 'src/generated-schema.d.ts', namespace: 'public' });
90
- ```
91
-
92
- Use the project's [generation script](human-guide.md#generate-types), review the diff, and commit
93
- migrations with exact generated output. Keep handwritten aliases separate and generated files out
94
- of formatting. In CI, migrate the test database and use `check: true` without first regenerating.
95
- Production consumes committed declarations.
87
+ After migrations commit, run `tenon generate` with the project's
88
+ [shared configuration](configuration.md). Review the diff and commit migrations with exact
89
+ generated output. Keep handwritten aliases separate and generated files out of formatting. In CI,
90
+ migrate the test database and run `tenon generate --check` without first regenerating. Production
91
+ consumes committed declarations.
96
92
 
97
93
  ## Verify the changed workflow
98
94
 
@@ -107,8 +103,9 @@ intentional omission behavior and test it. Narrow to required fields only when a
107
103
  establishes that contract; do not cast merely to silence a diagnostic. See
108
104
  [responding to findings](query-checking.md#responding-to-findings).
109
105
 
110
- Match the runtime role, search path, timezone, and codecs. Describe uses a separate session and
111
- cannot establish result nullability, JSON members, cardinality, or execution permissions. Native
106
+ Share runtime connection/codecs through `tenon.config.ts`; CLI generation, checking, inspection,
107
+ and migrations read it. Select a named export with `--database`, or another file with `--config`.
108
+ Match runtime role, search path, timezone, and codecs. Describe uses a separate session and cannot establish result nullability, JSON members, cardinality, or execution permissions. Native
112
109
  calls bypass Tenon's guards/observations and need explicit resource ownership; see
113
110
  [connections and driver access](connections.md). For common failures, use
114
111
  [troubleshooting](reference.md#troubleshooting).