@rebasepro/server-postgres 0.25.0 → 0.26.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.
- package/dist/{BranchService-iXXZeVYD.js → BranchService-CWBnDhcv.js} +88 -12
- package/dist/BranchService-CWBnDhcv.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +13 -0
- package/dist/PostgresBootstrapper.d.ts +7 -0
- package/dist/auth/services.d.ts +8 -1
- package/dist/auth/verify-auth-tables.d.ts +21 -0
- package/dist/backup/backup-cli.d.ts +40 -3
- package/dist/backup/backup-service.d.ts +11 -0
- package/dist/{backup-service-Bb39plam.js → backup-cli-PoRG-zEq.js} +777 -10
- package/dist/backup-cli-PoRG-zEq.js.map +1 -0
- package/dist/{boot-ddl-s_3Hkz94.js → boot-ddl-EsgqDkx0.js} +3 -3
- package/dist/{boot-ddl-s_3Hkz94.js.map → boot-ddl-EsgqDkx0.js.map} +1 -1
- package/dist/branch-argv.d.ts +15 -0
- package/dist/branch-diff-CxE4VlxT.js +1396 -0
- package/dist/branch-diff-CxE4VlxT.js.map +1 -0
- package/dist/branch-diff-report-DpvVf7hR.js +48 -0
- package/dist/branch-diff-report-DpvVf7hR.js.map +1 -0
- package/dist/branching/branch-diff-report.d.ts +7 -0
- package/dist/branching/branch-diff.d.ts +115 -0
- package/dist/{cli-console-DQ_K-sdM.js → cli-console-f0k6_TsJ.js} +41 -9
- package/dist/cli-console-f0k6_TsJ.js.map +1 -0
- package/dist/cli-console.d.ts +17 -1
- package/dist/{cli-errors-DwQnNATs.js → cli-errors-DsYyNYzd.js} +90 -11
- package/dist/cli-errors-DsYyNYzd.js.map +1 -0
- package/dist/cli-errors.d.ts +14 -0
- package/dist/cli-flags.d.ts +51 -0
- package/dist/{cli-prompt-egtNKkZl.js → cli-prompt-DXG0yk-h.js} +39 -4
- package/dist/{cli-prompt-egtNKkZl.js.map → cli-prompt-DXG0yk-h.js.map} +1 -1
- package/dist/cli.js +121 -271
- package/dist/cli.js.map +1 -1
- package/dist/{collections-schema-version-T6cGvrDi.js → collections-schema-version-D7bDI0PJ.js} +13 -9
- package/dist/collections-schema-version-D7bDI0PJ.js.map +1 -0
- package/dist/{column-plan-helpers-D_V4oaZh.js → column-plan-helpers-IG6uFb1s.js} +2 -2
- package/dist/{column-plan-helpers-D_V4oaZh.js.map → column-plan-helpers-IG6uFb1s.js.map} +1 -1
- package/dist/{connection-C938nR3E.js → connection-czvI7TZL.js} +57 -10
- package/dist/connection-czvI7TZL.js.map +1 -0
- package/dist/{databasePoolManager-Yqdw14wY.js → databasePoolManager-3stMNFL-.js} +4 -6
- package/dist/databasePoolManager-3stMNFL-.js.map +1 -0
- package/dist/db-command-line.d.ts +14 -0
- package/dist/db-subcommands.d.ts +11 -0
- package/dist/{deferred-index-build-Cis_HoGn.js → deferred-index-build-CqaOv1Xx.js} +8 -8
- package/dist/{deferred-index-build-Cis_HoGn.js.map → deferred-index-build-CqaOv1Xx.js.map} +1 -1
- package/dist/{doctor-DvJH-IQG.js → doctor-DW_mlTzz.js} +186 -42
- package/dist/doctor-DW_mlTzz.js.map +1 -0
- package/dist/{ensure-collection-policies-CCqayoUz.js → ensure-collection-policies-DLWgabQ2.js} +13 -10
- package/dist/ensure-collection-policies-DLWgabQ2.js.map +1 -0
- package/dist/{ensure-collection-tables-DvvImXDe.js → ensure-collection-tables-CFIRq8ni.js} +9 -9
- package/dist/{ensure-collection-tables-DvvImXDe.js.map → ensure-collection-tables-CFIRq8ni.js.map} +1 -1
- package/dist/{ensure-tables-Dpgwx5sy.js → ensure-tables-4nA-tfwi.js} +5 -5
- package/dist/{ensure-tables-Dpgwx5sy.js.map → ensure-tables-4nA-tfwi.js.map} +1 -1
- package/dist/{enum-labels-kept-CUlUlRkO.js → enum-labels-kept-PcPW65xy.js} +3 -3
- package/dist/{enum-labels-kept-CUlUlRkO.js.map → enum-labels-kept-PcPW65xy.js.map} +1 -1
- package/dist/{generate-drizzle-schema-C3EAP9HE.js → generate-drizzle-schema-DC0BOUpn.js} +3 -3
- package/dist/{generate-drizzle-schema-C3EAP9HE.js.map → generate-drizzle-schema-DC0BOUpn.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-DA3zM8zc.js → generate-drizzle-schema-logic-CPzCRD-8.js} +4 -4
- package/dist/{generate-drizzle-schema-logic-DA3zM8zc.js.map → generate-drizzle-schema-logic-CPzCRD-8.js.map} +1 -1
- package/dist/{generate-files-ClmOUHXj.js → generate-files-BP-4W_gm.js} +6 -6
- package/dist/{generate-files-ClmOUHXj.js.map → generate-files-BP-4W_gm.js.map} +1 -1
- package/dist/{generate-next-step-DCeS4LrJ.js → generate-next-step-DgK5BlBn.js} +2 -2
- package/dist/{generate-next-step-DCeS4LrJ.js.map → generate-next-step-DgK5BlBn.js.map} +1 -1
- package/dist/{generate-postgres-ddl-logic-4eUP4t8w.js → generate-postgres-ddl-logic-DPl1oH_z.js} +4 -4
- package/dist/{generate-postgres-ddl-logic-4eUP4t8w.js.map → generate-postgres-ddl-logic-DPl1oH_z.js.map} +1 -1
- package/dist/generated-schema-diff-eZCklRZQ.js +1025 -0
- package/dist/generated-schema-diff-eZCklRZQ.js.map +1 -0
- package/dist/{generated-schema-staleness-FlxM2i4i.js → generated-schema-staleness-BvNF3i-s.js} +2 -2
- package/dist/{generated-schema-staleness-FlxM2i4i.js.map → generated-schema-staleness-BvNF3i-s.js.map} +1 -1
- package/dist/history/HistoryService.d.ts +7 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.es.js +288 -11
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-CI5CU_n0.js → introspect-db-logic-DgQVzccW.js} +2 -2
- package/dist/{introspect-db-logic-CI5CU_n0.js.map → introspect-db-logic-DgQVzccW.js.map} +1 -1
- package/dist/{rls-bootstrap-sql-Bd-hJk08.js → migrate-baseline-B-2Ef4Mv.js} +718 -41
- package/dist/migrate-baseline-B-2Ef4Mv.js.map +1 -0
- package/dist/migrate-policy-stamps-BQtFHApx.js +89 -0
- package/dist/migrate-policy-stamps-BQtFHApx.js.map +1 -0
- package/dist/{open-rebase-BKA7Nyoe.js → open-rebase-C8jQ6s5a.js} +324 -1047
- package/dist/open-rebase-C8jQ6s5a.js.map +1 -0
- package/dist/open-rebase.d.ts +9 -1
- package/dist/{pg-error-utils-Bv5n2PLn.js → pg-error-utils-DhiUTlVx.js} +2 -2
- package/dist/{pg-error-utils-Bv5n2PLn.js.map → pg-error-utils-DhiUTlVx.js.map} +1 -1
- package/dist/{plan-schema-CXOucCw-.js → plan-schema-Czf3q4-Q.js} +4 -4
- package/dist/{plan-schema-CXOucCw-.js.map → plan-schema-Czf3q4-Q.js.map} +1 -1
- package/dist/{policy-drift-vm0BcvcE.js → policy-drift-X4A-xAzv.js} +3 -3
- package/dist/{policy-drift-vm0BcvcE.js.map → policy-drift-X4A-xAzv.js.map} +1 -1
- package/dist/pool-disconnects.d.ts +28 -0
- package/dist/{push-index-prebuild-DIequ4bV.js → push-index-prebuild-UTvB5pqH.js} +4 -4
- package/dist/{push-index-prebuild-DIequ4bV.js.map → push-index-prebuild-UTvB5pqH.js.map} +1 -1
- package/dist/push-kept-defaults-BGOlOqRQ.js +137 -0
- package/dist/push-kept-defaults-BGOlOqRQ.js.map +1 -0
- package/dist/{push-legacy-renames-BqJut4F3.js → push-legacy-renames-BwaKi8xF.js} +4 -4
- package/dist/{push-legacy-renames-BqJut4F3.js.map → push-legacy-renames-BwaKi8xF.js.map} +1 -1
- package/dist/{push-policy-plan-CvX8hiPb.js → push-policy-plan-DQ2tWceD.js} +5 -5
- package/dist/{push-policy-plan-CvX8hiPb.js.map → push-policy-plan-DQ2tWceD.js.map} +1 -1
- package/dist/{render-ddl-CQNwZdse.js → render-ddl-4v88y-Gv.js} +4 -4
- package/dist/{render-ddl-CQNwZdse.js.map → render-ddl-4v88y-Gv.js.map} +1 -1
- package/dist/{rls-enforcement-D7P4-ZYX.js → rls-enforcement-BOHuHVlc.js} +43 -4
- package/dist/rls-enforcement-BOHuHVlc.js.map +1 -0
- package/dist/schema/destructive-sql.d.ts +16 -0
- package/dist/schema/doctor-cli.js +5 -5
- package/dist/schema/doctor.d.ts +20 -2
- package/dist/schema/ensure-collection-policies.d.ts +4 -1
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl.js +2 -2
- package/dist/schema/generated-schema-diff.d.ts +21 -0
- package/dist/schema/introspect-db.js +5 -6
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/migrate-policy-stamps.d.ts +23 -0
- package/dist/schema/migration-freshness.d.ts +69 -0
- package/dist/schema/push-kept-defaults.d.ts +57 -0
- package/dist/schema/push-policy-plan.d.ts +5 -0
- package/dist/schema/rls-bootstrap-sql.d.ts +47 -10
- package/dist/security/rls-enforcement.d.ts +18 -0
- package/dist/services/BranchService.d.ts +20 -2
- package/dist/services/FetchService.d.ts +12 -0
- package/dist/services/channel-presence.d.ts +3 -1
- package/dist/services/nested-path.d.ts +16 -0
- package/dist/services/pg-notify-listener.d.ts +2 -0
- package/dist/services/realtimeService.d.ts +18 -1
- package/dist/{source-Bv5KPeIx.js → source-fZb92pN8.js} +2 -2
- package/dist/{source-Bv5KPeIx.js.map → source-fZb92pN8.js.map} +1 -1
- package/dist/{vector-index-BFhyEaiz.js → vector-index-DsQ3V2d2.js} +2 -2
- package/dist/{vector-index-BFhyEaiz.js.map → vector-index-DsQ3V2d2.js.map} +1 -1
- package/dist/{websocket-MC0zTBar.js → websocket-CKYc3V4N.js} +56 -14
- package/dist/websocket-CKYc3V4N.js.map +1 -0
- package/package.json +7 -7
- package/dist/BranchService-iXXZeVYD.js.map +0 -1
- package/dist/backup-cli-B2CAn9fR.js +0 -647
- package/dist/backup-cli-B2CAn9fR.js.map +0 -1
- package/dist/backup-service-Bb39plam.js.map +0 -1
- package/dist/cli-console-DQ_K-sdM.js.map +0 -1
- package/dist/cli-errors-DwQnNATs.js.map +0 -1
- package/dist/cli-flags-D-b0qL9T.js +0 -206
- package/dist/cli-flags-D-b0qL9T.js.map +0 -1
- package/dist/collections-schema-version-T6cGvrDi.js.map +0 -1
- package/dist/connection-C938nR3E.js.map +0 -1
- package/dist/databasePoolManager-Yqdw14wY.js.map +0 -1
- package/dist/doctor-DvJH-IQG.js.map +0 -1
- package/dist/ensure-collection-policies-CCqayoUz.js.map +0 -1
- package/dist/open-rebase-BKA7Nyoe.js.map +0 -1
- package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +0 -1
- package/dist/rls-enforcement-D7P4-ZYX.js.map +0 -1
- package/dist/rolldown-runtime-V7rFbMRe.js +0 -39
- package/dist/websocket-MC0zTBar.js.map +0 -1
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import { createRequire as __createRequire } from "module";
|
|
2
|
-
__createRequire(import.meta.url);
|
|
3
|
-
import { i as __toESM } from "./rolldown-runtime-V7rFbMRe.js";
|
|
4
|
-
import { n as require_arg } from "./cli-prompt-egtNKkZl.js";
|
|
5
|
-
import { suggestNearMiss } from "@rebasepro/utils";
|
|
6
|
-
//#region src/cli-flags.ts
|
|
7
|
-
var import_arg = /* @__PURE__ */ __toESM(require_arg(), 1);
|
|
8
|
-
/**
|
|
9
|
-
* Flags that reach this process on a line it did not compose.
|
|
10
|
-
*
|
|
11
|
-
* `rebase db …` hands the driver the user's whole line: the CLI *reads*
|
|
12
|
-
* `--database-url` and `--docker` to resolve the database and then relays them
|
|
13
|
-
* verbatim, and `--debug` is what `bin/rebase.js` prints after every failure as
|
|
14
|
-
* the thing to re-run with. Rejecting any of the three would make the driver
|
|
15
|
-
* refuse commands the CLI documents.
|
|
16
|
-
*/
|
|
17
|
-
var RELAYED_FLAGS = {
|
|
18
|
-
"--database-url": String,
|
|
19
|
-
"--docker": Boolean,
|
|
20
|
-
"--debug": Boolean,
|
|
21
|
-
"--help": Boolean,
|
|
22
|
-
"-h": "--help"
|
|
23
|
-
};
|
|
24
|
-
/**
|
|
25
|
-
* Parse a driver command's line — `["db", "<subcommand>", …]` — with the
|
|
26
|
-
* relayed flags declared alongside the command's own.
|
|
27
|
-
*
|
|
28
|
-
* Every driver parser is permissive, and an undeclared flag in a permissive
|
|
29
|
-
* parse is a positional. The positionals are what `db migrate` hands
|
|
30
|
-
* `atlas migrate apply`, what `db generate` names its migration after and
|
|
31
|
-
* which backup `db restore` restores, so a relayed flag left undeclared was
|
|
32
|
-
* read as one of those: `rebase db migrate --docker` died on Atlas's
|
|
33
|
-
* `unknown flag: --docker` after it had already written the auth schema.
|
|
34
|
-
*/
|
|
35
|
-
function parseDriverLine(spec, args) {
|
|
36
|
-
return (0, import_arg.default)({
|
|
37
|
-
...RELAYED_FLAGS,
|
|
38
|
-
...spec
|
|
39
|
-
}, {
|
|
40
|
-
argv: args.slice(2),
|
|
41
|
-
permissive: true
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
/** Keyed by `"<domain> <subcommand>"`, the way the user types it. */
|
|
45
|
-
var DRIVER_FLAG_SPECS = {
|
|
46
|
-
"db push": {
|
|
47
|
-
"--collections": String,
|
|
48
|
-
"-c": "--collections",
|
|
49
|
-
"--dry-run": Boolean,
|
|
50
|
-
"--allow-destructive": Boolean,
|
|
51
|
-
"--yes": Boolean,
|
|
52
|
-
"-y": "--yes"
|
|
53
|
-
},
|
|
54
|
-
"db generate": {
|
|
55
|
-
"--collections": String,
|
|
56
|
-
"-c": "--collections"
|
|
57
|
-
},
|
|
58
|
-
"db migrate": {
|
|
59
|
-
"--baseline": String,
|
|
60
|
-
"--refuse-destructive": Boolean
|
|
61
|
-
},
|
|
62
|
-
"schema generate": {
|
|
63
|
-
"--collections": String,
|
|
64
|
-
"-c": "--collections",
|
|
65
|
-
"--output": String,
|
|
66
|
-
"-o": "--output",
|
|
67
|
-
"--out": "--output",
|
|
68
|
-
"--watch": Boolean,
|
|
69
|
-
"-w": "--watch"
|
|
70
|
-
},
|
|
71
|
-
"schema introspect": {
|
|
72
|
-
"--output": String,
|
|
73
|
-
"-o": "--output",
|
|
74
|
-
"--out": "--output",
|
|
75
|
-
"--collections": String,
|
|
76
|
-
"-c": "--collections",
|
|
77
|
-
"--force": Boolean,
|
|
78
|
-
"-f": "--force",
|
|
79
|
-
"--schema": String,
|
|
80
|
-
"--data-inference": Boolean,
|
|
81
|
-
"--no-data-inference": Boolean
|
|
82
|
-
},
|
|
83
|
-
"schema stale": {
|
|
84
|
-
"--collections": String,
|
|
85
|
-
"-c": "--collections",
|
|
86
|
-
"--output": String,
|
|
87
|
-
"-o": "--output",
|
|
88
|
-
"--out": "--output",
|
|
89
|
-
"--fix": Boolean
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
/**
|
|
93
|
-
* The `--collections` path the user typed, or `null` if they typed none.
|
|
94
|
-
*
|
|
95
|
-
* Read once, at the entry point, because every command that takes it re-enters
|
|
96
|
-
* the generators with the same line and each of them resolved it again — which
|
|
97
|
-
* is how "Collections path not found" came to be printed four times before the
|
|
98
|
-
* real output, and how a path that does not exist got as far as *writing* an
|
|
99
|
-
* empty schema.
|
|
100
|
-
*
|
|
101
|
-
* Permissive, and it has to be: {@link assertKnownFlags} has already judged the
|
|
102
|
-
* line, and `db push` carries flags the `--collections` spec does not name.
|
|
103
|
-
*/
|
|
104
|
-
function collectionsPathIn(args) {
|
|
105
|
-
try {
|
|
106
|
-
return (0, import_arg.default)({
|
|
107
|
-
"--collections": String,
|
|
108
|
-
"-c": "--collections"
|
|
109
|
-
}, {
|
|
110
|
-
argv: args.slice(2),
|
|
111
|
-
permissive: true
|
|
112
|
-
})["--collections"] ?? null;
|
|
113
|
-
} catch {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Reject a flag the named command does not take.
|
|
119
|
-
*
|
|
120
|
-
* `args` is the driver's whole line — `["db", "push", …]` — so the flags are
|
|
121
|
-
* read from `args.slice(2)`, past the two command words.
|
|
122
|
-
*
|
|
123
|
-
* Throws rather than exiting: `runPluginCommand`'s caller already turns a
|
|
124
|
-
* thrown error into one red line and exit 1, and a thrown error is what the
|
|
125
|
-
* tests can see.
|
|
126
|
-
*/
|
|
127
|
-
function assertKnownFlags(domain, subcommand, args) {
|
|
128
|
-
if (!subcommand) return;
|
|
129
|
-
const spec = DRIVER_FLAG_SPECS[`${domain} ${subcommand}`];
|
|
130
|
-
if (!spec) return;
|
|
131
|
-
try {
|
|
132
|
-
(0, import_arg.default)({
|
|
133
|
-
...RELAYED_FLAGS,
|
|
134
|
-
...spec
|
|
135
|
-
}, { argv: args.slice(2) });
|
|
136
|
-
} catch (err) {
|
|
137
|
-
const unknown = unknownFlagIn(err);
|
|
138
|
-
if (unknown !== null && err instanceof Error) {
|
|
139
|
-
const suggestion = closestFlag(unknown, {
|
|
140
|
-
...RELAYED_FLAGS,
|
|
141
|
-
...spec
|
|
142
|
-
});
|
|
143
|
-
const own = Object.keys(spec).filter((flag) => flag.startsWith("--"));
|
|
144
|
-
throw new Error(`\`rebase ${domain} ${subcommand}\` has no option ${unknown.split("=")[0]}` + (suggestion ? ` — did you mean ${suggestion}?` : ` — it takes ${own.join(", ")}.`) + ` Nothing was run. See \`rebase ${domain} --help\`.`);
|
|
145
|
-
}
|
|
146
|
-
throw err;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
/** The flag `arg` refused as undeclared, or `null` for any other failure. */
|
|
150
|
-
function unknownFlagIn(err) {
|
|
151
|
-
if (!(err instanceof Error) || !("code" in err) || err.code !== "ARG_UNKNOWN_OPTION") return null;
|
|
152
|
-
return /option: (\S+)$/.exec(err.message)?.[1] ?? null;
|
|
153
|
-
}
|
|
154
|
-
/** The declared long flag `typed` is one edit or one swap away from, if there is one. */
|
|
155
|
-
function closestFlag(typed, spec) {
|
|
156
|
-
const name = typed.split("=")[0];
|
|
157
|
-
return closestWord(name, Object.keys(spec).filter((flag) => flag.startsWith("--")));
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* The word in `known` that `typed` was probably meant to be: one edit away
|
|
161
|
-
* (`--targt-db`), or two neighbouring letters swapped (`--clena`, `--froce`),
|
|
162
|
-
* which `suggestNearMiss` counts as two edits and so never offers.
|
|
163
|
-
*/
|
|
164
|
-
function closestWord(typed, known) {
|
|
165
|
-
return suggestNearMiss(known, typed) ?? transposedFrom(known, typed);
|
|
166
|
-
}
|
|
167
|
-
function transposedFrom(known, typed) {
|
|
168
|
-
return known.find((candidate) => {
|
|
169
|
-
if (candidate.length !== typed.length || candidate === typed) return false;
|
|
170
|
-
const differ = [...candidate].flatMap((letter, index) => letter === typed[index] ? [] : [index]);
|
|
171
|
-
return differ.length === 2 && differ[1] === differ[0] + 1 && candidate[differ[0]] === typed[differ[1]] && candidate[differ[1]] === typed[differ[0]];
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Parse a command that owns its argument handling, refusing what it does not
|
|
176
|
-
* declare.
|
|
177
|
-
*
|
|
178
|
-
* `db backup`, `db restore` and `db backups list` parsed permissively, so an
|
|
179
|
-
* undeclared flag became a positional and was ignored: `db restore <dump>
|
|
180
|
-
* --targt-db x --create-db --yes` dropped the misspelled target and restored
|
|
181
|
-
* into the live database. A typo is refused here before anything runs, naming
|
|
182
|
-
* the declared flag it is one edit from, or every flag the command takes when
|
|
183
|
-
* none is that close.
|
|
184
|
-
*
|
|
185
|
-
* @param argv the words after the command, e.g. `rawArgs.slice(2)`.
|
|
186
|
-
* @param command the command as typed, for the message: `"db restore"`.
|
|
187
|
-
*/
|
|
188
|
-
function parseCommandFlags(spec, argv, command) {
|
|
189
|
-
const full = {
|
|
190
|
-
...RELAYED_FLAGS,
|
|
191
|
-
...spec
|
|
192
|
-
};
|
|
193
|
-
try {
|
|
194
|
-
return (0, import_arg.default)(full, { argv });
|
|
195
|
-
} catch (err) {
|
|
196
|
-
const unknown = unknownFlagIn(err);
|
|
197
|
-
if (unknown === null) throw err;
|
|
198
|
-
const suggestion = closestFlag(unknown, full);
|
|
199
|
-
const flags = Object.keys(spec).filter((flag) => flag.startsWith("--"));
|
|
200
|
-
throw new Error(`\`rebase ${command}\` has no option ${unknown}` + (suggestion ? ` — did you mean ${suggestion}?` : ` — it takes ${flags.join(", ")}.`) + ` Nothing was run. See \`rebase ${command} --help\`.`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
//#endregion
|
|
204
|
-
export { parseCommandFlags as a, collectionsPathIn as i, assertKnownFlags as n, parseDriverLine as o, closestWord as r, RELAYED_FLAGS as t };
|
|
205
|
-
|
|
206
|
-
//# sourceMappingURL=cli-flags-D-b0qL9T.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli-flags-D-b0qL9T.js","names":[],"sources":["../src/cli-flags.ts"],"sourcesContent":["/**\n * The flags each driver command takes, in one place, and the check that\n * enforces them.\n *\n * Every parser in `cli.ts` runs `arg(..., { permissive: true })`, which does not\n * mean \"be lenient\" — it means **an undeclared flag becomes a positional**. So\n * `rebase db push --alow-destructive` did not fail: the typo landed in `_`, the\n * push ran with the destructive gate still closed, and the developer read the\n * refusal as Rebase ignoring the flag they had just typed. `rebase schema\n * generate --ouput src/schema.ts` was worse — it wrote the default path and\n * said nothing, so the next build compiled a file nobody had regenerated.\n *\n * The check has to live at the entry point rather than in those parsers, and\n * that is not a detail: `db push` and `db generate` re-enter `schemaCommand`\n * and `generatePostgresDdlCommand` with the *db* line, so a strict parser\n * inside either of them would reject `--allow-destructive` on a line where it\n * is correct. One validation, once, against the spec for the command the user\n * actually named; the inner parsers keep reading an already-validated line.\n *\n * Commands with their own argument handling — `db branch` (`unexpectedBranchArgs`),\n * `db backup`, `db restore`, `db backups` — are deliberately absent: they own\n * their spec, and a second list here would be one more thing to keep in step.\n * An absent entry is \"not checked here\", never \"takes nothing\".\n */\nimport arg from \"arg\";\nimport { suggestNearMiss } from \"@rebasepro/utils\";\n\n/**\n * Flags that reach this process on a line it did not compose.\n *\n * `rebase db …` hands the driver the user's whole line: the CLI *reads*\n * `--database-url` and `--docker` to resolve the database and then relays them\n * verbatim, and `--debug` is what `bin/rebase.js` prints after every failure as\n * the thing to re-run with. Rejecting any of the three would make the driver\n * refuse commands the CLI documents.\n */\nexport const RELAYED_FLAGS = {\n \"--database-url\": String,\n \"--docker\": Boolean,\n \"--debug\": Boolean,\n \"--help\": Boolean,\n \"-h\": \"--help\"\n} satisfies arg.Spec;\n\n/**\n * Parse a driver command's line — `[\"db\", \"<subcommand>\", …]` — with the\n * relayed flags declared alongside the command's own.\n *\n * Every driver parser is permissive, and an undeclared flag in a permissive\n * parse is a positional. The positionals are what `db migrate` hands\n * `atlas migrate apply`, what `db generate` names its migration after and\n * which backup `db restore` restores, so a relayed flag left undeclared was\n * read as one of those: `rebase db migrate --docker` died on Atlas's\n * `unknown flag: --docker` after it had already written the auth schema.\n */\nexport function parseDriverLine<S extends arg.Spec>(\n spec: S,\n args: string[]\n): arg.Result<typeof RELAYED_FLAGS & S> {\n return arg({ ...RELAYED_FLAGS, ...spec }, { argv: args.slice(2), permissive: true });\n}\n\n/** Keyed by `\"<domain> <subcommand>\"`, the way the user types it. */\nexport const DRIVER_FLAG_SPECS: Record<string, arg.Spec> = {\n \"db push\": {\n \"--collections\": String,\n \"-c\": \"--collections\",\n \"--dry-run\": Boolean,\n \"--allow-destructive\": Boolean,\n \"--yes\": Boolean,\n \"-y\": \"--yes\"\n },\n \"db generate\": {\n \"--collections\": String,\n \"-c\": \"--collections\"\n },\n // Bare positionals still pass through to `atlas migrate apply`, which takes\n // an optional amount; flags do not, apart from `--baseline`.\n //\n // `--baseline <version>` is the escape hatch for the normal case, not an\n // exotic one: every Rebase boot ensures the schema, so a production\n // database has the tables before its first migration ever runs, and\n // `migrate apply` then dies on `type \"…\" already exists (42710)`. It maps\n // onto Atlas's own `migrate apply --baseline`, which writes the revision\n // row Atlas reads — no ledger of ours.\n //\n // `--refuse-destructive` is opt-in: it dry-runs the pending files and exits\n // 1, applying nothing, when one drops or narrows data — the gate `db push`\n // has always had, for the path that had none.\n \"db migrate\": {\n \"--baseline\": String,\n \"--refuse-destructive\": Boolean\n },\n \"schema generate\": {\n \"--collections\": String,\n \"-c\": \"--collections\",\n \"--output\": String,\n \"-o\": \"--output\",\n \"--out\": \"--output\",\n \"--watch\": Boolean,\n \"-w\": \"--watch\"\n },\n \"schema introspect\": {\n \"--output\": String,\n \"-o\": \"--output\",\n \"--out\": \"--output\",\n \"--collections\": String,\n \"-c\": \"--collections\",\n \"--force\": Boolean,\n \"-f\": \"--force\",\n \"--schema\": String,\n // Whether rows are sampled at all: without a terminal the prompt is\n // skipped, and the skip line names `--data-inference`.\n \"--data-inference\": Boolean,\n \"--no-data-inference\": Boolean\n },\n \"schema stale\": {\n \"--collections\": String,\n \"-c\": \"--collections\",\n \"--output\": String,\n \"-o\": \"--output\",\n \"--out\": \"--output\",\n \"--fix\": Boolean\n }\n};\n\n/**\n * One destination flag, two spellings, everywhere.\n *\n * `--out` is the primary on `rebase build`, an alias on `generate-sdk`, `db\n * backup` and `cloud env pull`, and was refused outright by the three `schema`\n * commands — so the spelling a user learned on one command was an\n * \"unknown or unexpected option\" on the next. Neither name can be retired (both\n * are shipped), so both are accepted, and {@link assertOutputAliasesPaired}\n * makes that the rule rather than a habit.\n */\nexport const OUTPUT_FLAG_ALIASES = [\"--out\", \"--output\"] as const;\n\n/**\n * Every spec that names one of the pair names both.\n *\n * Exported so the CLI's own specs can be held to it too: the drift this fixes\n * ran across two packages, and a check that only reads this file would let the\n * next `--out`-only command through.\n */\nexport function assertOutputAliasesPaired(specs: Record<string, arg.Spec>): string[] {\n const problems: string[] = [];\n for (const [command, spec] of Object.entries(specs)) {\n const present = OUTPUT_FLAG_ALIASES.filter(flag => flag in spec);\n if (present.length === 0 || present.length === OUTPUT_FLAG_ALIASES.length) continue;\n const missing = OUTPUT_FLAG_ALIASES.filter(flag => !(flag in spec));\n problems.push(`\\`${command}\\` takes ${present.join(\", \")} but not ${missing.join(\", \")}`);\n }\n return problems;\n}\n\n/**\n * The `--collections` path the user typed, or `null` if they typed none.\n *\n * Read once, at the entry point, because every command that takes it re-enters\n * the generators with the same line and each of them resolved it again — which\n * is how \"Collections path not found\" came to be printed four times before the\n * real output, and how a path that does not exist got as far as *writing* an\n * empty schema.\n *\n * Permissive, and it has to be: {@link assertKnownFlags} has already judged the\n * line, and `db push` carries flags the `--collections` spec does not name.\n */\nexport function collectionsPathIn(args: string[]): string | null {\n try {\n const parsed = arg(\n { \"--collections\": String, \"-c\": \"--collections\" },\n { argv: args.slice(2), permissive: true }\n );\n\n return parsed[\"--collections\"] ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * The long flags a `--help` usage line documents.\n *\n * `\"rebase db push [--collections <dir>] [--dry-run] …\"` → `[\"--collections\",\n * \"--dry-run\"]`. Placeholders (`<dir>`) and the alternation inside a positional\n * (`<create|list|switch>`) are not flags and are not returned.\n */\nexport function flagsInUsage(usage: string): string[] {\n return [...new Set(usage.match(/--[a-z][a-z0-9-]*/g) ?? [])];\n}\n\n/**\n * Every flag the help documents is accepted, and every flag accepted is documented.\n *\n * The drift this catches shipped: `rebase db push --help` has printed\n * `[--dry-run]` since the flag was written, `DRIVER_FLAG_SPECS[\"db push\"]` never\n * listed it, and {@link assertKnownFlags} — added later to stop typos being\n * swallowed — turned the documented flag into `unknown or unexpected option`.\n * The only way to see a push's SQL was to trip the destructive gate, which is\n * the exact problem `--dry-run` was written to solve. Worse in a project on an\n * older driver, whose permissive parser *applied* the schema on that line.\n *\n * Two hand-maintained lists in two packages cannot be kept in step by care, so\n * they are held to each other instead: `usages` is keyed the way\n * {@link DRIVER_FLAG_SPECS} is (`\"db push\"`), and comes from the CLI's own help\n * pages. A key in only one of the two is not this function's business — the\n * help covers commands that parse their own lines (`db branch`, `db backup`),\n * and an absent spec entry means \"not checked here\".\n *\n * Aliases (`\"-c\": \"--collections\"`, `\"--out\": \"--output\"`) need no line of their\n * own: they are spellings of a documented flag. Neither do {@link RELAYED_FLAGS},\n * which every driver command accepts because the CLI relays them.\n */\nexport function assertSpecMatchesUsage(\n specs: Record<string, arg.Spec>,\n usages: Record<string, string>\n): string[] {\n const problems: string[] = [];\n for (const [command, spec] of Object.entries(specs)) {\n const usage = usages[command];\n if (usage === undefined) continue;\n\n const documented = flagsInUsage(usage);\n const accepted = Object.entries(spec);\n // An alias resolves to the flag it spells; only the target needs a line.\n const canonical = new Set(\n accepted\n .map(([flag, kind]) => (typeof kind === \"string\" ? kind : flag))\n .filter(flag => flag.startsWith(\"--\"))\n );\n\n for (const flag of documented) {\n if (flag in spec || flag in RELAYED_FLAGS) continue;\n problems.push(`\\`${command}\\` documents ${flag} in its usage line but the spec rejects it`);\n }\n for (const flag of canonical) {\n if (documented.includes(flag) || flag in RELAYED_FLAGS) continue;\n problems.push(`\\`${command}\\` accepts ${flag} but no usage line documents it`);\n }\n }\n return problems;\n}\n\n/**\n * Reject a flag the named command does not take.\n *\n * `args` is the driver's whole line — `[\"db\", \"push\", …]` — so the flags are\n * read from `args.slice(2)`, past the two command words.\n *\n * Throws rather than exiting: `runPluginCommand`'s caller already turns a\n * thrown error into one red line and exit 1, and a thrown error is what the\n * tests can see.\n */\nexport function assertKnownFlags(domain: string, subcommand: string | undefined, args: string[]): void {\n if (!subcommand) return;\n const spec = DRIVER_FLAG_SPECS[`${domain} ${subcommand}`];\n if (!spec) return;\n\n try {\n arg({ ...RELAYED_FLAGS, ...spec }, { argv: args.slice(2) });\n } catch (err) {\n const unknown = unknownFlagIn(err);\n if (unknown !== null && err instanceof Error) {\n // In the words `parseCommandFlags` and the CLI's own parser use.\n const suggestion = closestFlag(unknown, { ...RELAYED_FLAGS, ...spec });\n const own = Object.keys(spec).filter(flag => flag.startsWith(\"--\"));\n throw new Error(\n `\\`rebase ${domain} ${subcommand}\\` has no option ${unknown.split(\"=\")[0]}` +\n (suggestion ? ` — did you mean ${suggestion}?` : ` — it takes ${own.join(\", \")}.`) +\n ` Nothing was run. See \\`rebase ${domain} --help\\`.`\n );\n }\n throw err;\n }\n}\n\n/** The flag `arg` refused as undeclared, or `null` for any other failure. */\nfunction unknownFlagIn(err: unknown): string | null {\n if (!(err instanceof Error) || !(\"code\" in err) || err.code !== \"ARG_UNKNOWN_OPTION\") return null;\n // arg's wording: `unknown or unexpected option: --targt-db`.\n return /option: (\\S+)$/.exec(err.message)?.[1] ?? null;\n}\n\n/** The declared long flag `typed` is one edit or one swap away from, if there is one. */\nfunction closestFlag(typed: string, spec: arg.Spec): string | undefined {\n const name = typed.split(\"=\")[0];\n return closestWord(name, Object.keys(spec).filter(flag => flag.startsWith(\"--\")));\n}\n\n/**\n * The word in `known` that `typed` was probably meant to be: one edit away\n * (`--targt-db`), or two neighbouring letters swapped (`--clena`, `--froce`),\n * which `suggestNearMiss` counts as two edits and so never offers.\n */\nexport function closestWord(typed: string, known: readonly string[]): string | undefined {\n return suggestNearMiss(known, typed) ?? transposedFrom(known, typed);\n}\n\nfunction transposedFrom(known: readonly string[], typed: string): string | undefined {\n return known.find((candidate) => {\n if (candidate.length !== typed.length || candidate === typed) return false;\n const differ = [...candidate].flatMap((letter, index) => (letter === typed[index] ? [] : [index]));\n return differ.length === 2 && differ[1] === differ[0] + 1\n && candidate[differ[0]] === typed[differ[1]] && candidate[differ[1]] === typed[differ[0]];\n });\n}\n\n/**\n * Parse a command that owns its argument handling, refusing what it does not\n * declare.\n *\n * `db backup`, `db restore` and `db backups list` parsed permissively, so an\n * undeclared flag became a positional and was ignored: `db restore <dump>\n * --targt-db x --create-db --yes` dropped the misspelled target and restored\n * into the live database. A typo is refused here before anything runs, naming\n * the declared flag it is one edit from, or every flag the command takes when\n * none is that close.\n *\n * @param argv the words after the command, e.g. `rawArgs.slice(2)`.\n * @param command the command as typed, for the message: `\"db restore\"`.\n */\nexport function parseCommandFlags<S extends arg.Spec>(\n spec: S,\n argv: string[],\n command: string\n): arg.Result<typeof RELAYED_FLAGS & S> {\n const full = { ...RELAYED_FLAGS, ...spec };\n try {\n return arg(full, { argv });\n } catch (err) {\n const unknown = unknownFlagIn(err);\n if (unknown === null) throw err;\n const suggestion = closestFlag(unknown, full);\n const flags = Object.keys(spec).filter(flag => flag.startsWith(\"--\"));\n throw new Error(\n `\\`rebase ${command}\\` has no option ${unknown}` +\n (suggestion ? ` — did you mean ${suggestion}?` : ` — it takes ${flags.join(\", \")}.`) +\n ` Nothing was run. See \\`rebase ${command} --help\\`.`\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"collections-schema-version-T6cGvrDi.js","names":[],"sources":["../src/schema/collections-schema-version.ts"],"sourcesContent":["/**\n * The collections schema version, stamped into the database.\n *\n * Deliberately the same table shape, the same key/value convention and the same\n * \"unstamped is null, and null is not an error\" rule as `auth/schema-version.ts`.\n * They answer the same kind of question — \"was this database provisioned by\n * something that agrees with me\" — and inventing a second convention for the\n * second one would be a second thing to find, back up and reason about.\n *\n * It does *not* follow the auth stamp's schema. `resolveAuthSchema` tracks the\n * users collection, so a project that puts its users in a custom schema moves\n * the auth stamp with them — correct there, because that stamp describes those\n * tables. This one describes the collections as a whole and belongs with the\n * runtime's own internal state, which is always `rebase`. Usually they land in\n * the same table anyway; when they do not, that is the reason.\n *\n * The value is opaque here on purpose. It is a hash produced by\n * `computeSchemaVersion` in `@rebasepro/types`, and this module's only job is to\n * put a string in and take the same string out. Comparing them, and deciding\n * what a difference means, is `boot/schema-stamp.ts` in the runtime — where it\n * can be tested without a database.\n *\n * Unlike the auth version, this is **not** ordered. The auth stamp is an integer\n * whose comparison direction carries meaning (a database newer than the runtime\n * is unrecoverable, older is the ordinary upgrade path). A schema hash has no\n * order at all: it can say the two disagree and never which is ahead. That is a\n * deliberate limit, and the reason the runtime's response to a mismatch is to\n * describe it rather than to decide who is wrong.\n */\nimport { sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\n\n/** Key under which the version is stored in the meta table. */\nconst VERSION_KEY = \"collections_schema_version\";\n\n/**\n * Anything that can run SQL for us.\n *\n * Narrower than `NodePgDatabase` so the provisioning handle — which is what the\n * boot path actually has in hand — satisfies it without a cast at every call\n * site.\n */\nexport interface SchemaMetaQueryable {\n execute(query: unknown): Promise<{ rows: Record<string, unknown>[] }>;\n}\n\n/**\n * Read the stamped version, or `null` when this database has never been stamped.\n *\n * `to_regclass` rather than a plain `SELECT` so a missing schema or table is a\n * `null` instead of a thrown 42P01 — the overwhelmingly common case on a fresh\n * database is that neither exists yet, and that is not news.\n */\nexport async function readCollectionsSchemaVersion(\n db: SchemaMetaQueryable | NodePgDatabase,\n metaSchema: string\n): Promise<string | null> {\n const qualified = `\"${metaSchema}\".\"schema_meta\"`;\n const exists = await (db as SchemaMetaQueryable).execute(\n sql`SELECT to_regclass(${qualified}) IS NOT NULL AS present`\n );\n if (!(exists.rows[0] as { present: boolean } | undefined)?.present) return null;\n\n const result = await (db as SchemaMetaQueryable).execute(sql`\n SELECT value FROM ${sql.raw(qualified)} WHERE key = ${VERSION_KEY}\n `);\n const raw = (result.rows[0] as { value: string } | undefined)?.value;\n\n // An empty string is not a version anybody computed, and treating it as one\n // would make every process disagree with the database forever. Unstamped.\n return raw === undefined || raw.trim() === \"\" ? null : raw;\n}\n\n/**\n * Record the version this runtime just applied.\n *\n * Creates the meta table if the auth stamp has not already — the two are\n * independent halves of one boot and either may run first, so neither can assume\n * the table is there. `CREATE TABLE IF NOT EXISTS` is not atomic against a\n * concurrent identical statement, which is exactly the race a split deployment\n * arranges; it is tolerated here for the same reason it is elsewhere in the boot\n * path, and the caller treats any failure as a warning rather than a fatal.\n */\nexport async function stampCollectionsSchemaVersion(\n db: SchemaMetaQueryable | NodePgDatabase,\n metaSchema: string,\n version: string\n): Promise<void> {\n const qualified = `\"${metaSchema}\".\"schema_meta\"`;\n await (db as SchemaMetaQueryable).execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.identifier(metaSchema)}`);\n await (db as SchemaMetaQueryable).execute(sql`\n CREATE TABLE IF NOT EXISTS ${sql.raw(qualified)} (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL\n )\n `);\n await (db as SchemaMetaQueryable).execute(sql`\n INSERT INTO ${sql.raw(qualified)} (key, value)\n VALUES (${VERSION_KEY}, ${version})\n ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW()\n `);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,IAAM,cAAc;;;;;;;;AAoBpB,eAAsB,6BAClB,IACA,YACsB;CACtB,MAAM,YAAY,IAAI,WAAW;CAIjC,IAAI,EAAE,MAHgB,GAA2B,QAC7C,GAAG,sBAAsB,UAAU,yBACvC,EAAA,CACa,KAAK,EAAE,EAAuC,SAAS,OAAO;CAK3E,MAAM,OAAO,MAHS,GAA2B,QAAQ,GAAG;4BACpC,IAAI,IAAI,SAAS,EAAE,eAAe,YAAY;KACrE,EAAA,CACmB,KAAK,EAAE,EAAoC;CAI/D,OAAO,QAAQ,KAAA,KAAa,IAAI,KAAK,MAAM,KAAK,OAAO;AAC3D;;;;;;;;;;;AAYA,eAAsB,8BAClB,IACA,YACA,SACa;CACb,MAAM,YAAY,IAAI,WAAW;CACjC,MAAO,GAA2B,QAAQ,GAAG,+BAA+B,IAAI,WAAW,UAAU,GAAG;CACxG,MAAO,GAA2B,QAAQ,GAAG;qCACZ,IAAI,IAAI,SAAS,EAAE;;;;;KAKnD;CACD,MAAO,GAA2B,QAAQ,GAAG;sBAC3B,IAAI,IAAI,SAAS,EAAE;kBACvB,YAAY,IAAI,QAAQ;;KAErC;AACL"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"connection-C938nR3E.js","names":[],"sources":["../src/connection.ts"],"sourcesContent":["import { Pool, PoolConfig } from \"pg\";\nimport { drizzle } from \"drizzle-orm/node-postgres\";\nimport { logger } from \"@rebasepro/server\";\n\n/**\n * Configuration for the Postgres connection pool.\n *\n * Sensible defaults are provided for production Cloud Run / single-instance\n * deployments. Override via environment variables or explicit config.\n */\nexport interface PostgresPoolConfig {\n /** Maximum number of connections in the pool (default: 20) */\n max?: number;\n /** Close idle connections after this many ms (default: 30 000) */\n idleTimeoutMillis?: number;\n /** Abort connection attempts after this many ms (default: 10 000) */\n connectionTimeoutMillis?: number;\n /** Per-query timeout in ms (default: 30 000) */\n queryTimeout?: number;\n /** Per-statement timeout in ms (default: 30 000) */\n statementTimeout?: number;\n /** Enable TCP keep-alive (default: true) */\n keepAlive?: boolean;\n /**\n * `search_path` pinned on every connection (default: `\"public\"`).\n *\n * Pass `false` to send no `search_path` at all and inherit whatever the\n * server/role defaults to. See {@link pinSearchPath} for why the default\n * is not \"inherit\".\n */\n searchPath?: string | false;\n}\n\n/**\n * Ceiling on pool size, from the environment.\n *\n * Exists for the managed development database. PGlite is a single session\n * behind a multiplexing socket server, and two pooled clients holding\n * overlapping transactions deadlock there — which a request-per-transaction\n * server produces the moment two requests overlap. One connection turns that\n * deadlock into ordinary queueing.\n *\n * A ceiling rather than a value, so it constrains every pool in the process\n * without any of them having to know why. Ignored when unset or unparseable,\n * because a malformed limit must not silently serialize a production server.\n */\nexport function poolMaxCeiling(env: NodeJS.ProcessEnv = process.env): number | null {\n const raw = env.REBASE_DB_POOL_MAX;\n if (typeof raw !== \"string\") return null;\n\n // Plain decimal digits only. `Number` would also accept `1e3`, `0x10` and\n // ` 4 ` — none of which anyone writes on purpose in a config value, and all\n // of which would mean this silently did something other than it appears to.\n if (!/^[0-9]+$/.test(raw.trim())) return null;\n\n const parsed = Number(raw.trim());\n\n return parsed >= 1 ? parsed : null;\n}\n\n/** Apply {@link poolMaxCeiling} to a requested pool size. */\nexport function cappedPoolMax(requested: number, env: NodeJS.ProcessEnv = process.env): number {\n const ceiling = poolMaxCeiling(env);\n\n return ceiling === null ? requested : Math.min(requested, ceiling);\n}\n\nconst DEFAULT_POOL: Required<PostgresPoolConfig> = {\n max: 20,\n searchPath: \"public\",\n idleTimeoutMillis: 30_000,\n connectionTimeoutMillis: 10_000,\n // The client-side read timeout MUST be comfortably above the server-side\n // statement_timeout. When the client timer fires first, node-postgres\n // abandons the in-flight statement but keeps the connection — inside a\n // transaction that leaves the tx open (and any pending ROLLBACK is\n // spliced out of the client queue before it ever reaches the wire), so\n // the pooled connection is returned still in-transaction with its RLS\n // GUCs set. The server abort (SQLSTATE 57014) is the clean path; the\n // client timeout is only a backstop for a dead network.\n queryTimeout: 60_000,\n statementTimeout: 30_000,\n keepAlive: true\n};\n\n/**\n * The time limits every pool to this server shares: how long to wait for a\n * connection (then 503), the server-side statement timeout, the client-side\n * read backstop above it, and TCP keep-alive. Exported for the per-database\n * pools (`DatabasePoolManager`), which had none of them — pg's default\n * `connectionTimeoutMillis` is 0, wait forever.\n */\nexport function defaultPoolTimeouts(): Pick<PoolConfig,\n \"connectionTimeoutMillis\" | \"query_timeout\" | \"statement_timeout\" | \"keepAlive\" | \"keepAliveInitialDelayMillis\"> {\n return {\n connectionTimeoutMillis: DEFAULT_POOL.connectionTimeoutMillis,\n query_timeout: DEFAULT_POOL.queryTimeout,\n statement_timeout: DEFAULT_POOL.statementTimeout,\n keepAlive: DEFAULT_POOL.keepAlive,\n keepAliveInitialDelayMillis: 0\n };\n}\n\n/** ReadyForQuery status byte: `I` idle, `T` in transaction, `E` failed transaction. */\nconst TX_IDLE = \"I\";\n\n/**\n * Pin `search_path` into a connection string, so unqualified SQL resolves to a\n * schema this framework chose rather than to one Postgres inferred.\n *\n * Postgres defaults `search_path` to `\"$user\", public`: the *first* candidate\n * is a schema named after the connecting role. Rebase creates a schema called\n * `rebase` (auth, history, api keys), and every template, compose file and\n * deployment doc names the database role `rebase` too — so `$user` resolves to\n * a schema that exists, and every unqualified statement lands there instead of\n * in `public`. The generated Drizzle schema emits bare `pgTable(\"posts\", …)`\n * for any collection without an explicit `schema`, which makes the *runtime's*\n * own reads and writes unqualified; a developer's raw `rebase.sql(...)`, the\n * Studio SQL editor and any hand-written migration are unqualified too. The\n * result is collection tables created in, and served from, `rebase`.\n *\n * Drizzle cannot express the fix on its side: `pgSchema(\"public\")` throws by\n * design (\"just use pgTable() instead\"), so there is no way to emit a\n * public-qualified table from the generator. The pin has to live on the\n * connection.\n *\n * Precedence is deliberate and verified against node-postgres: `options` in\n * the connection string wins over the `options` field passed to `Pool`, so\n * rewriting the URL — rather than setting the field — is what makes this\n * authoritative. Two escape hatches survive it:\n *\n * - an `options` that already mentions `search_path` is left untouched, so a\n * deployment that deliberately pins something else keeps it;\n * - `searchPath: false` (or an unparseable, non-URL connection string) sends\n * nothing and inherits the server default.\n *\n * Anything else in `options` (a `statement_timeout`, say) is preserved and the\n * `search_path` flag is appended to it.\n */\nexport function pinSearchPath(connectionString: string, searchPath: string | false = \"public\"): string {\n if (searchPath === false) return connectionString;\n\n let url: URL;\n try {\n url = new URL(connectionString);\n } catch {\n // Key/value DSNs and anything else we cannot parse are returned as\n // given: a connection that works unpinned beats one we corrupted.\n return connectionString;\n }\n if (url.protocol !== \"postgres:\" && url.protocol !== \"postgresql:\") return connectionString;\n\n const existing = url.searchParams.get(\"options\");\n if (existing && /(^|\\s)-c\\s*search_path\\s*=/.test(existing)) return connectionString;\n\n const flag = `-c search_path=${searchPath}`;\n url.searchParams.set(\"options\", existing ? `${existing} ${flag}` : flag);\n // Re-serialize by hand. `URLSearchParams` writes a space as `+`, which\n // node-postgres happens to decode but libpq does not — and this same string\n // is handed to `pg_dump`/`psql` for backups. Percent-encoding is the form\n // both agree on, and is what the scaffolded `.env` already ships.\n url.search = Array.from(url.searchParams.entries())\n .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)\n .join(\"&\");\n return url.toString();\n}\n\n/**\n * Destroy pool clients that are released while still inside a transaction.\n *\n * pg-pool returns a client to the idle list whenever `release()` is called\n * without an error — even if the connection is still mid-transaction (status\n * `T`/`E`). That happens in practice: drizzle's pool transaction releases in\n * a `finally` after attempting ROLLBACK, and if the ROLLBACK itself fails\n * (e.g. it was queued behind a statement that hit the client-side\n * query_timeout), the client goes back dirty. The next checkout then runs\n * its statements inside the zombie transaction — with the previous request's\n * `app.*` RLS GUCs still applied, which turns unrelated queries into\n * RLS-scoped ones (observed in production as registration failing with\n * SQLSTATE 42501 under a leaked anonymous context).\n *\n * pg-pool emits `release` before it consults its private `_expired` set, so\n * marking the client expired here makes `_release()` destroy it instead of\n * pooling it. Both `client._txStatus` (pg ≥ 8.16) and `pool._expired` are\n * private APIs — feature-detect and fall back to loud logging so an upstream\n * change degrades to observability, never to silent corruption.\n */\nexport function guardPoolAgainstDirtyRelease(pool: Pool, label: string): void {\n pool.on(\"release\", (err: Error | undefined, client: unknown) => {\n if (err) return; // errored clients are already destroyed by pg-pool\n const txStatus = (client as { _txStatus?: string | null })?._txStatus;\n if (typeof txStatus !== \"string\" || txStatus === TX_IDLE) return;\n\n // pg-pool keeps expired clients in a WeakSet (a plain object works too\n // if upstream ever changes it — duck-type on add/has).\n const expired = (pool as unknown as { _expired?: { add(c: object): unknown; has(c: object): boolean } })._expired;\n if (expired && typeof expired.add === \"function\" && typeof expired.has === \"function\" && client && typeof client === \"object\") {\n expired.add(client);\n logger.error(\n `[${label}] Client released back to the pool while still in a transaction ` +\n `(status '${txStatus}') — destroying it so the open transaction and its ` +\n `session state (RLS GUCs) cannot leak into the next request.`\n );\n } else {\n logger.error(\n `[${label}] Client released mid-transaction (status '${txStatus}') but the ` +\n `pool's internal expiry set is unavailable (pg-pool internals changed?). ` +\n `The connection may leak its open transaction into subsequent requests.`\n );\n }\n });\n}\n\n/**\n * Create a Drizzle-backed Postgres connection with a production-grade\n * connection pool.\n *\n * @param connectionString Postgres connection URL\n * @param schema Optional Drizzle schema for the relational API\n * @param poolConfig Optional pool tuning (merged over defaults)\n * @param env Where the pool-size ceiling `REBASE_DB_POOL_MAX`\n * is read from (see {@link poolMaxCeiling}); pass\n * `{}` to read none.\n *\n * @returns `{ db, pool, connectionString }` — the `pool` is exposed so\n * callers can register shutdown hooks (`pool.end()`) or monitor\n * pool metrics.\n */\nexport function createPostgresDatabaseConnection(\n connectionString: string,\n schema?: Record<string, unknown>,\n poolConfig?: PostgresPoolConfig,\n env: NodeJS.ProcessEnv = process.env\n) {\n const opts = { ...DEFAULT_POOL,\n...poolConfig };\n connectionString = pinSearchPath(connectionString, opts.searchPath);\n\n const pgPoolConfig: PoolConfig = {\n connectionString,\n max: cappedPoolMax(opts.max, env),\n idleTimeoutMillis: opts.idleTimeoutMillis,\n connectionTimeoutMillis: opts.connectionTimeoutMillis,\n query_timeout: opts.queryTimeout,\n statement_timeout: opts.statementTimeout,\n keepAlive: opts.keepAlive,\n keepAliveInitialDelayMillis: 0\n };\n\n const pool = new Pool(pgPoolConfig);\n\n // ── Pool event logging ────────────────────────────────────────────────\n // Uses console.* because the structured logger lives in server\n // (a separate package). The caller can replace these with the structured\n // logger if desired via pool.on() after creation.\n pool.on(\"error\", (err) => {\n logger.error(\"[pg-pool] Unexpected pool error\", { detail: err.message });\n if (err.message.includes(\"ETIMEDOUT\")) {\n logger.warn(\"[pg-pool] Connection timeout detected — pool will auto-retry\");\n }\n });\n guardPoolAgainstDirtyRelease(pool, \"pg-pool\");\n\n // Create drizzle instance — pass schema when available to enable db.query relational API\n const db = schema ? drizzle(pool, { schema }) : drizzle(pool);\n\n return { db,\npool,\nconnectionString };\n}\n\n/**\n * Create a direct (non-pooled) connection for operations that require\n * session-level features incompatible with PgBouncer transaction mode,\n * such as LISTEN/NOTIFY, prepared statements, or advisory locks.\n *\n * Uses a smaller pool since this is only for specific use cases.\n */\nexport function createDirectDatabaseConnection(\n connectionString: string,\n schema?: Record<string, unknown>,\n poolConfig?: PostgresPoolConfig\n) {\n const opts = {\n ...DEFAULT_POOL,\n max: 5,\n ...poolConfig\n };\n connectionString = pinSearchPath(connectionString, opts.searchPath);\n\n const pgPoolConfig: PoolConfig = {\n connectionString,\n max: cappedPoolMax(opts.max),\n idleTimeoutMillis: opts.idleTimeoutMillis,\n connectionTimeoutMillis: opts.connectionTimeoutMillis,\n query_timeout: opts.queryTimeout,\n statement_timeout: opts.statementTimeout,\n keepAlive: opts.keepAlive,\n keepAliveInitialDelayMillis: 0\n };\n\n const pool = new Pool(pgPoolConfig);\n\n pool.on(\"error\", (err) => {\n logger.error(\"[pg-direct-pool] Unexpected pool error\", { detail: err.message });\n });\n guardPoolAgainstDirtyRelease(pool, \"pg-direct-pool\");\n\n const db = schema ? drizzle(pool, { schema }) : drizzle(pool);\n\n return { db,\npool,\nconnectionString };\n}\n\n/**\n * Create a read-only connection for routing read queries to replicas.\n * Uses a moderate pool size since reads are distributed across replicas.\n */\nexport function createReadReplicaConnection(\n connectionString: string,\n schema?: Record<string, unknown>,\n poolConfig?: PostgresPoolConfig\n) {\n const opts = {\n ...DEFAULT_POOL,\n max: 10,\n ...poolConfig\n };\n connectionString = pinSearchPath(connectionString, opts.searchPath);\n\n const pgPoolConfig: PoolConfig = {\n connectionString,\n max: cappedPoolMax(opts.max),\n idleTimeoutMillis: opts.idleTimeoutMillis,\n connectionTimeoutMillis: opts.connectionTimeoutMillis,\n query_timeout: opts.queryTimeout,\n statement_timeout: opts.statementTimeout,\n keepAlive: opts.keepAlive,\n keepAliveInitialDelayMillis: 0\n };\n\n const pool = new Pool(pgPoolConfig);\n\n pool.on(\"error\", (err) => {\n logger.error(\"[pg-replica-pool] Unexpected pool error\", { detail: err.message });\n });\n guardPoolAgainstDirtyRelease(pool, \"pg-replica-pool\");\n\n const db = schema ? drizzle(pool, { schema }) : drizzle(pool);\n\n return { db,\npool,\nconnectionString };\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"databasePoolManager-Yqdw14wY.js","names":[],"sources":["../src/databasePoolManager.ts"],"sourcesContent":["import { Pool } from \"pg\";\nimport { drizzle } from \"drizzle-orm/node-postgres\";\nimport { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport { logger } from \"@rebasepro/server\";\nimport { guardPoolAgainstDirtyRelease, pinSearchPath, cappedPoolMax, defaultPoolTimeouts } from \"./connection\";\n\nexport class DatabasePoolManager {\n private pools: Map<string, Pool> = new Map();\n private drizzleInstances: Map<string, NodePgDatabase> = new Map();\n public readonly defaultDatabaseName: string;\n private readonly rootConnectionString: string;\n\n constructor(adminConnectionString: string) {\n this.rootConnectionString = adminConnectionString;\n try {\n const url = new URL(adminConnectionString);\n this.defaultDatabaseName = url.pathname.slice(1);\n } catch (e) {\n throw new Error(`Invalid adminConnectionString provided: ${e}`);\n }\n }\n\n public getDrizzle(databaseName: string): NodePgDatabase<Record<string, never>> {\n const existing = this.drizzleInstances.get(databaseName);\n if (existing) {\n return existing;\n }\n\n const pool = this.getPool(databaseName);\n const db = drizzle(pool);\n this.drizzleInstances.set(databaseName, db);\n return db;\n }\n\n public getPool(databaseName: string): Pool {\n if (this.pools.has(databaseName)) {\n return this.pools.get(databaseName)!;\n }\n\n const url = new URL(this.rootConnectionString);\n url.pathname = `/${databaseName}`;\n\n const pool = new Pool({\n // Same pin as the primary pool: these are branch/multi-database\n // connections to the *same* server, so they inherit the same\n // `\"$user\"` hazard. See `pinSearchPath`.\n connectionString: pinSearchPath(url.toString()),\n // Capped by REBASE_DB_POOL_MAX, which the managed development\n // database sets to 1: PGlite multiplexes onto a single session and\n // overlapping transactions deadlock there.\n max: cappedPoolMax(10),\n idleTimeoutMillis: 10000, // Reduced from 30000 for aggressive cleanup\n allowExitOnIdle: true, // Prevent idle clients from hanging the Node.js process\n // The main pool's limits: a full pool answers 503 after 10 s rather\n // than queueing forever, and a statement stops at 30 s server-side.\n ...defaultPoolTimeouts()\n });\n\n // Prevent idle client errors from crashing the Node.js process\n pool.on(\"error\", (err) => {\n logger.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, { error: err });\n });\n guardPoolAgainstDirtyRelease(pool, `pg-pool:${databaseName}`);\n\n this.pools.set(databaseName, pool);\n return pool;\n }\n\n /**\n * Disconnect and remove the pool for a specific database.\n * Required before `CREATE DATABASE ... TEMPLATE` or `DROP DATABASE`,\n * which need exclusive access to the target database.\n */\n public async disconnectDatabase(databaseName: string): Promise<void> {\n const pool = this.pools.get(databaseName);\n if (pool) {\n await pool.end();\n this.pools.delete(databaseName);\n this.drizzleInstances.delete(databaseName);\n }\n }\n\n /** Check if a pool exists for a given database name. */\n public hasPool(databaseName: string): boolean {\n return this.pools.has(databaseName);\n }\n\n /**\n * Does this server serve exactly one database?\n *\n * The managed development database is PGlite, and PGlite serves exactly\n * one. `CREATE DATABASE \"rb_feature\" TEMPLATE \"postgres\"` there writes a\n * `pg_database` catalogue row and nothing else: nothing errors, the row is\n * listable, `pg_database_size` answers — and connecting to the \"branch\"\n * lands you in the parent. Every write made in the belief that it is\n * sandboxed goes to the developer's real development database.\n *\n * The CLI already refuses `rebase db branch` on this ground, but it does it\n * by parsing argv, so Studio — which reaches `BranchService` over the\n * websocket — walked straight past it. The connection is the only thing\n * both callers share, so the answer belongs here.\n *\n * Measured, not assumed: `select version()` over the PGlite socket returns\n * `PostgreSQL 18.3 (PGlite 0.5.6) on wasm32-…`, which is how a wasm build\n * announces itself and what `dev-db/constraints.ts` documents. A real\n * Postgres never carries `(PGlite `.\n *\n * Cached, because a live connection cannot change engine underneath us, and\n * a `PostgresBackendDriver` — which builds a `BranchService` — is\n * constructed per transaction.\n */\n public servesOneDatabase(): Promise<boolean> {\n this.oneDatabaseProbe ??= this.probeOneDatabase();\n return this.oneDatabaseProbe;\n }\n\n private oneDatabaseProbe?: Promise<boolean>;\n\n private async probeOneDatabase(): Promise<boolean> {\n try {\n const result = await this.getPool(this.defaultDatabaseName)\n .query<{ version: string }>(\"SELECT version() AS version\");\n return /\\(PGlite\\b/i.test(result.rows[0]?.version ?? \"\");\n } catch (err) {\n // Fail open, and do not cache the failure. The cost of a wrong\n // \"yes\" is refusing branching on a real Postgres that momentarily\n // could not answer; the cost of a wrong \"no\" is one fake branch,\n // which is what this exists to prevent — but a server that cannot\n // run `select version()` is not one we can conclude anything about.\n this.oneDatabaseProbe = undefined;\n logger.warn(\"[DatabasePoolManager] Could not read the server version\", { error: err });\n return false;\n }\n }\n\n public async shutdown(): Promise<void> {\n const promises = [];\n for (const [dbName, pool] of this.pools.entries()) {\n // Debug: every CLI command that reaches a database ends here, and\n // the reader of `rebase db branch create` did not ask about pools.\n logger.debug(`[DatabasePoolManager] Shutting down pool for ${dbName}`);\n promises.push(pool.end());\n }\n await Promise.all(promises);\n this.pools.clear();\n this.drizzleInstances.clear();\n }\n}\n"],"mappings":";;;;;;;;;AAMA,IAAa,sBAAb,MAAiC;CAC7B,wBAAmC,IAAI,IAAI;CAC3C,mCAAwD,IAAI,IAAI;CAChE;CACA;CAEA,YAAY,uBAA+B;EACvC,KAAK,uBAAuB;EAC5B,IAAI;GACA,MAAM,MAAM,IAAI,IAAI,qBAAqB;GACzC,KAAK,sBAAsB,IAAI,SAAS,MAAM,CAAC;EACnD,SAAS,GAAG;GACR,MAAM,IAAI,MAAM,2CAA2C,GAAG;EAClE;CACJ;CAEA,WAAkB,cAA6D;EAC3E,MAAM,WAAW,KAAK,iBAAiB,IAAI,YAAY;EACvD,IAAI,UACA,OAAO;EAIX,MAAM,KAAK,QADE,KAAK,QAAQ,YACP,CAAI;EACvB,KAAK,iBAAiB,IAAI,cAAc,EAAE;EAC1C,OAAO;CACX;CAEA,QAAe,cAA4B;EACvC,IAAI,KAAK,MAAM,IAAI,YAAY,GAC3B,OAAO,KAAK,MAAM,IAAI,YAAY;EAGtC,MAAM,MAAM,IAAI,IAAI,KAAK,oBAAoB;EAC7C,IAAI,WAAW,IAAI;EAEnB,MAAM,OAAO,IAAI,KAAK;GAIlB,kBAAkB,cAAc,IAAI,SAAS,CAAC;GAI9C,KAAK,cAAc,EAAE;GACrB,mBAAmB;GACnB,iBAAiB;GAGjB,GAAG,oBAAoB;EAC3B,CAAC;EAGD,KAAK,GAAG,UAAU,QAAQ;GACtB,OAAO,MAAM,gEAAgE,gBAAgB,EAAE,OAAO,IAAI,CAAC;EAC/G,CAAC;EACD,6BAA6B,MAAM,WAAW,cAAc;EAE5D,KAAK,MAAM,IAAI,cAAc,IAAI;EACjC,OAAO;CACX;;;;;;CAOA,MAAa,mBAAmB,cAAqC;EACjE,MAAM,OAAO,KAAK,MAAM,IAAI,YAAY;EACxC,IAAI,MAAM;GACN,MAAM,KAAK,IAAI;GACf,KAAK,MAAM,OAAO,YAAY;GAC9B,KAAK,iBAAiB,OAAO,YAAY;EAC7C;CACJ;;CAGA,QAAe,cAA+B;EAC1C,OAAO,KAAK,MAAM,IAAI,YAAY;CACtC;;;;;;;;;;;;;;;;;;;;;;;;;CA0BA,oBAA6C;EACzC,KAAK,qBAAqB,KAAK,iBAAiB;EAChD,OAAO,KAAK;CAChB;CAEA;CAEA,MAAc,mBAAqC;EAC/C,IAAI;GACA,MAAM,SAAS,MAAM,KAAK,QAAQ,KAAK,mBAAmB,CAAC,CACtD,MAA2B,6BAA6B;GAC7D,OAAO,cAAc,KAAK,OAAO,KAAK,EAAE,EAAE,WAAW,EAAE;EAC3D,SAAS,KAAK;GAMV,KAAK,mBAAmB,KAAA;GACxB,OAAO,KAAK,2DAA2D,EAAE,OAAO,IAAI,CAAC;GACrF,OAAO;EACX;CACJ;CAEA,MAAa,WAA0B;EACnC,MAAM,WAAW,CAAC;EAClB,KAAK,MAAM,CAAC,QAAQ,SAAS,KAAK,MAAM,QAAQ,GAAG;GAG/C,OAAO,MAAM,gDAAgD,QAAQ;GACrE,SAAS,KAAK,KAAK,IAAI,CAAC;EAC5B;EACA,MAAM,QAAQ,IAAI,QAAQ;EAC1B,KAAK,MAAM,MAAM;EACjB,KAAK,iBAAiB,MAAM;CAChC;AACJ"}
|