@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.
Files changed (149) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +31 -0
  3. package/dist/backup-argv.d.ts +19 -0
  4. package/dist/branch-argv.d.ts +28 -0
  5. package/dist/branch-prune.d.ts +77 -0
  6. package/dist/cli-collections-path.d.ts +31 -0
  7. package/dist/cli-errors.d.ts +123 -0
  8. package/dist/cli-flags.d.ts +101 -0
  9. package/dist/cli-helpers.d.ts +94 -10
  10. package/dist/collection-index-C7LopMyX.js +247 -0
  11. package/dist/collection-index-C7LopMyX.js.map +1 -0
  12. package/dist/databasePoolManager.d.ts +27 -0
  13. package/dist/{ensure-collection-policies-UCqgv_8c.js → ensure-collection-policies-CagOzEPS.js} +6 -6
  14. package/dist/{ensure-collection-policies-UCqgv_8c.js.map → ensure-collection-policies-CagOzEPS.js.map} +1 -1
  15. package/dist/{ensure-collection-tables-DgVixhX3.js → ensure-collection-tables-DbAhrjnG.js} +614 -52
  16. package/dist/ensure-collection-tables-DbAhrjnG.js.map +1 -0
  17. package/dist/index.d.ts +12 -0
  18. package/dist/index.es.js +19307 -3059
  19. package/dist/index.es.js.map +1 -1
  20. package/dist/{rls-enforcement-DvAbL9YJ.js → rls-enforcement-ClJoTboT.js} +53 -189
  21. package/dist/rls-enforcement-ClJoTboT.js.map +1 -0
  22. package/dist/schema/column-type-drift.d.ts +79 -0
  23. package/dist/schema/doctor.d.ts +42 -0
  24. package/dist/schema/ensure-collection-tables.d.ts +90 -0
  25. package/dist/schema/generate-next-step.d.ts +30 -0
  26. package/dist/schema/generate-postgres-ddl-logic.d.ts +28 -0
  27. package/dist/schema/generated-schema-staleness.d.ts +37 -0
  28. package/dist/schema/introspect-db-queries.d.ts +34 -0
  29. package/dist/security/rls-enforcement.d.ts +63 -2
  30. package/dist/services/BranchService.d.ts +86 -1
  31. package/dist/services/collection-helpers.d.ts +1 -1
  32. package/dist/services/realtimeService.d.ts +30 -2
  33. package/dist/services/row-pipeline.d.ts +12 -1
  34. package/dist/utils/pg-error-utils.d.ts +3 -1
  35. package/dist/utils/pg-numeric-number-patch.d.ts +31 -0
  36. package/package.json +32 -27
  37. package/dist/backup-service-DCk7KhhL.js +0 -9044
  38. package/dist/backup-service-DCk7KhhL.js.map +0 -1
  39. package/dist/collection-index-BRUg10H5.js +0 -5690
  40. package/dist/collection-index-BRUg10H5.js.map +0 -1
  41. package/dist/connection-GOKU3Hu5.js +0 -277
  42. package/dist/connection-GOKU3Hu5.js.map +0 -1
  43. package/dist/ensure-collection-tables-DgVixhX3.js.map +0 -1
  44. package/dist/rls-bootstrap-sql-B5C9LoJ6.js +0 -244
  45. package/dist/rls-bootstrap-sql-B5C9LoJ6.js.map +0 -1
  46. package/dist/rls-enforcement-DvAbL9YJ.js.map +0 -1
  47. package/dist/src-DiB5RP2Z.js +0 -541
  48. package/dist/src-DiB5RP2Z.js.map +0 -1
  49. package/dist/websocket-BZ4H5wUz.js +0 -8198
  50. package/dist/websocket-BZ4H5wUz.js.map +0 -1
  51. package/src/PostgresAdapter.ts +0 -91
  52. package/src/PostgresBackendDriver.ts +0 -1857
  53. package/src/PostgresBootstrapper.ts +0 -1157
  54. package/src/auth/ensure-tables.ts +0 -896
  55. package/src/auth/schema-version.ts +0 -260
  56. package/src/auth/services.ts +0 -1610
  57. package/src/backup/backup-cli.ts +0 -520
  58. package/src/backup/backup-cron.ts +0 -212
  59. package/src/backup/backup-logic.ts +0 -93
  60. package/src/backup/backup-service.ts +0 -485
  61. package/src/backup/index.ts +0 -12
  62. package/src/backup/pg-tools.ts +0 -403
  63. package/src/backup/retention.ts +0 -86
  64. package/src/cli-errors.ts +0 -265
  65. package/src/cli-helpers.ts +0 -569
  66. package/src/cli-output.ts +0 -43
  67. package/src/cli.ts +0 -1339
  68. package/src/collections/PostgresCollectionRegistry.ts +0 -108
  69. package/src/collections/buildRegistry.ts +0 -68
  70. package/src/collections/validate-relations.ts +0 -402
  71. package/src/connection.ts +0 -333
  72. package/src/data-transformer.ts +0 -845
  73. package/src/databasePoolManager.ts +0 -95
  74. package/src/history/HistoryService.ts +0 -254
  75. package/src/history/ensure-history-table.ts +0 -53
  76. package/src/index.ts +0 -19
  77. package/src/interfaces.ts +0 -60
  78. package/src/module-dir.ts +0 -7
  79. package/src/schema/atlas-argv.ts +0 -94
  80. package/src/schema/auth-schema.ts +0 -298
  81. package/src/schema/auth-users-columns.ts +0 -131
  82. package/src/schema/carved-out-migration.ts +0 -404
  83. package/src/schema/classify-change.ts +0 -436
  84. package/src/schema/collection-index.ts +0 -427
  85. package/src/schema/collections-schema-version.ts +0 -103
  86. package/src/schema/destructive-sql.ts +0 -94
  87. package/src/schema/doctor-cli.ts +0 -66
  88. package/src/schema/doctor-policy-checks.ts +0 -105
  89. package/src/schema/doctor.ts +0 -837
  90. package/src/schema/drizzle-ddl.ts +0 -33
  91. package/src/schema/dynamic-tables.test.ts +0 -302
  92. package/src/schema/dynamic-tables.ts +0 -293
  93. package/src/schema/ensure-collection-policies.ts +0 -198
  94. package/src/schema/ensure-collection-tables.test.ts +0 -518
  95. package/src/schema/ensure-collection-tables.ts +0 -1267
  96. package/src/schema/generate-drizzle-schema-logic.ts +0 -957
  97. package/src/schema/generate-drizzle-schema.ts +0 -143
  98. package/src/schema/generate-postgres-ddl-logic.ts +0 -1339
  99. package/src/schema/generate-postgres-ddl.ts +0 -180
  100. package/src/schema/generate-schema-commit.ts +0 -267
  101. package/src/schema/generated-schema-staleness.ts +0 -284
  102. package/src/schema/introspect-db-constraints.ts +0 -385
  103. package/src/schema/introspect-db-inference.ts +0 -255
  104. package/src/schema/introspect-db-logic.ts +0 -1387
  105. package/src/schema/introspect-db-naming.ts +0 -15
  106. package/src/schema/introspect-db-project.ts +0 -78
  107. package/src/schema/introspect-db-queries.ts +0 -326
  108. package/src/schema/introspect-db-structure.ts +0 -670
  109. package/src/schema/introspect-db-types.ts +0 -56
  110. package/src/schema/introspect-db.ts +0 -257
  111. package/src/schema/introspect-runtime.test.ts +0 -260
  112. package/src/schema/introspect-runtime.ts +0 -327
  113. package/src/schema/non-sql-collections.test.ts +0 -131
  114. package/src/schema/rls-bootstrap-sql.ts +0 -288
  115. package/src/schema/search-column.ts +0 -643
  116. package/src/schema/test-schema.ts +0 -11
  117. package/src/schema/vector-index.ts +0 -491
  118. package/src/security/anonymous-grants.test.ts +0 -73
  119. package/src/security/policy-drift.test.ts +0 -435
  120. package/src/security/policy-drift.ts +0 -473
  121. package/src/security/rls-enforcement.ts +0 -505
  122. package/src/services/BranchService.ts +0 -326
  123. package/src/services/FetchService.ts +0 -2115
  124. package/src/services/PersistService.ts +0 -557
  125. package/src/services/RelationService.ts +0 -978
  126. package/src/services/RelationWriteService.ts +0 -653
  127. package/src/services/cdc/CdcListener.ts +0 -103
  128. package/src/services/cdc/junction-tables.ts +0 -91
  129. package/src/services/cdc/trigger-cdc.ts +0 -173
  130. package/src/services/channel-bus/ChannelBus.ts +0 -44
  131. package/src/services/channel-bus/PostgresChannelBus.ts +0 -299
  132. package/src/services/channel-bus/index.ts +0 -114
  133. package/src/services/channel-history.ts +0 -424
  134. package/src/services/channel-presence.ts +0 -185
  135. package/src/services/collection-helpers.ts +0 -420
  136. package/src/services/dataService.ts +0 -256
  137. package/src/services/index.ts +0 -14
  138. package/src/services/junction-writes.ts +0 -295
  139. package/src/services/nested-path.ts +0 -145
  140. package/src/services/pg-notify-listener.ts +0 -151
  141. package/src/services/realtimeService.ts +0 -2545
  142. package/src/services/row-pipeline.ts +0 -240
  143. package/src/services/write-denial.ts +0 -55
  144. package/src/types.ts +0 -4
  145. package/src/utils/connection-string.ts +0 -58
  146. package/src/utils/drizzle-conditions.ts +0 -2743
  147. package/src/utils/pg-array-null-patch.ts +0 -42
  148. package/src/utils/pg-error-utils.ts +0 -391
  149. 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
- /** Owns at least one user table — owners bypass non-FORCE RLS. */
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
- export declare function detectConnectionPosture(run: RawSqlRunner): Promise<ConnectionPosture>;
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): Promise<void>;
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.data` write at all.
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
- /** Whether to emit verbose debug logs (disabled in production). */
229
- private static readonly DEBUG;
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.data` / `context.data` serve too. A developer never sees a ref.
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
- "type": "module",
4
- "version": "0.17.3",
3
+ "version": "0.18.0",
5
4
  "description": "PostgreSQL data source backend implementation for Rebase with Drizzle ORM",
6
- "funding": {
7
- "url": "https://github.com/sponsors/rebaseco"
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": ">=20"
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": "^4.1.2",
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.17.3",
51
- "@rebasepro/common": "0.17.3",
52
- "@rebasepro/server": "0.17.3",
53
- "@rebasepro/types": "0.17.3",
54
- "@rebasepro/utils": "0.17.3"
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",