pgserve 2.6.1 → 2.6.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.
package/src/postgres.js CHANGED
@@ -1049,8 +1049,23 @@ export class PostgresManager extends EventEmitter {
1049
1049
  if (!expected) {
1050
1050
  this.socketDir = null;
1051
1051
  this.databaseDir = null;
1052
+ // B5 (v2.6.3): include the captured postgres stderr/stdout tail
1053
+ // in the WARN so operators tailing pm2 logs see the actual cause
1054
+ // (e.g. "FATAL: could not bind IPv4 address ... Address already
1055
+ // in use", "FATAL: data directory ... has wrong ownership", etc.)
1056
+ // instead of just "subprocess exited unexpectedly". The tail is
1057
+ // capped at 4 KB so a runaway log spam can't bloat the WARN
1058
+ // payload; the bottom of startupOutput is where the fatal
1059
+ // diagnostic typically lands. Empty string when postgres exited
1060
+ // before producing any output (rare; preserved as 'no postgres
1061
+ // output captured' so the field stays present in structured
1062
+ // logs).
1063
+ const STDERR_TAIL_BUDGET = 4096;
1064
+ const tail = startupOutput.length > STDERR_TAIL_BUDGET
1065
+ ? startupOutput.slice(-STDERR_TAIL_BUDGET)
1066
+ : startupOutput;
1052
1067
  this.logger?.warn(
1053
- { code },
1068
+ { code, postgresStderrTail: tail.trim() || 'no postgres output captured' },
1054
1069
  'PostgreSQL subprocess exited unexpectedly — socketDir/databaseDir reset'
1055
1070
  );
1056
1071
  }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * `autopg_meta` table bootstrap.
3
+ *
4
+ * pgserve singleton (v2.6) — `autopg-distribution-cutover-finalize`
5
+ * wish, Group 3 (`pgserve create-app` + manifest LOCK 1).
6
+ *
7
+ * Source-of-truth split (per wish Decision #2 / G3 deliverable 4):
8
+ *
9
+ * `autopg_meta` is the single source of truth for "which apps are
10
+ * registered with this pgserve instance + what cosign trust roots are
11
+ * locked at create-app time." The per-consumer manifest file at
12
+ * `~/.autopg/<sanitized-slug>/manifest.json` (and the sibling
13
+ * `admin.json`) are derived caches; on divergence, the table wins.
14
+ *
15
+ * The `--fix` mutation modes that would regenerate the cache files
16
+ * from the table are NOT implemented in v2.4 read-only V1
17
+ * (src/commands/doctor.js:440-442 prints "--fix tiered modes are not
18
+ * implemented in v2.4"). Until those land, the cache-recovery story is
19
+ * manual: operator deletes the per-consumer dir + re-runs
20
+ * `pgserve create-app <slug>`. The verb is idempotent and preserves
21
+ * the locked_roots already on the row (idempotent re-run touches
22
+ * `last_updated` ONLY).
23
+ *
24
+ * Why a separate table from `pgserve_meta`: different lifecycle.
25
+ * `pgserve_meta` is per-database (provision/gc cohort, fingerprint as
26
+ * PK). `autopg_meta` is per-consumer-app (slug as PK), and an app can
27
+ * exist before any of its DBs do. Splitting the tables keeps each
28
+ * bootstrap genuinely additive + lets the wish Group 4/5 work
29
+ * (per-consumer doctor surface) reach for autopg_meta without
30
+ * crossing into pgserve_meta's invariants.
31
+ *
32
+ * Idempotency: every statement uses `IF NOT EXISTS` (table, indexes).
33
+ * Re-running on an already-bootstrapped database is a no-op.
34
+ */
35
+
36
+ export const AUTOPG_META_TABLE = 'autopg_meta';
37
+
38
+ /**
39
+ * Base columns owned by this module.
40
+ *
41
+ * - slug: PRIMARY KEY — the sanitized consumer slug
42
+ * (sanitizeSlug from src/provision/db-naming.js)
43
+ * - manifest_path: NOT NULL — absolute path to the cache manifest
44
+ * file at ~/.autopg/<slug>/manifest.json
45
+ * - locked_roots: NOT NULL — JSONB array shaped like
46
+ * TRUSTED_IDENTITIES entries, frozen-at-create
47
+ * - created_at: NOT NULL DEFAULT now() — set on insert
48
+ * - last_updated: NOT NULL DEFAULT now() — touched by every
49
+ * create-app re-run; locked_roots stays untouched
50
+ */
51
+ export const AUTOPG_META_COLUMNS = Object.freeze([
52
+ 'slug',
53
+ 'manifest_path',
54
+ 'locked_roots',
55
+ 'created_at',
56
+ 'last_updated',
57
+ ]);
58
+
59
+ // Schema-qualified name. The doctor / verifier read paths probe
60
+ // `to_regclass('public.autopg_meta')`; an unqualified CREATE TABLE
61
+ // could land in a non-public schema if a non-default search_path is
62
+ // configured on the active role, leaving subsequent reads unable to
63
+ // find it. Match the qualification convention pgserve_meta uses.
64
+ const QUALIFIED = `public.${AUTOPG_META_TABLE}`;
65
+
66
+ /**
67
+ * Idempotent statements that CREATE the table + supporting indexes.
68
+ * Returned as an array so callers can run each one individually for
69
+ * clear error reporting (mirrors src/schema/pgserve-meta.js shape).
70
+ */
71
+ export function getBootstrapStatements() {
72
+ return [
73
+ [
74
+ `CREATE TABLE IF NOT EXISTS ${QUALIFIED} (`,
75
+ ' slug TEXT PRIMARY KEY,',
76
+ ' manifest_path TEXT NOT NULL,',
77
+ ' locked_roots JSONB NOT NULL,',
78
+ ' created_at TIMESTAMPTZ NOT NULL DEFAULT now(),',
79
+ ' last_updated TIMESTAMPTZ NOT NULL DEFAULT now()',
80
+ ')',
81
+ ].join('\n'),
82
+ `CREATE INDEX IF NOT EXISTS ${AUTOPG_META_TABLE}_last_updated_idx ON ${QUALIFIED} (last_updated)`,
83
+ ];
84
+ }
85
+
86
+ /**
87
+ * Single SQL string variant — convenient for embedding the bootstrap in
88
+ * a transaction or pg-init script.
89
+ */
90
+ export function getBootstrapSQL() {
91
+ return `${getBootstrapStatements().join(';\n\n')};\n`;
92
+ }
93
+
94
+ /**
95
+ * Apply the bootstrap via a node-postgres-compatible client. The client
96
+ * must expose an async `query(sql)` method (matches both `pg.Client` and
97
+ * `pg.PoolClient`). Returns the list of statements executed.
98
+ *
99
+ * Statements run sequentially so a failure on the index half doesn't
100
+ * masquerade as success after the table half ran.
101
+ */
102
+ export async function bootstrapAutopgMeta(client) {
103
+ if (!client || typeof client.query !== 'function') {
104
+ throw new TypeError('bootstrapAutopgMeta: client must expose an async query() method');
105
+ }
106
+ const statements = getBootstrapStatements();
107
+ for (const sql of statements) {
108
+ await client.query(sql);
109
+ }
110
+ return statements;
111
+ }
112
+
113
+ /**
114
+ * Predicate the doctor / verify read paths can call before deciding
115
+ * whether to query the table. Callers pass the result of
116
+ * `SELECT to_regclass('public.autopg_meta') IS NOT NULL`.
117
+ */
118
+ export function tableExistsFromRegclass(toRegclassResult) {
119
+ return toRegclassResult === true;
120
+ }