@rebasepro/server-postgres 0.12.1-canary.gf5f1d39 → 0.13.1-canary.g06dbe5b

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 (120) hide show
  1. package/dist/PostgresBackendDriver.d.ts +49 -2
  2. package/dist/PostgresBootstrapper.d.ts +25 -1
  3. package/dist/auth/services.d.ts +40 -0
  4. package/dist/{src-BbFOPJ1S.js → auth-users-columns-CBEOeYqa.js} +1118 -220
  5. package/dist/auth-users-columns-CBEOeYqa.js.map +1 -0
  6. package/dist/backup/backup-service.d.ts +10 -1
  7. package/dist/backup/pg-tools.d.ts +47 -0
  8. package/dist/backup-service-Bww-Lg0s.js +8999 -0
  9. package/dist/backup-service-Bww-Lg0s.js.map +1 -0
  10. package/dist/cli-helpers.d.ts +96 -1
  11. package/dist/connection-BuZ97wsr.js +250 -0
  12. package/dist/connection-BuZ97wsr.js.map +1 -0
  13. package/dist/connection.d.ts +42 -0
  14. package/dist/data-transformer.d.ts +7 -2
  15. package/dist/data_driver-ULAyJEi9.js +193 -0
  16. package/dist/data_driver-ULAyJEi9.js.map +1 -0
  17. package/dist/ensure-collection-policies-B_JMGa5K.js +57 -0
  18. package/dist/ensure-collection-policies-B_JMGa5K.js.map +1 -0
  19. package/dist/ensure-collection-tables-DzeTEvMv.js +800 -0
  20. package/dist/ensure-collection-tables-DzeTEvMv.js.map +1 -0
  21. package/dist/index.es.js +2460 -10233
  22. package/dist/index.es.js.map +1 -1
  23. package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
  24. package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
  25. package/dist/schema/auth-schema.d.ts +185 -144
  26. package/dist/schema/auth-users-columns.d.ts +97 -0
  27. package/dist/schema/doctor-policy-checks.d.ts +28 -0
  28. package/dist/schema/doctor.d.ts +23 -25
  29. package/dist/schema/ensure-collection-policies.d.ts +60 -0
  30. package/dist/schema/ensure-collection-tables.d.ts +99 -3
  31. package/dist/schema/generate-drizzle-schema-logic.d.ts +10 -2
  32. package/dist/schema/generate-postgres-ddl-logic.d.ts +185 -3
  33. package/dist/schema/generated-schema-staleness.d.ts +39 -0
  34. package/dist/schema/introspect-db-constraints.d.ts +57 -0
  35. package/dist/schema/introspect-db-logic.d.ts +94 -5
  36. package/dist/schema/introspect-db-queries.d.ts +119 -0
  37. package/dist/schema/introspect-db-structure.d.ts +263 -0
  38. package/dist/schema/introspect-db-types.d.ts +11 -0
  39. package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
  40. package/dist/schema/search-column.d.ts +248 -0
  41. package/dist/security/rls-enforcement.d.ts +61 -5
  42. package/dist/services/FetchService.d.ts +38 -8
  43. package/dist/services/RelationService.d.ts +54 -1
  44. package/dist/services/channel-bus/index.d.ts +1 -7
  45. package/dist/services/collection-helpers.d.ts +62 -2
  46. package/dist/services/dataService.d.ts +8 -1
  47. package/dist/services/realtimeService.d.ts +131 -21
  48. package/dist/services/row-pipeline.d.ts +1 -1
  49. package/dist/{src-Zqwaw3P5.js → src-C_wvdMnl.js} +78 -145
  50. package/dist/src-C_wvdMnl.js.map +1 -0
  51. package/dist/utils/connection-string.d.ts +29 -0
  52. package/dist/utils/drizzle-conditions.d.ts +129 -6
  53. package/dist/utils/pg-error-utils.d.ts +35 -0
  54. package/dist/websocket-D1qbmLZ2.js +587 -0
  55. package/dist/websocket-D1qbmLZ2.js.map +1 -0
  56. package/package.json +17 -16
  57. package/src/PostgresAdapter.ts +21 -2
  58. package/src/PostgresBackendDriver.ts +176 -6
  59. package/src/PostgresBootstrapper.ts +245 -37
  60. package/src/auth/ensure-tables.ts +357 -81
  61. package/src/auth/services.ts +93 -7
  62. package/src/backup/backup-cli.ts +41 -2
  63. package/src/backup/backup-service.ts +38 -5
  64. package/src/backup/pg-tools.ts +96 -3
  65. package/src/cli-helpers.ts +199 -10
  66. package/src/cli.ts +276 -56
  67. package/src/collections/validate-relations.ts +136 -14
  68. package/src/connection.ts +73 -0
  69. package/src/data-transformer.ts +129 -20
  70. package/src/databasePoolManager.ts +5 -2
  71. package/src/history/ensure-history-table.ts +7 -0
  72. package/src/schema/auth-schema.ts +46 -19
  73. package/src/schema/auth-users-columns.ts +131 -0
  74. package/src/schema/doctor-cli.ts +12 -63
  75. package/src/schema/doctor-policy-checks.ts +105 -0
  76. package/src/schema/doctor.ts +135 -76
  77. package/src/schema/ensure-collection-policies.ts +105 -0
  78. package/src/schema/ensure-collection-tables.test.ts +105 -9
  79. package/src/schema/ensure-collection-tables.ts +572 -42
  80. package/src/schema/generate-drizzle-schema-logic.ts +169 -43
  81. package/src/schema/generate-postgres-ddl-logic.ts +670 -29
  82. package/src/schema/generate-postgres-ddl.ts +25 -2
  83. package/src/schema/generated-schema-staleness.ts +169 -0
  84. package/src/schema/introspect-db-constraints.ts +385 -0
  85. package/src/schema/introspect-db-inference.ts +18 -8
  86. package/src/schema/introspect-db-logic.ts +446 -100
  87. package/src/schema/introspect-db-queries.ts +326 -0
  88. package/src/schema/introspect-db-structure.ts +670 -0
  89. package/src/schema/introspect-db-types.ts +56 -0
  90. package/src/schema/introspect-db.ts +37 -80
  91. package/src/schema/introspect-runtime.test.ts +56 -8
  92. package/src/schema/introspect-runtime.ts +31 -9
  93. package/src/schema/non-sql-collections.test.ts +131 -0
  94. package/src/schema/rls-bootstrap-sql.ts +288 -0
  95. package/src/schema/search-column.ts +643 -0
  96. package/src/security/anonymous-grants.test.ts +4 -2
  97. package/src/security/policy-drift.test.ts +11 -3
  98. package/src/security/rls-enforcement.ts +149 -6
  99. package/src/services/BranchService.ts +71 -28
  100. package/src/services/FetchService.ts +265 -122
  101. package/src/services/PersistService.ts +57 -7
  102. package/src/services/RelationService.ts +358 -114
  103. package/src/services/channel-bus/index.ts +0 -9
  104. package/src/services/channel-history.ts +14 -0
  105. package/src/services/channel-presence.ts +13 -0
  106. package/src/services/collection-helpers.ts +123 -4
  107. package/src/services/dataService.ts +8 -1
  108. package/src/services/realtimeService.ts +346 -81
  109. package/src/services/row-pipeline.ts +1 -1
  110. package/src/utils/connection-string.ts +58 -0
  111. package/src/utils/drizzle-conditions.ts +396 -29
  112. package/src/utils/pg-error-utils.ts +73 -3
  113. package/src/websocket.ts +130 -24
  114. package/dist/chunk-DSJWtz9O.js +0 -40
  115. package/dist/ensure-collection-tables-CNTcZGvn.js +0 -304
  116. package/dist/ensure-collection-tables-CNTcZGvn.js.map +0 -1
  117. package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
  118. package/dist/src-BbFOPJ1S.js.map +0 -1
  119. package/dist/src-Zqwaw3P5.js.map +0 -1
  120. package/src/schema/auth-bootstrap-sql.ts +0 -47
@@ -80,15 +80,6 @@ export function resolveChannelBusSetting(configured?: ChannelBusSetting): Channe
80
80
  return raw === "memory" ? { type: "memory" } : { type: "postgres" };
81
81
  }
82
82
 
83
- /**
84
- * @deprecated Use {@link resolveChannelBusSetting}, which also accepts a
85
- * supplied {@link ChannelBus} instance. Kept as a narrow alias so existing
86
- * config-only callers keep their exact types.
87
- */
88
- export function resolveChannelBusConfig(configured?: ChannelBusConfig): ChannelBusConfig {
89
- return resolveChannelBusSetting(configured) as ChannelBusConfig;
90
- }
91
-
92
83
  /**
93
84
  * Produce the bus a setting asks for.
94
85
  *
@@ -34,6 +34,7 @@ import { sql } from "drizzle-orm";
34
34
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
35
35
  import type { ChannelHistoryEntry, ChannelRetentionRule } from "@rebasepro/types";
36
36
  import { logger } from "@rebasepro/server";
37
+ import { revokeInternalTableSql } from "@rebasepro/common";
37
38
 
38
39
  /** How many messages a replay returns when the caller does not say. */
39
40
  const DEFAULT_REPLAY_LIMIT = 200;
@@ -198,6 +199,19 @@ export class ChannelHistoryStore {
198
199
  )
199
200
  `);
200
201
 
202
+ // Retained broadcasts for every channel in one table, with no RLS: who
203
+ // may replay a channel is decided before the read, by the channel gate
204
+ // in `realtimeService.authorizeChannelAction` — a replay is answered
205
+ // only for a client that has joined the channel, plus whatever an
206
+ // installed `ChannelAuthorizer` adds. That gate is the entire reason
207
+ // this table can sit outside the RLS model, so it fails closed; the
208
+ // rule language it does not yet have is written up in
209
+ // `docs/channel-authorization.md`. The driver's schema-wide
210
+ // grant reaches these (created here, after it ran), so take the
211
+ // privilege back.
212
+ await this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_messages")));
213
+ await this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_cursors")));
214
+
201
215
  this.tablesReady = true;
202
216
  logger.info(`✅ [ChannelHistory] Retained channels ready (${this.rules.length} rule(s)).`);
203
217
  }
@@ -28,6 +28,7 @@
28
28
 
29
29
  import { sql } from "drizzle-orm";
30
30
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
31
+ import { revokeInternalTableSql } from "@rebasepro/common";
31
32
 
32
33
  /** A tracked client, as any instance sees it. */
33
34
  export interface PresenceRow {
@@ -71,6 +72,18 @@ export class ChannelPresenceStore {
71
72
  ON rebase.channel_presence (last_seen)
72
73
  `);
73
74
 
75
+ // The roster of every client on every channel, with no RLS — a row
76
+ // policy has nothing to match on here, since a channel name is a string
77
+ // a client invents rather than a row anyone owns. What guards it is the
78
+ // channel gate in `realtimeService.authorizeChannelAction`: presence is
79
+ // readable only to a client that has joined the channel, plus whatever
80
+ // an installed `ChannelAuthorizer` adds. That gate is the entire reason
81
+ // this table can sit outside the RLS model, so it fails closed —
82
+ // see `docs/channel-authorization.md` for what it does *not*
83
+ // yet decide. Revoke the schema-wide grant the driver handed out
84
+ // before this table existed.
85
+ await this.db.execute(sql.raw(revokeInternalTableSql("rebase", "channel_presence")));
86
+
74
87
  this.tablesReady = true;
75
88
  }
76
89
 
@@ -1,15 +1,16 @@
1
1
  import { PgTable, AnyPgColumn } from "drizzle-orm/pg-core";
2
- import { CollectionConfig, Property } from "@rebasepro/types";
2
+ import { getTableColumns } from "drizzle-orm";
3
+ import { CollectionConfig, Property, ResolvedHasMany, ResolvedHasOne } from "@rebasepro/types";
3
4
  import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
4
5
  import { getTableName } from "@rebasepro/common";
5
- import { logger } from "@rebasepro/server";
6
+ import { ApiError, logger } from "@rebasepro/server";
6
7
 
7
8
  // Row identity is derived on both sides of the wire — the driver parses an
8
9
  // incoming address into key columns, the admin derives one from a served row —
9
10
  // so the implementation lives in `common` and both agree by construction.
10
- export { buildCompositeId, parseIdValues, COMPOSITE_ID_SEPARATOR } from "@rebasepro/common";
11
+ export { buildCompositeId, parseIdValues, isAddressableId, COMPOSITE_ID_SEPARATOR } from "@rebasepro/common";
11
12
  export type { PrimaryKeyInfo } from "@rebasepro/common";
12
- import { buildCompositeId, COMPOSITE_ID_SEPARATOR, getDeclaredPrimaryKeys } from "@rebasepro/common";
13
+ import { buildCompositeId, COMPOSITE_ID_SEPARATOR, getDeclaredPrimaryKeys, isAddressableId } from "@rebasepro/common";
13
14
  import type { PrimaryKeyInfo } from "@rebasepro/common";
14
15
 
15
16
  /**
@@ -40,6 +41,33 @@ export function getColumnMeta(col: AnyPgColumn): DrizzleColumnMeta {
40
41
  };
41
42
  }
42
43
 
44
+ /**
45
+ * Whether an address could name a row in this table, judged by the columns.
46
+ *
47
+ * {@link getPrimaryKeys} lets a config's `isId: "uuid"` win over the schema, so
48
+ * its `isUUID` is a claim rather than a fact — right for deriving addresses,
49
+ * wrong for refusing a query. Here the Drizzle column type decides, because it
50
+ * is what Postgres will enforce: a `uuid` column meets `/c/products/new` with
51
+ * `22P02`, which aborts the surrounding transaction and turns every later
52
+ * statement into an unrelated-looking `25P02`.
53
+ */
54
+ export function idCanAddressTable(
55
+ id: string | number,
56
+ table: PgTable,
57
+ idInfoArray: PrimaryKeyInfo[]
58
+ ): boolean {
59
+ const columnBacked = idInfoArray.map(info => {
60
+ const col = table[info.fieldName as keyof typeof table] as AnyPgColumn | undefined;
61
+ const meta = col ? getColumnMeta(col) : undefined;
62
+ // No column to ask (a key the schema does not carry) leaves the id
63
+ // addressable: refusing on a guess would 404 rows that do exist.
64
+ if (!meta?.columnType) return info;
65
+ return { ...info,
66
+ isUUID: meta.columnType === "PgUUID" };
67
+ });
68
+ return isAddressableId(id, columnBacked);
69
+ }
70
+
43
71
  export function getCollectionByPath(collectionPath: string, registry: PostgresCollectionRegistry): CollectionConfig {
44
72
  const collection = registry.getCollectionByPath(collectionPath);
45
73
  if (!collection) {
@@ -49,6 +77,58 @@ export function getCollectionByPath(collectionPath: string, registry: PostgresCo
49
77
  return collection;
50
78
  }
51
79
 
80
+ /**
81
+ * Reject a write naming something that is not a column of the table.
82
+ *
83
+ * Drizzle builds INSERT from `Object.entries(table[Symbol.Columns])` and UPDATE
84
+ * from `Object.keys(tableColumns)`, so a key the table does not carry is not
85
+ * rejected by anything — it is *left out of the statement*. The insert answers
86
+ * 201 having stored nothing under that name; the update, if the key was the
87
+ * only one, builds `update "posts" set where …` and Postgres raises a syntax
88
+ * error (SQLSTATE 42601), which is neither class 22 nor 23 and so surfaces as a
89
+ * 500 for what is a caller's typo.
90
+ *
91
+ * That makes this the last honest place to check, and the only one every write
92
+ * passes through. `assertKnownWriteFields` in the REST layer checks the same
93
+ * thing against the *config* and is skipped on four paths — `strictWrites:
94
+ * false`, a collection declaring no properties, an auth adapter that owns the
95
+ * body's shape, and a nested route whose target cannot be walked — and it never
96
+ * sees an in-process `rebase.data` write at all.
97
+ *
98
+ * It also gives `strictWrites: false` a truthful implementation. The flag is
99
+ * documented for "a column that really does exist which the config never
100
+ * declared", and skipping the config check alone could not deliver that: the
101
+ * value was dropped a layer later regardless. Skipping the config check and
102
+ * keeping this one does exactly what the flag says — the column must exist,
103
+ * the property need not.
104
+ */
105
+ export function assertWritableColumns(
106
+ values: Record<string, unknown>,
107
+ table: PgTable,
108
+ collectionPath: string
109
+ ): void {
110
+ // Only a real Drizzle table carries a column list. A stand-in that does not
111
+ // (a test double, a registry entry built by hand) has nothing to check
112
+ // against, and inventing an answer from its own keys would reject writes
113
+ // over the shape of the double rather than the shape of the table.
114
+ const columns = getTableColumns(table) as Record<string, unknown> | undefined;
115
+ if (!columns || Object.keys(columns).length === 0) return;
116
+
117
+ const unknown = Object.keys(values).filter(key => !(key in columns));
118
+ if (unknown.length === 0) return;
119
+
120
+ // The offending keys, and deliberately not the list of real ones: this
121
+ // error is reachable on paths where the REST field check was skipped, and
122
+ // an `excludeFromApi` column is documented as never being served to a
123
+ // caller. Naming what was sent is the actionable half anyway.
124
+ throw ApiError.badRequest(
125
+ `'${collectionPath}' has no column${unknown.length > 1 ? "s" : ""} ` +
126
+ `${unknown.map(key => `'${key}'`).join(", ")}, so the value${unknown.length > 1 ? "s" : ""} ` +
127
+ "would have been dropped before the statement was built.",
128
+ "VALIDATION_UNKNOWN_FIELDS"
129
+ );
130
+ }
131
+
52
132
  export function getTableForCollection(collection: CollectionConfig, registry: PostgresCollectionRegistry): PgTable<any> {
53
133
  const tableName = getTableName(collection);
54
134
  const table = registry.getTable(tableName);
@@ -149,6 +229,45 @@ export function requirePrimaryKeys(collection: CollectionConfig, registry: Postg
149
229
  return keys;
150
230
  }
151
231
 
232
+ /**
233
+ * The column on the *source* table that a `hasOne`/`hasMany` link points at.
234
+ *
235
+ * `sourceKey` is authored when the two sides join on a natural key — an
236
+ * external identity id, a SKU — and left off when they join on the row id,
237
+ * which is the overwhelming majority. That makes `undefined` the only optional
238
+ * field on a resolved relation, so it gets exactly one reader: this function.
239
+ * Every consumer that needs the column asks here, and none of them re-derives
240
+ * "or else the primary key" for itself. That is the whole point — the fallback
241
+ * chains this codebase removed from relation resolution were dangerous because
242
+ * they were *duplicated* and could disagree, not because they existed.
243
+ */
244
+ export function sourceKeyField(
245
+ relation: ResolvedHasOne | ResolvedHasMany,
246
+ sourceCollection: CollectionConfig,
247
+ registry: PostgresCollectionRegistry
248
+ ): string {
249
+ if (relation.sourceKey) return relation.sourceKey;
250
+ return requirePrimaryKeys(sourceCollection, registry)[0].fieldName;
251
+ }
252
+
253
+ /**
254
+ * Whether this link joins on something other than the source's primary key.
255
+ *
256
+ * Callers that hold a parent *id* — which is most of them, since an id is what
257
+ * a URL carries — must translate it to the source key's value before it can be
258
+ * compared with the target's foreign key. Those that hold the parent *row*, or
259
+ * that build a correlated subquery over the source table, can read the column
260
+ * directly and skip the lookup.
261
+ */
262
+ export function joinsOnNaturalKey(
263
+ relation: ResolvedHasOne | ResolvedHasMany,
264
+ sourceCollection: CollectionConfig,
265
+ registry: PostgresCollectionRegistry
266
+ ): boolean {
267
+ if (!relation.sourceKey) return false;
268
+ return relation.sourceKey !== requirePrimaryKeys(sourceCollection, registry)[0].fieldName;
269
+ }
270
+
152
271
  /**
153
272
  * Collections whose key the *browser* cannot resolve, and what it will do
154
273
  * instead.
@@ -1,5 +1,5 @@
1
1
  // import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
- import { FilterValues } from "@rebasepro/types";
2
+ import { FilterValues, LogicalCondition } from "@rebasepro/types";
3
3
  import type { VectorSearchParams } from "@rebasepro/types";
4
4
  import { FetchService } from "./FetchService";
5
5
  import { PersistService } from "./PersistService";
@@ -60,6 +60,8 @@ export class DataService implements DataRepository {
60
60
  collectionPath: string,
61
61
  options: {
62
62
  filter?: FilterValues<Extract<keyof M, string>>;
63
+ /** An `or(...)`/`and(...)` group, applied alongside `filter`. */
64
+ logical?: LogicalCondition;
63
65
  orderBy?: string;
64
66
  order?: "desc" | "asc";
65
67
  limit?: number;
@@ -81,10 +83,13 @@ export class DataService implements DataRepository {
81
83
  searchString: string,
82
84
  options: {
83
85
  filter?: FilterValues<Extract<keyof M, string>>;
86
+ /** An `or(...)`/`and(...)` group, applied alongside `filter`. */
87
+ logical?: LogicalCondition;
84
88
  orderBy?: string;
85
89
  order?: "desc" | "asc";
86
90
  limit?: number;
87
91
  databaseId?: string;
92
+ searchExplain?: boolean;
88
93
  } = {}
89
94
  ): Promise<Record<string, unknown>[]> {
90
95
  return this.fetchService.searchRows<M>(collectionPath, searchString, options);
@@ -97,6 +102,8 @@ export class DataService implements DataRepository {
97
102
  collectionPath: string,
98
103
  options: {
99
104
  filter?: FilterValues<Extract<keyof M, string>>;
105
+ /** An `or(...)`/`and(...)` group, applied alongside `filter`. */
106
+ logical?: LogicalCondition;
100
107
  searchString?: string;
101
108
  databaseId?: string;
102
109
  } = {}