@rebasepro/server-postgres 0.17.3 → 0.18.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/LICENSE +0 -1
- package/README.md +31 -0
- package/dist/backup-argv.d.ts +19 -0
- package/dist/branch-argv.d.ts +28 -0
- package/dist/branch-prune.d.ts +77 -0
- package/dist/cli-collections-path.d.ts +31 -0
- package/dist/cli-errors.d.ts +123 -0
- package/dist/cli-flags.d.ts +101 -0
- package/dist/cli-helpers.d.ts +94 -10
- package/dist/collection-index-C7LopMyX.js +247 -0
- package/dist/collection-index-C7LopMyX.js.map +1 -0
- package/dist/databasePoolManager.d.ts +27 -0
- package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-CagOzEPS.js} +6 -6
- package/dist/{ensure-collection-policies-UCqgv_8c.js.map → ensure-collection-policies-CagOzEPS.js.map} +1 -1
- package/dist/{ensure-collection-tables-DgVixhX3.js → ensure-collection-tables-DbAhrjnG.js} +614 -52
- package/dist/ensure-collection-tables-DbAhrjnG.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.es.js +19307 -3059
- package/dist/index.es.js.map +1 -1
- package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-ClJoTboT.js} +53 -189
- package/dist/rls-enforcement-ClJoTboT.js.map +1 -0
- package/dist/schema/column-type-drift.d.ts +79 -0
- package/dist/schema/doctor.d.ts +42 -0
- package/dist/schema/ensure-collection-tables.d.ts +90 -0
- package/dist/schema/generate-next-step.d.ts +30 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
- package/dist/schema/generated-schema-staleness.d.ts +37 -0
- package/dist/schema/introspect-db-queries.d.ts +34 -0
- package/dist/security/rls-enforcement.d.ts +63 -2
- package/dist/services/BranchService.d.ts +86 -1
- package/dist/services/collection-helpers.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +30 -2
- package/dist/services/row-pipeline.d.ts +12 -1
- package/dist/utils/pg-error-utils.d.ts +3 -1
- package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
- package/package.json +32 -27
- package/dist/backup-service-DCk7KhhL.js +0 -9044
- package/dist/backup-service-DCk7KhhL.js.map +0 -1
- package/dist/collection-index-BRUg10H5.js +0 -5690
- package/dist/collection-index-BRUg10H5.js.map +0 -1
- package/dist/connection-GOKU3Hu5.js +0 -277
- package/dist/connection-GOKU3Hu5.js.map +0 -1
- package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
- package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
- package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
- package/dist/src-DiB5RP2Z.js +0 -541
- package/dist/src-DiB5RP2Z.js.map +0 -1
- package/dist/websocket-BZ4H5wUz.js +0 -8198
- package/dist/websocket-BZ4H5wUz.js.map +0 -1
- package/src/PostgresAdapter.ts +0 -91
- package/src/PostgresBackendDriver.ts +0 -1857
- package/src/PostgresBootstrapper.ts +0 -1157
- package/src/auth/ensure-tables.ts +0 -896
- package/src/auth/schema-version.ts +0 -260
- package/src/auth/services.ts +0 -1610
- package/src/backup/backup-cli.ts +0 -520
- package/src/backup/backup-cron.ts +0 -212
- package/src/backup/backup-logic.ts +0 -93
- package/src/backup/backup-service.ts +0 -485
- package/src/backup/index.ts +0 -12
- package/src/backup/pg-tools.ts +0 -403
- package/src/backup/retention.ts +0 -86
- package/src/cli-errors.ts +0 -265
- package/src/cli-helpers.ts +0 -569
- package/src/cli-output.ts +0 -43
- package/src/cli.ts +0 -1339
- package/src/collections/PostgresCollectionRegistry.ts +0 -108
- package/src/collections/buildRegistry.ts +0 -68
- package/src/collections/validate-relations.ts +0 -402
- package/src/connection.ts +0 -333
- package/src/data-transformer.ts +0 -845
- package/src/databasePoolManager.ts +0 -95
- package/src/history/HistoryService.ts +0 -254
- package/src/history/ensure-history-table.ts +0 -53
- package/src/index.ts +0 -19
- package/src/interfaces.ts +0 -60
- package/src/module-dir.ts +0 -7
- package/src/schema/atlas-argv.ts +0 -94
- package/src/schema/auth-schema.ts +0 -298
- package/src/schema/auth-users-columns.ts +0 -131
- package/src/schema/carved-out-migration.ts +0 -404
- package/src/schema/classify-change.ts +0 -436
- package/src/schema/collection-index.ts +0 -427
- package/src/schema/collections-schema-version.ts +0 -103
- package/src/schema/destructive-sql.ts +0 -94
- package/src/schema/doctor-cli.ts +0 -66
- package/src/schema/doctor-policy-checks.ts +0 -105
- package/src/schema/doctor.ts +0 -837
- package/src/schema/drizzle-ddl.ts +0 -33
- package/src/schema/dynamic-tables.test.ts +0 -302
- package/src/schema/dynamic-tables.ts +0 -293
- package/src/schema/ensure-collection-policies.ts +0 -198
- package/src/schema/ensure-collection-tables.test.ts +0 -518
- package/src/schema/ensure-collection-tables.ts +0 -1267
- package/src/schema/generate-drizzle-schema-logic.ts +0 -957
- package/src/schema/generate-drizzle-schema.ts +0 -143
- package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
- package/src/schema/generate-postgres-ddl.ts +0 -180
- package/src/schema/generate-schema-commit.ts +0 -267
- package/src/schema/generated-schema-staleness.ts +0 -284
- package/src/schema/introspect-db-constraints.ts +0 -385
- package/src/schema/introspect-db-inference.ts +0 -255
- package/src/schema/introspect-db-logic.ts +0 -1387
- package/src/schema/introspect-db-naming.ts +0 -15
- package/src/schema/introspect-db-project.ts +0 -78
- package/src/schema/introspect-db-queries.ts +0 -326
- package/src/schema/introspect-db-structure.ts +0 -670
- package/src/schema/introspect-db-types.ts +0 -56
- package/src/schema/introspect-db.ts +0 -257
- package/src/schema/introspect-runtime.test.ts +0 -260
- package/src/schema/introspect-runtime.ts +0 -327
- package/src/schema/non-sql-collections.test.ts +0 -131
- package/src/schema/rls-bootstrap-sql.ts +0 -288
- package/src/schema/search-column.ts +0 -643
- package/src/schema/test-schema.ts +0 -11
- package/src/schema/vector-index.ts +0 -491
- package/src/security/anonymous-grants.test.ts +0 -73
- package/src/security/policy-drift.test.ts +0 -435
- package/src/security/policy-drift.ts +0 -473
- package/src/security/rls-enforcement.ts +0 -505
- package/src/services/BranchService.ts +0 -326
- package/src/services/FetchService.ts +0 -2115
- package/src/services/PersistService.ts +0 -557
- package/src/services/RelationService.ts +0 -978
- package/src/services/RelationWriteService.ts +0 -653
- package/src/services/cdc/CdcListener.ts +0 -103
- package/src/services/cdc/junction-tables.ts +0 -91
- package/src/services/cdc/trigger-cdc.ts +0 -173
- package/src/services/channel-bus/ChannelBus.ts +0 -44
- package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
- package/src/services/channel-bus/index.ts +0 -114
- package/src/services/channel-history.ts +0 -424
- package/src/services/channel-presence.ts +0 -185
- package/src/services/collection-helpers.ts +0 -420
- package/src/services/dataService.ts +0 -256
- package/src/services/index.ts +0 -14
- package/src/services/junction-writes.ts +0 -295
- package/src/services/nested-path.ts +0 -145
- package/src/services/pg-notify-listener.ts +0 -151
- package/src/services/realtimeService.ts +0 -2545
- package/src/services/row-pipeline.ts +0 -240
- package/src/services/write-denial.ts +0 -55
- package/src/types.ts +0 -4
- package/src/utils/connection-string.ts +0 -58
- package/src/utils/drizzle-conditions.ts +0 -2743
- package/src/utils/pg-array-null-patch.ts +0 -42
- package/src/utils/pg-error-utils.ts +0 -391
- package/src/websocket.ts +0 -855
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The line `rebase schema generate` ends on.
|
|
3
|
+
*
|
|
4
|
+
* Its own module because `generate-drizzle-schema.ts` uses `import.meta` to
|
|
5
|
+
* decide whether it was executed directly, which the driver's jest suite cannot
|
|
6
|
+
* load at all — so the one sentence in that file with a decision in it had no
|
|
7
|
+
* test and could not have one where it stood.
|
|
8
|
+
*/
|
|
9
|
+
export declare const formatTerminalText: (text: string, options?: {
|
|
10
|
+
bold?: boolean;
|
|
11
|
+
backgroundColor?: "blue" | "green" | "red" | "yellow" | "cyan" | "magenta";
|
|
12
|
+
textColor?: "white" | "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan";
|
|
13
|
+
}) => string;
|
|
14
|
+
/**
|
|
15
|
+
* What to do with the file that was just generated.
|
|
16
|
+
*
|
|
17
|
+
* Not the same answer on the managed development database: `rebase db generate`
|
|
18
|
+
* plans with Atlas, Atlas needs a second empty database to diff against, and
|
|
19
|
+
* PGlite serves exactly one — so the CLI refuses that command outright there.
|
|
20
|
+
* This line recommended it unconditionally, which meant every stock scaffold
|
|
21
|
+
* was told, by the tool itself, to run something the same tool would refuse.
|
|
22
|
+
*
|
|
23
|
+
* The kind arrives as `REBASE_DEV_DATABASE_KIND`, set by the CLI from the
|
|
24
|
+
* ordered resolution in `dev-db/resolve.ts`. It is a pure decision — nothing is
|
|
25
|
+
* started to make it — and the driver cannot make it for itself: on the managed
|
|
26
|
+
* path `DATABASE_URL` is a perfectly ordinary connection string to a Postgres
|
|
27
|
+
* on loopback. Absent (the driver run directly, outside the CLI) keeps the
|
|
28
|
+
* original wording.
|
|
29
|
+
*/
|
|
30
|
+
export declare const nextStepAfterGenerate: () => string;
|
|
@@ -9,6 +9,34 @@ export declare const isNumericId: (collection: CollectionConfig) => boolean;
|
|
|
9
9
|
export declare const getPrimaryKeyName: (collection: CollectionConfig) => string;
|
|
10
10
|
export declare const isIdProperty: (propName: string, prop: Property, collection: CollectionConfig) => boolean;
|
|
11
11
|
type ResolveCollection = (slug: string) => CollectionConfig | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* The `ON DELETE` a foreign key gets when the author did not say.
|
|
14
|
+
*
|
|
15
|
+
* One rule for every property that emits one — `belongsTo` relations and
|
|
16
|
+
* `reference` properties alike. `reference` kept its own `CASCADE` default for
|
|
17
|
+
* a release after this rule was written, which is exactly the split this
|
|
18
|
+
* function exists to prevent: two spellings of the same link, two data-retention
|
|
19
|
+
* behaviours, and only one of them documented.
|
|
20
|
+
*
|
|
21
|
+
* An optional link is `SET NULL`: the column can hold NULL, so dropping the
|
|
22
|
+
* parent leaves the child row with an empty pointer, which is what "optional"
|
|
23
|
+
* already means.
|
|
24
|
+
*
|
|
25
|
+
* A required link is **`RESTRICT`**, not `CASCADE`. `NOT NULL` says the child
|
|
26
|
+
* cannot exist without a parent; it does not say deleting the parent should
|
|
27
|
+
* take the child with it. That second claim is a data-retention decision, and
|
|
28
|
+
* defaulting to it meant `onDelete` — a field nobody has to write — silently
|
|
29
|
+
* turned every `DELETE FROM authors` into a cascade through posts, comments and
|
|
30
|
+
* anything else that hung off them. `RESTRICT` fails the delete instead and
|
|
31
|
+
* names the constraint, so the author chooses `cascade` on purpose or clears
|
|
32
|
+
* the children first.
|
|
33
|
+
*
|
|
34
|
+
* Both DDL paths — the `CREATE TABLE` generator and {@link planRelationalColumns},
|
|
35
|
+
* which boot-ensure compares the live database against — call this, so a
|
|
36
|
+
* default that appears in one and not the other cannot make `db push` plan a
|
|
37
|
+
* constraint rewrite on every run.
|
|
38
|
+
*/
|
|
39
|
+
export declare const defaultBelongsToOnDelete: (required: boolean | undefined) => "RESTRICT" | "SET NULL";
|
|
12
40
|
/**
|
|
13
41
|
* The individual SQL statements a single security rule compiles to: a
|
|
14
42
|
* `DROP POLICY IF EXISTS` / `CREATE POLICY` pair per operation, each a complete
|
|
@@ -56,3 +56,40 @@ export interface MissingGeneratedName {
|
|
|
56
56
|
export declare function findMissingGeneratedNames(generatedSource: string, collections: CollectionConfig[]): MissingGeneratedName[];
|
|
57
57
|
/** One-line summary for a log or a CLI notice. */
|
|
58
58
|
export declare function describeMissingGeneratedNames(missing: MissingGeneratedName[]): string;
|
|
59
|
+
/** What `rebase schema stale` found, and what that costs. */
|
|
60
|
+
export interface StaleVerdict {
|
|
61
|
+
/** The lines to print, in order. Empty only under `--fix` with nothing to fix. */
|
|
62
|
+
lines: string[];
|
|
63
|
+
/** Non-zero only when the reader must act before the server will boot. */
|
|
64
|
+
exitCode: 0 | 1;
|
|
65
|
+
/** True when `--fix` should regenerate. */
|
|
66
|
+
regenerate: boolean;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The whole of `schema stale`'s decision, separated from doing it.
|
|
70
|
+
*
|
|
71
|
+
* Separated because of what the command used to do on the clean path: **exit 0
|
|
72
|
+
* having written zero bytes to stdout and zero to stderr.** For a command the
|
|
73
|
+
* top-level help describes as "Report generated schema files the collections
|
|
74
|
+
* have moved past", that is indistinguishable from a no-op, a crash, and a
|
|
75
|
+
* subcommand the driver does not implement. Three of its four paths were
|
|
76
|
+
* silent, and none of them could be tested where they stood: `cli.ts` uses
|
|
77
|
+
* `import.meta`, which the driver's jest suite cannot load at all.
|
|
78
|
+
*
|
|
79
|
+
* `--fix` is the exception, and stays silent when there is nothing to fix.
|
|
80
|
+
* `rebase dev` runs `schema stale --fix` before every boot with inherited
|
|
81
|
+
* stdio, and "nothing was wrong" is not news a hundred lines into a start-up
|
|
82
|
+
* transcript. The flag is the automated caller's; its silence is the point.
|
|
83
|
+
*/
|
|
84
|
+
export declare function staleVerdict(input: {
|
|
85
|
+
/** `--output`, as the reader typed it. */
|
|
86
|
+
outputPath: string;
|
|
87
|
+
/** Whether the generated schema exists at all. */
|
|
88
|
+
generatedExists: boolean;
|
|
89
|
+
/** The legacy foreign-key names found in it, if it was read. */
|
|
90
|
+
stale: LegacyForeignKeyName[];
|
|
91
|
+
/** Why the comparison could not be made, when it could not. */
|
|
92
|
+
unreadable?: string;
|
|
93
|
+
/** Whether `--fix` was given. */
|
|
94
|
+
fix: boolean;
|
|
95
|
+
}): StaleVerdict;
|
|
@@ -16,6 +16,40 @@ export interface QueryableClient {
|
|
|
16
16
|
rows: R[];
|
|
17
17
|
}>;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* A Postgres identifier, quoted so that whatever is inside it stays inside it.
|
|
21
|
+
*
|
|
22
|
+
* Introspection reads table and schema names out of `pg_class` and puts them
|
|
23
|
+
* straight back into SQL, and wrapping a name in double quotes is not the same
|
|
24
|
+
* as escaping it: a table named `x" ; DROP TABLE users; --` closes the quote
|
|
25
|
+
* and then the statement. Postgres's own escape for a quote inside a quoted
|
|
26
|
+
* identifier is to double it, which is what this does.
|
|
27
|
+
*
|
|
28
|
+
* "The catalogue is trusted" is true of your own database, and is exactly the
|
|
29
|
+
* assumption this tool cannot make. Introspection exists to be pointed at
|
|
30
|
+
* databases somebody else built, and it runs with the privileges of whoever
|
|
31
|
+
* ran it — which is usually a superuser, because that is who has a connection
|
|
32
|
+
* string to a database they are adopting.
|
|
33
|
+
*/
|
|
34
|
+
export declare function quoteIdentifier(name: string): string;
|
|
35
|
+
/** `"schema"."table"`, both halves quoted. */
|
|
36
|
+
export declare function quoteRelation(schema: string, table: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Can this table's name be the name of the file describing it?
|
|
39
|
+
*
|
|
40
|
+
* Introspection writes `<collectionsDir>/<tableName>.ts`, and the table name
|
|
41
|
+
* came from a database this tool did not create. Postgres will hold a table
|
|
42
|
+
* called `../../../etc/cron.d/x`, and `path.join` resolves it — so without this
|
|
43
|
+
* the generator writes a file wherever the name points, as whoever ran the
|
|
44
|
+
* command.
|
|
45
|
+
*
|
|
46
|
+
* Deliberately narrow, and deliberately not a blocklist of `..` and `/`: the
|
|
47
|
+
* question is "is this a plain file name?", and every answer that is not
|
|
48
|
+
* obviously yes is a table better written by hand. NUL and the Windows
|
|
49
|
+
* separator are in scope because a collections directory is checked out on
|
|
50
|
+
* whatever machine the developer has.
|
|
51
|
+
*/
|
|
52
|
+
export declare function isUsableFileName(name: string): boolean;
|
|
19
53
|
/**
|
|
20
54
|
* Base tables, excluding partitions.
|
|
21
55
|
*
|
|
@@ -57,8 +57,24 @@ export interface ConnectionPosture {
|
|
|
57
57
|
role: string;
|
|
58
58
|
superuser: boolean;
|
|
59
59
|
bypassRLS: boolean;
|
|
60
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Owns at least one user table — owners bypass non-FORCE RLS.
|
|
62
|
+
*
|
|
63
|
+
* "Owns" in Postgres's sense, not by name: a role that INHERITs the owner is
|
|
64
|
+
* the owner for every ownership test the planner makes, including the one
|
|
65
|
+
* that decides whether a policy applies at all.
|
|
66
|
+
*/
|
|
61
67
|
ownsTables: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* May create tables somewhere the server writes — and would then own them.
|
|
70
|
+
*
|
|
71
|
+
* The reason this is part of the posture rather than a separate question:
|
|
72
|
+
* the posture is decided ONCE, at boot, and on a fresh database the
|
|
73
|
+
* catalogue has nothing to report yet. The process that asks then goes on to
|
|
74
|
+
* create every table in the schema and owns all of them for the rest of its
|
|
75
|
+
* life. Asking only what is already there answers about the wrong instant.
|
|
76
|
+
*/
|
|
77
|
+
canCreateTables: boolean;
|
|
62
78
|
/** True when RLS would NOT constrain this connection. */
|
|
63
79
|
privileged: boolean;
|
|
64
80
|
}
|
|
@@ -66,6 +82,21 @@ export interface AuthContext {
|
|
|
66
82
|
uid: string;
|
|
67
83
|
/** Raw roles as carried on the user (strings or `{ id }` objects). */
|
|
68
84
|
roles: unknown[];
|
|
85
|
+
/**
|
|
86
|
+
* Whether this session came from anonymous sign-in rather than an account.
|
|
87
|
+
*
|
|
88
|
+
* Anonymous sign-in mints a real user row and a real uid, so a guest was
|
|
89
|
+
* indistinguishable from a registered account inside a policy: same
|
|
90
|
+
* `rebase.uid()`, same default role. Every rule meaning "a signed-in
|
|
91
|
+
* person" was therefore also a rule about anybody who had called
|
|
92
|
+
* `POST /auth/anonymous`, which asks for no email, no password and no
|
|
93
|
+
* agreement to anything.
|
|
94
|
+
*
|
|
95
|
+
* Optional, and absent reads as `false`: a caller that predates this — a
|
|
96
|
+
* realtime subscription, a custom validator — keeps the behaviour it had
|
|
97
|
+
* rather than having every one of its users reclassified as guests.
|
|
98
|
+
*/
|
|
99
|
+
isAnonymous?: boolean;
|
|
69
100
|
}
|
|
70
101
|
/**
|
|
71
102
|
* Warn when the connection role shares its name with an existing schema.
|
|
@@ -95,7 +126,37 @@ export interface AuthContext {
|
|
|
95
126
|
* inherited would be worse than the risk.
|
|
96
127
|
*/
|
|
97
128
|
export declare function warnOnRoleSchemaCollision(run: RawSqlRunner): Promise<void>;
|
|
98
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Would RLS actually constrain this connection?
|
|
131
|
+
*
|
|
132
|
+
* Four ways the answer is no, and only two of them are obvious. Superuser and
|
|
133
|
+
* BYPASSRLS announce themselves in `pg_roles`. The other two are about
|
|
134
|
+
* ownership, and both used to read as "no bypass here":
|
|
135
|
+
*
|
|
136
|
+
* - **Ownership through membership.** The old query compared
|
|
137
|
+
* `pg_tables.tableowner` to `current_user` as a string. Postgres does not: a
|
|
138
|
+
* role that INHERITs the owner passes every ownership test the planner makes,
|
|
139
|
+
* including the one that decides whether a policy applies. So a perfectly
|
|
140
|
+
* ordinary "app role that is a member of the owner role" setup reported
|
|
141
|
+
* unprivileged and bypassed every policy — permanently, since no restart
|
|
142
|
+
* changes a string comparison's answer.
|
|
143
|
+
*
|
|
144
|
+
* - **Ownership that has not happened yet.** The posture is decided once, at
|
|
145
|
+
* boot, and on a fresh database there is nothing in the catalogue to find. The
|
|
146
|
+
* same process then creates every table and owns all of them. The first boot
|
|
147
|
+
* of a new deployment therefore served every request unconstrained, the log
|
|
148
|
+
* said RLS was fine, and the SECOND boot silently fixed it — which is the
|
|
149
|
+
* worst possible shape for a security check, because the state that reproduces
|
|
150
|
+
* it is gone by the time anyone looks. Asking "may this role create tables
|
|
151
|
+
* where the server provisions?" answers about the process's whole life rather
|
|
152
|
+
* than about one instant of it.
|
|
153
|
+
*
|
|
154
|
+
* `schemas` narrows the CREATE question to where the server actually writes.
|
|
155
|
+
* Omitted, it asks about every schema the role can see, which is the safe
|
|
156
|
+
* direction: over-reporting costs a role switch that was not strictly needed,
|
|
157
|
+
* under-reporting costs the entire authorization model.
|
|
158
|
+
*/
|
|
159
|
+
export declare function detectConnectionPosture(run: RawSqlRunner, schemas?: string[]): Promise<ConnectionPosture>;
|
|
99
160
|
/**
|
|
100
161
|
* Human-actionable instructions for when the connection cannot provision the
|
|
101
162
|
* user role itself (no CREATEROLE and role not pre-created by the platform).
|
|
@@ -8,11 +8,38 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { BranchInfo } from "@rebasepro/types";
|
|
10
10
|
import { DrizzleClient } from "../interfaces.js";
|
|
11
|
+
import { BranchRow } from "../branch-prune.js";
|
|
11
12
|
import { DatabasePoolManager } from "../databasePoolManager.js";
|
|
13
|
+
/**
|
|
14
|
+
* The connected server cannot branch, so nothing was done.
|
|
15
|
+
*
|
|
16
|
+
* A refusal, not a fault: typed so the transports can forward it intact.
|
|
17
|
+
* `websocket.ts` answers it with its own code and message rather than letting
|
|
18
|
+
* it fall into the generic handler, which suppresses the text in production —
|
|
19
|
+
* and the text is the only part that says what to do instead.
|
|
20
|
+
*/
|
|
21
|
+
export declare class BranchingUnsupportedError extends Error {
|
|
22
|
+
/** Wire code, forwarded verbatim to the client. */
|
|
23
|
+
readonly code = "BRANCHING_UNSUPPORTED";
|
|
24
|
+
constructor(message: string);
|
|
25
|
+
}
|
|
12
26
|
export declare class BranchService {
|
|
13
27
|
private db;
|
|
14
28
|
private poolManager;
|
|
15
29
|
constructor(db: DrizzleClient, poolManager: DatabasePoolManager);
|
|
30
|
+
/**
|
|
31
|
+
* Refuse a branch mutation the connected server cannot honour.
|
|
32
|
+
*
|
|
33
|
+
* The check lives here rather than in the CLI's argv parsing because the
|
|
34
|
+
* CLI is not the only caller: Studio reaches `createBranch` over the
|
|
35
|
+
* websocket, and before this it got "Branch created successfully" and a
|
|
36
|
+
* detail pane promising isolation for a database that is the parent.
|
|
37
|
+
*
|
|
38
|
+
* Reads only, and only mutations are guarded: `listBranches` on a managed
|
|
39
|
+
* database is an honest empty list, and refusing it would turn the Branches
|
|
40
|
+
* pane into a failure with a "Try again" that can never succeed.
|
|
41
|
+
*/
|
|
42
|
+
private assertBranchingSupported;
|
|
16
43
|
/**
|
|
17
44
|
* Ensure the `rebase.branches` metadata table exists in the default database.
|
|
18
45
|
* Idempotent — safe to call on every startup.
|
|
@@ -29,12 +56,70 @@ export declare class BranchService {
|
|
|
29
56
|
*/
|
|
30
57
|
createBranch(name: string, options?: {
|
|
31
58
|
source?: string;
|
|
59
|
+
force?: boolean;
|
|
32
60
|
}): Promise<BranchInfo>;
|
|
33
61
|
/**
|
|
34
62
|
* Delete a branch database and remove its metadata.
|
|
35
63
|
* Cannot delete the main/default database.
|
|
36
64
|
*/
|
|
37
|
-
deleteBranch(name: string
|
|
65
|
+
deleteBranch(name: string, options?: {
|
|
66
|
+
force?: boolean;
|
|
67
|
+
}): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Who is holding the database open, in words a developer can act on.
|
|
70
|
+
*
|
|
71
|
+
* `CREATE DATABASE ... TEMPLATE` and `DROP DATABASE` both require that no
|
|
72
|
+
* other session is connected, and the one thing the old message could not
|
|
73
|
+
* say is the only thing worth knowing: *which* session. "Close other
|
|
74
|
+
* clients or connections and try again" is advice you cannot follow when
|
|
75
|
+
* you do not know there is a `rebase dev` in another terminal — and that is
|
|
76
|
+
* the common case, because wanting a branch and running the app are the
|
|
77
|
+
* same moment.
|
|
78
|
+
*
|
|
79
|
+
* `application_name` is the useful column: node-postgres sends none by
|
|
80
|
+
* default, so a Rebase process shows as `(unnamed)` while DBeaver, pgAdmin
|
|
81
|
+
* and psql all name themselves. Counting by it separates "my own dev
|
|
82
|
+
* server" from "the GUI I forgot about" without any guessing.
|
|
83
|
+
*
|
|
84
|
+
* Best-effort by construction: this runs while reporting a failure, and a
|
|
85
|
+
* diagnostic that throws would replace a real error with its own.
|
|
86
|
+
*/
|
|
87
|
+
private describeBlockingConnections;
|
|
88
|
+
/**
|
|
89
|
+
* Disconnect every other session on a database, and say how many.
|
|
90
|
+
*
|
|
91
|
+
* `pg_terminate_backend` rather than `pg_cancel_backend`: cancelling a
|
|
92
|
+
* query leaves the session connected, and a connected session is exactly
|
|
93
|
+
* what blocks the operation.
|
|
94
|
+
*
|
|
95
|
+
* The current backend is excluded — terminating the connection running this
|
|
96
|
+
* statement would abort the command that asked for it.
|
|
97
|
+
*/
|
|
98
|
+
private terminateConnections;
|
|
99
|
+
/**
|
|
100
|
+
* Everything prune needs to decide, in two queries.
|
|
101
|
+
*
|
|
102
|
+
* Reads the rows and the server's database list rather than joining them,
|
|
103
|
+
* so the *disagreements* between the two are visible — a row whose database
|
|
104
|
+
* is gone and a database with no row are the two things prune exists to
|
|
105
|
+
* find, and a join hides both.
|
|
106
|
+
*/
|
|
107
|
+
pruneCandidates(): Promise<{
|
|
108
|
+
rows: BranchRow[];
|
|
109
|
+
databases: string[];
|
|
110
|
+
}>;
|
|
111
|
+
/** Remove a metadata row whose database is already gone. */
|
|
112
|
+
forgetBranchRow(name: string): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Drop a database by name, with no metadata row required.
|
|
115
|
+
*
|
|
116
|
+
* `deleteBranch` deliberately takes its database name from the row, because
|
|
117
|
+
* the row is the only value that is true by construction. Prune has the
|
|
118
|
+
* opposite job — the cases it handles are exactly the ones where row and
|
|
119
|
+
* database disagree — so it needs to name a database directly. Guarded the
|
|
120
|
+
* same way regardless: the main database is never droppable.
|
|
121
|
+
*/
|
|
122
|
+
dropDatabase(dbName: string): Promise<void>;
|
|
38
123
|
/**
|
|
39
124
|
* List all branches recorded in the metadata table.
|
|
40
125
|
* Optionally fetches database sizes from pg_database.
|
|
@@ -51,7 +51,7 @@ export declare function getCollectionByPath(collectionPath: string, registry: Po
|
|
|
51
51
|
* thing against the *config* and is skipped on four paths — `strictWrites:
|
|
52
52
|
* false`, a collection declaring no properties, an auth adapter that owns the
|
|
53
53
|
* body's shape, and a nested route whose target cannot be walked — and it never
|
|
54
|
-
* sees an in-process `rebase.
|
|
54
|
+
* sees an in-process `rebase.dataAsAdmin` write at all.
|
|
55
55
|
*
|
|
56
56
|
* It also gives `strictWrites: false` a truthful implementation. The flag is
|
|
57
57
|
* documented for "a column that really does exist which the config never
|
|
@@ -13,6 +13,14 @@ import type { ChannelRetentionRule } from "@rebasepro/types";
|
|
|
13
13
|
export interface SubscriptionAuthContext {
|
|
14
14
|
uid: string;
|
|
15
15
|
roles: string[];
|
|
16
|
+
/**
|
|
17
|
+
* Whether this session is a guest — anonymous sign-in rather than an
|
|
18
|
+
* account. Carried so a refetch's policies see the same principal the
|
|
19
|
+
* initial fetch did; without it a rule that excludes guests would filter on
|
|
20
|
+
* the REST read and not on the frames that follow, which is the shape that
|
|
21
|
+
* makes a realtime leak invisible from the surface people test.
|
|
22
|
+
*/
|
|
23
|
+
isAnonymous?: boolean;
|
|
16
24
|
}
|
|
17
25
|
/** What a channel frame is asking to do. */
|
|
18
26
|
export type ChannelAction = "join" | "broadcast" | "presence" | "history";
|
|
@@ -225,8 +233,15 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
225
233
|
* would leak rows the initial (isolated) fetch correctly hid.
|
|
226
234
|
*/
|
|
227
235
|
rlsUserRole?: string;
|
|
228
|
-
/**
|
|
229
|
-
|
|
236
|
+
/**
|
|
237
|
+
* Verbose subscription tracing.
|
|
238
|
+
*
|
|
239
|
+
* Through the logger at `debug`, behind the same switch that lifts the
|
|
240
|
+
* `Failed query:` redaction: these lines quote paths, filters and refetch
|
|
241
|
+
* SQL, and a `console.debug` gated on `NODE_ENV` alone wrote them to stdout
|
|
242
|
+
* whatever `LOG_LEVEL` said, without passing through the one function where
|
|
243
|
+
* redaction lives.
|
|
244
|
+
*/
|
|
230
245
|
private debugLog;
|
|
231
246
|
setDataDriver(driver: DataDriver): void;
|
|
232
247
|
get subscriptions(): Map<string, Subscription>;
|
|
@@ -366,6 +381,19 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
366
381
|
*/
|
|
367
382
|
/** The key columns of the collection at `path`, if they can be resolved. */
|
|
368
383
|
private primaryKeysForPath;
|
|
384
|
+
/**
|
|
385
|
+
* `channel` addresses the error to the channel frame it is about.
|
|
386
|
+
*
|
|
387
|
+
* Without it the client had nowhere to deliver a channel error: channel
|
|
388
|
+
* frames are fire-and-forget, so there is no pending request to reject and
|
|
389
|
+
* no subscription id to match, and `CHANNEL_FORBIDDEN`, `RATE_LIMITED` and
|
|
390
|
+
* the two history failures fell through every branch of the client's
|
|
391
|
+
* message handler into a console warning. The client already routes
|
|
392
|
+
* channel-addressed frames by name — `onChannelMessage(channel, …)` — so
|
|
393
|
+
* naming the channel is all that was missing.
|
|
394
|
+
*
|
|
395
|
+
* Additive on the wire: a client that does not read it behaves as before.
|
|
396
|
+
*/
|
|
369
397
|
private sendError;
|
|
370
398
|
private sendMessage;
|
|
371
399
|
/**
|
|
@@ -12,7 +12,7 @@ import { PostgresCollectionRegistry } from "../collections/PostgresCollectionReg
|
|
|
12
12
|
* target's values. This is what the admin renders.
|
|
13
13
|
* - `"inline"` — the target's own columns, flat. This is what REST serves, and
|
|
14
14
|
* — since the in-process SDK reads through the same pipeline — what
|
|
15
|
-
* `rebase.
|
|
15
|
+
* `rebase.dataAsAdmin` / `context.data` serve too. A developer never sees a ref.
|
|
16
16
|
*
|
|
17
17
|
* They used to be two functions that happened to agree, and the agreement was
|
|
18
18
|
* not enforced by anything: the row-identity bug had to be fixed five times
|
|
@@ -29,6 +29,17 @@ export type RelationStyle = "ref" | "inline";
|
|
|
29
29
|
* to unwrap that same level back out.
|
|
30
30
|
*/
|
|
31
31
|
export declare function isJunctionRelation(relation: ResolvedRelation): boolean;
|
|
32
|
+
/** Render one target row in the requested style. */
|
|
33
|
+
/**
|
|
34
|
+
* Drop every column the collection marked `excludeFromApi`.
|
|
35
|
+
*
|
|
36
|
+
* Password hashes and verification tokens have to be readable server-side but
|
|
37
|
+
* must never reach a client — and "never" has to mean every exit from this
|
|
38
|
+
* pipeline, including relation targets, or a secret leaks through whichever
|
|
39
|
+
* path was overlooked. Keyed by both the property name and its column name,
|
|
40
|
+
* since a row can arrive keyed either way depending on the caller.
|
|
41
|
+
*/
|
|
42
|
+
export declare function stripExcluded(row: Record<string, unknown>, collection: CollectionConfig): Record<string, unknown>;
|
|
32
43
|
/**
|
|
33
44
|
* The address a relation ref points at.
|
|
34
45
|
*
|
|
@@ -107,7 +107,9 @@ export declare function isRowLevelSecurityDenial(error: unknown): boolean;
|
|
|
107
107
|
* @param context - A human-readable context string (e.g. collection slug or path)
|
|
108
108
|
* @returns An object with a `message` safe for the client and the PG `code`.
|
|
109
109
|
*/
|
|
110
|
-
export declare function pgErrorToFriendlyMessage(pgError: PostgresError, context: string
|
|
110
|
+
export declare function pgErrorToFriendlyMessage(pgError: PostgresError, context: string, options?: {
|
|
111
|
+
verbose?: boolean;
|
|
112
|
+
}): {
|
|
111
113
|
message: string;
|
|
112
114
|
code: string;
|
|
113
115
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serve NUMERIC columns as JavaScript numbers.
|
|
3
|
+
*
|
|
4
|
+
* A property declared `type: "number"` without `validation.integer` becomes a
|
|
5
|
+
* Postgres `numeric` column (see `generate-drizzle-schema-logic`). Postgres
|
|
6
|
+
* sends `numeric` as text, node-postgres registers no parser for OID 1700, and
|
|
7
|
+
* drizzle's `PgNumeric.mapFromDriverValue` keeps the string — so a price
|
|
8
|
+
* written as `2.5` reads back as `"2.5"`. The generated SDK types that field
|
|
9
|
+
* `number`, the OpenAPI document this server publishes says `type: number`, and
|
|
10
|
+
* the admin's view model parses it as a number, so the string is the one shape
|
|
11
|
+
* nothing in the system claims. It also breaks *asymmetrically*: the create
|
|
12
|
+
* response carries the number and the read that follows carries the string, so
|
|
13
|
+
* a client that multiplies a price works until the first refresh.
|
|
14
|
+
*
|
|
15
|
+
* Cast here — in the driver's row mapping — rather than in one of the read
|
|
16
|
+
* paths, because there are several of them (the relational query builder, the
|
|
17
|
+
* `select` fallback, the realtime frame, the admin's view model) and only the
|
|
18
|
+
* REST renderer used to coerce. Every one of them goes through the column's
|
|
19
|
+
* `mapFromDriverValue`, so this is the single place that cannot be bypassed.
|
|
20
|
+
*
|
|
21
|
+
* The cast is safe for what a `number` property can hold: declaring a property
|
|
22
|
+
* `number` already promises double precision — that is what the admin has
|
|
23
|
+
* always parsed it to and what the generated `Row` type says. A column wider
|
|
24
|
+
* than a double (a hand-written `numeric(38, 0)` reached through an
|
|
25
|
+
* introspected collection) loses precision, which is the trade the declared
|
|
26
|
+
* type already made.
|
|
27
|
+
*
|
|
28
|
+
* `null` stays `null`; an unparseable value stays whatever the driver sent,
|
|
29
|
+
* because inventing `NaN` for it would be worse than passing it through.
|
|
30
|
+
*/
|
|
31
|
+
export declare function patchPgNumericToNumber(tables: Record<string, unknown>): void;
|
package/package.json
CHANGED
|
@@ -1,57 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server-postgres",
|
|
3
|
-
"
|
|
4
|
-
"version": "0.17.3",
|
|
3
|
+
"version": "0.18.0",
|
|
5
4
|
"description": "PostgreSQL data source backend implementation for Rebase with Drizzle ORM",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cms",
|
|
7
|
+
"admin",
|
|
8
|
+
"postgresql",
|
|
9
|
+
"database",
|
|
10
|
+
"drizzle",
|
|
11
|
+
"backend",
|
|
12
|
+
"rebase"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://rebase.pro",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/rebasepro/rebase/issues"
|
|
8
17
|
},
|
|
9
|
-
"author": "Rebase",
|
|
10
|
-
"license": "MIT",
|
|
11
18
|
"repository": {
|
|
12
19
|
"type": "git",
|
|
13
20
|
"url": "https://github.com/rebasepro/rebase.git",
|
|
14
21
|
"directory": "packages/server-postgres"
|
|
15
22
|
},
|
|
23
|
+
"funding": {
|
|
24
|
+
"url": "https://github.com/sponsors/rebasepro"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "Rebase",
|
|
28
|
+
"type": "module",
|
|
16
29
|
"main": "./dist/index.es.js",
|
|
17
30
|
"module": "./dist/index.es.js",
|
|
18
31
|
"types": "./dist/index.d.ts",
|
|
19
|
-
"source": "src/index.ts",
|
|
20
32
|
"engines": {
|
|
21
|
-
"node": ">=
|
|
33
|
+
"node": ">=22.22.0"
|
|
22
34
|
},
|
|
23
|
-
"keywords": [
|
|
24
|
-
"cms",
|
|
25
|
-
"admin",
|
|
26
|
-
"postgresql",
|
|
27
|
-
"database",
|
|
28
|
-
"drizzle",
|
|
29
|
-
"backend",
|
|
30
|
-
"rebase"
|
|
31
|
-
],
|
|
32
35
|
"exports": {
|
|
33
36
|
".": {
|
|
34
37
|
"types": "./dist/index.d.ts",
|
|
35
38
|
"development": "./dist/index.es.js",
|
|
36
|
-
"import": "./dist/index.es.js"
|
|
39
|
+
"import": "./dist/index.es.js",
|
|
40
|
+
"default": "./dist/index.es.js"
|
|
37
41
|
},
|
|
38
42
|
"./package.json": "./package.json"
|
|
39
43
|
},
|
|
40
44
|
"dependencies": {
|
|
41
45
|
"@ariga/atlas": "^1.2.3",
|
|
42
46
|
"arg": "^5.0.2",
|
|
43
|
-
"chalk": "^
|
|
47
|
+
"chalk": "^5.6.2",
|
|
44
48
|
"chokidar": "5.0.0",
|
|
45
49
|
"dotenv": "^17.4.2",
|
|
46
50
|
"drizzle-orm": "^0.45.2",
|
|
47
51
|
"execa": "^9.6.1",
|
|
48
52
|
"pg": "^8.22.0",
|
|
49
53
|
"ws": "^8.21.1",
|
|
50
|
-
"@rebasepro/codegen": "0.
|
|
51
|
-
"@rebasepro/common": "0.
|
|
52
|
-
"@rebasepro/server": "0.
|
|
53
|
-
"@rebasepro/types": "0.
|
|
54
|
-
"@rebasepro/utils": "0.
|
|
54
|
+
"@rebasepro/codegen": "0.18.0",
|
|
55
|
+
"@rebasepro/common": "0.18.0",
|
|
56
|
+
"@rebasepro/server": "0.18.0",
|
|
57
|
+
"@rebasepro/types": "0.18.0",
|
|
58
|
+
"@rebasepro/utils": "0.18.0"
|
|
55
59
|
},
|
|
56
60
|
"devDependencies": {
|
|
57
61
|
"@hono/node-server": "^2.0.12",
|
|
@@ -65,21 +69,22 @@
|
|
|
65
69
|
"ts-jest": "^29.4.12",
|
|
66
70
|
"typescript": "^6.0.3",
|
|
67
71
|
"vite": "^8.1.5",
|
|
68
|
-
"vitest": "^4.1.10"
|
|
72
|
+
"vitest": "^4.1.10",
|
|
73
|
+
"@rebasepro/client": "0.18.0"
|
|
69
74
|
},
|
|
70
75
|
"gitHead": "d935eefa5aa8d1009a2398cfac2c1e4ee9aeb6b6",
|
|
71
76
|
"publishConfig": {
|
|
72
77
|
"access": "public"
|
|
73
78
|
},
|
|
74
79
|
"files": [
|
|
75
|
-
"dist"
|
|
76
|
-
"src"
|
|
80
|
+
"dist"
|
|
77
81
|
],
|
|
78
82
|
"scripts": {
|
|
79
83
|
"watch": "vite build --watch",
|
|
80
84
|
"build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && node ../../tooling/scripts/add-dts-extensions.mjs dist && node ../../tooling/scripts/assert-build-output.mjs",
|
|
81
85
|
"test:lint": "eslint \"src/**\" --quiet",
|
|
82
86
|
"test": "jest",
|
|
87
|
+
"test:watch": "jest --watch",
|
|
83
88
|
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
84
89
|
"clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f",
|
|
85
90
|
"smoke:baas": "tsx scripts/smoke-baas.ts",
|