@rebasepro/server-postgres 0.13.0 → 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 (85) hide show
  1. package/dist/PostgresBackendDriver.d.ts +48 -1
  2. package/dist/auth/services.d.ts +19 -0
  3. package/dist/{src-DlPBctw_.js → auth-users-columns-CBEOeYqa.js} +835 -63
  4. package/dist/auth-users-columns-CBEOeYqa.js.map +1 -0
  5. package/dist/{backup-service-CD8o_1Sl.js → backup-service-Bww-Lg0s.js} +2 -2
  6. package/dist/{backup-service-CD8o_1Sl.js.map → backup-service-Bww-Lg0s.js.map} +1 -1
  7. package/dist/cli-helpers.d.ts +57 -1
  8. package/dist/data-transformer.d.ts +7 -2
  9. package/dist/data_driver-ULAyJEi9.js +193 -0
  10. package/dist/data_driver-ULAyJEi9.js.map +1 -0
  11. package/dist/{ensure-collection-policies-ViG8XiPn.js → ensure-collection-policies-B_JMGa5K.js} +2 -2
  12. package/dist/{ensure-collection-policies-ViG8XiPn.js.map → ensure-collection-policies-B_JMGa5K.js.map} +1 -1
  13. package/dist/{ensure-collection-tables-CBQdOETu.js → ensure-collection-tables-DzeTEvMv.js} +170 -20
  14. package/dist/ensure-collection-tables-DzeTEvMv.js.map +1 -0
  15. package/dist/index.es.js +1365 -338
  16. package/dist/index.es.js.map +1 -1
  17. package/dist/rls-bootstrap-sql-Bpv3nUZo.js +244 -0
  18. package/dist/rls-bootstrap-sql-Bpv3nUZo.js.map +1 -0
  19. package/dist/schema/auth-schema.d.ts +102 -0
  20. package/dist/schema/auth-users-columns.d.ts +97 -0
  21. package/dist/schema/doctor-policy-checks.d.ts +28 -0
  22. package/dist/schema/doctor.d.ts +23 -25
  23. package/dist/schema/ensure-collection-tables.d.ts +61 -7
  24. package/dist/schema/generate-drizzle-schema-logic.d.ts +10 -2
  25. package/dist/schema/generate-postgres-ddl-logic.d.ts +53 -5
  26. package/dist/schema/generated-schema-staleness.d.ts +39 -0
  27. package/dist/schema/rls-bootstrap-sql.d.ts +135 -0
  28. package/dist/schema/search-column.d.ts +248 -0
  29. package/dist/security/rls-enforcement.d.ts +61 -5
  30. package/dist/services/FetchService.d.ts +34 -7
  31. package/dist/services/RelationService.d.ts +30 -0
  32. package/dist/services/collection-helpers.d.ts +26 -0
  33. package/dist/services/dataService.d.ts +5 -0
  34. package/dist/services/realtimeService.d.ts +131 -21
  35. package/dist/{src-DoU9yPqq.js → src-C_wvdMnl.js} +91 -2
  36. package/dist/src-C_wvdMnl.js.map +1 -0
  37. package/dist/utils/drizzle-conditions.d.ts +124 -2
  38. package/dist/{websocket-B2LsrINK.js → websocket-D1qbmLZ2.js} +75 -18
  39. package/dist/websocket-D1qbmLZ2.js.map +1 -0
  40. package/package.json +9 -8
  41. package/src/PostgresBackendDriver.ts +172 -6
  42. package/src/PostgresBootstrapper.ts +55 -6
  43. package/src/auth/ensure-tables.ts +207 -86
  44. package/src/auth/services.ts +69 -5
  45. package/src/cli-helpers.ts +129 -10
  46. package/src/cli.ts +232 -30
  47. package/src/collections/validate-relations.ts +124 -17
  48. package/src/data-transformer.ts +120 -17
  49. package/src/history/ensure-history-table.ts +7 -0
  50. package/src/schema/auth-schema.ts +17 -1
  51. package/src/schema/auth-users-columns.ts +131 -0
  52. package/src/schema/doctor-cli.ts +12 -63
  53. package/src/schema/doctor-policy-checks.ts +105 -0
  54. package/src/schema/doctor.ts +135 -76
  55. package/src/schema/ensure-collection-tables.ts +374 -32
  56. package/src/schema/generate-drizzle-schema-logic.ts +132 -42
  57. package/src/schema/generate-postgres-ddl-logic.ts +294 -16
  58. package/src/schema/generate-postgres-ddl.ts +25 -2
  59. package/src/schema/generated-schema-staleness.ts +169 -0
  60. package/src/schema/introspect-db-logic.ts +66 -34
  61. package/src/schema/non-sql-collections.test.ts +131 -0
  62. package/src/schema/rls-bootstrap-sql.ts +288 -0
  63. package/src/schema/search-column.ts +643 -0
  64. package/src/security/anonymous-grants.test.ts +4 -2
  65. package/src/security/rls-enforcement.ts +149 -6
  66. package/src/services/BranchService.ts +5 -0
  67. package/src/services/FetchService.ts +175 -108
  68. package/src/services/PersistService.ts +38 -2
  69. package/src/services/RelationService.ts +110 -67
  70. package/src/services/channel-history.ts +14 -0
  71. package/src/services/channel-presence.ts +13 -0
  72. package/src/services/collection-helpers.ts +54 -1
  73. package/src/services/dataService.ts +5 -0
  74. package/src/services/realtimeService.ts +344 -79
  75. package/src/utils/drizzle-conditions.ts +365 -23
  76. package/src/utils/pg-error-utils.ts +8 -3
  77. package/src/websocket.ts +113 -16
  78. package/dist/ensure-collection-tables-CBQdOETu.js.map +0 -1
  79. package/dist/policy-CeA1JcxP.js +0 -105
  80. package/dist/policy-CeA1JcxP.js.map +0 -1
  81. package/dist/schema/auth-bootstrap-sql.d.ts +0 -24
  82. package/dist/src-DlPBctw_.js.map +0 -1
  83. package/dist/src-DoU9yPqq.js.map +0 -1
  84. package/dist/websocket-B2LsrINK.js.map +0 -1
  85. package/src/schema/auth-bootstrap-sql.ts +0 -47
@@ -24,10 +24,66 @@ import type { CollectionConfig } from "@rebasepro/types";
24
24
  */
25
25
  export declare function diagnoseMissingBin(packageName: string): "not-installed" | "build-script-blocked";
26
26
  export declare function resolveLocalBin(binName: string): string | null;
27
- export declare function getTableIncludesFromCollections(collections: CollectionConfig[]): Promise<string[]>;
27
+ export declare function getTableIncludesFromCollections(allCollections: CollectionConfig[]): Promise<string[]>;
28
28
  export declare function getTableIncludes(collectionsPath: string): Promise<string[]>;
29
+ /**
30
+ * Load a project's collections the way the Atlas-facing commands need them.
31
+ *
32
+ * Deliberately forgiving — a file that fails to import is skipped rather than
33
+ * fatal — because the callers use this to *narrow* what Atlas may touch, and a
34
+ * hard failure here would block a push over an unrelated broken file. Callers
35
+ * that cannot tolerate a partial answer (the table excludes, which fail closed)
36
+ * check the result themselves.
37
+ */
38
+ export declare function loadCollectionsForCli(collectionsPath: string): Promise<CollectionConfig[]>;
29
39
  export declare function getDevDatabaseUrl(databaseUrl: string): string;
30
40
  export declare function ensureDevDatabaseExists(databaseUrl: string, devDatabaseUrl: string): Promise<void>;
41
+ /**
42
+ * The generated SQL for the project's `search` blocks, if it has any.
43
+ *
44
+ * @param drizzleDir directory holding the generated SQL. Defaults to `drizzle`
45
+ * under the working directory.
46
+ */
47
+ export declare function readSearchDdl(drizzleDir?: string): string;
48
+ /**
49
+ * Bring the search column, its index and their helpers up to date.
50
+ *
51
+ * Runs *after* Atlas, not before: the statements are `ALTER TABLE ... ADD
52
+ * COLUMN`, so the table has to exist. Atlas is told to ignore these objects
53
+ * entirely (`getSearchExcludes`) — it cannot manage them, and left to itself it
54
+ * would plan a `DROP COLUMN` for every one, since they are absent from the
55
+ * desired state it was given.
56
+ *
57
+ * A no-op when no collection declared `search`. A failure is *not* swallowed:
58
+ * silently pushing a schema whose search column never appeared is how a
59
+ * collection ends up with search configured, no error anywhere, and no results.
60
+ */
61
+ export declare function applySearchDdl(databaseUrl: string, drizzleDir?: string): Promise<void>;
62
+ /**
63
+ * Glob patterns keeping Atlas away from the search objects.
64
+ *
65
+ * Returns an empty list — and so changes nothing — for a project with no
66
+ * `search` block, which is every project that has not opted in.
67
+ */
68
+ export declare function getSearchExcludes(collectionsPath: string): Promise<string[]>;
69
+ /**
70
+ * Give the dev database the search helper functions before Atlas plans.
71
+ *
72
+ * Excluding the search column keeps Atlas from *diffing* it, but not from
73
+ * materialising the inspected schema — column and all — in the dev database to
74
+ * analyse the plan against. That replay is where a push against an
75
+ * already-searchable database died with `function public.rebase_search_text
76
+ * (jsonb) does not exist`: the column came across, the function it calls did
77
+ * not, because Atlas will not carry a function at all.
78
+ *
79
+ * Only the extensions and functions, never the tables: the dev database holds
80
+ * whatever Atlas puts there and nothing of ours.
81
+ *
82
+ * Best-effort by design. Failing here would block a push for a project whose
83
+ * collections merely failed to import, and if the functions really are needed
84
+ * and really are missing, Atlas says so a moment later in its own words.
85
+ */
86
+ export declare function seedDevDatabaseSearchHelpers(devDatabaseUrl: string, collectionsPath: string): Promise<void>;
31
87
  /**
32
88
  * Query the live database for every user table/view outside the system
33
89
  * catalogs. Separated from {@link getTableExcludes} so its failure mode can
@@ -42,9 +42,14 @@ export declare function sanitizeAndConvertDates(obj: unknown): unknown;
42
42
  */
43
43
  export declare function serializeDataToServer<M extends Record<string, unknown>>(row: M, properties: Properties, collection?: CollectionConfig, registry?: PostgresCollectionRegistry): SerializedEntityData;
44
44
  /**
45
- * Serialize a single property value for database storage
45
+ * Serialize a single property value for database storage.
46
+ *
47
+ * `propertyKey` is only ever used to phrase errors and warnings. Without it the
48
+ * one trace a bad value left was `Expected array value for array property, got
49
+ * string` — no collection, no property, no value, which in the log of a
50
+ * thousand-row import names nothing at all.
46
51
  */
47
- export declare function serializePropertyToServer(value: unknown, property: Property): unknown;
52
+ export declare function serializePropertyToServer(value: unknown, property: Property, propertyKey?: string): unknown;
48
53
  /**
49
54
  * Transform IDs back to relation objects for frontend
50
55
  */
@@ -0,0 +1,193 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ import "process";
3
+ __createRequire(import.meta.url);
4
+ //#region ../types/src/errors.ts
5
+ /**
6
+ * The single error type thrown across the entire Rebase client surface —
7
+ * HTTP data/control-plane calls, realtime/WebSocket operations, and
8
+ * client-side logic errors (e.g. an unknown collection accessor). A `catch`
9
+ * block only ever needs to check for this one class:
10
+ *
11
+ * ```ts
12
+ * import { RebaseApiError } from "@rebasepro/client"; // re-exported
13
+ *
14
+ * try {
15
+ * await client.data.products.update(id, { price: 9 });
16
+ * } catch (e) {
17
+ * if (e instanceof RebaseApiError) {
18
+ * if (e.status === 404) { ... } // HTTP failures carry a status
19
+ * console.error(e.code, e.details);
20
+ * }
21
+ * }
22
+ * ```
23
+ *
24
+ * `status` is present for HTTP failures and `undefined` otherwise, so its
25
+ * presence distinguishes transport-level errors from realtime/logic errors.
26
+ *
27
+ * @group Errors
28
+ */
29
+ var RebaseApiError = class extends Error {
30
+ /** HTTP status code, or `undefined` for non-HTTP errors. */
31
+ status;
32
+ /** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */
33
+ code;
34
+ /** Structured error payload from the server, when present. */
35
+ details;
36
+ constructor(message, init = {}) {
37
+ super(message);
38
+ this.name = "RebaseApiError";
39
+ this.status = init.status;
40
+ this.code = init.code;
41
+ this.details = init.details;
42
+ if (init.cause !== void 0) this.cause = init.cause;
43
+ }
44
+ };
45
+ //#endregion
46
+ //#region ../types/src/types/policy.ts
47
+ /**
48
+ * The id a request without a logged-in user reports as `rebase.uid()`.
49
+ *
50
+ * A user-context request always sets `app.uid`: blank would read back as
51
+ * `NULL`, and `NULL` is how the trusted server context is recognised, so an
52
+ * anonymous visitor would be promoted to server privileges. The driver
53
+ * therefore substitutes this sentinel at the single chokepoint where the GUC
54
+ * is set.
55
+ *
56
+ * The consequence for policy authors is that **`rebase.uid() IS NOT NULL` is a
57
+ * tautology on the user path** — it is true for anonymous visitors too. Use
58
+ * {@link policy.authenticated} to mean "signed in", and
59
+ * {@link policy.serverContext} to mean "the trusted server context". Do not
60
+ * hand-write the comparison: see {@link ANONYMOUS_USER_IDS} for why one
61
+ * literal is not enough.
62
+ *
63
+ * @group Models
64
+ */
65
+ var ANONYMOUS_USER_ID = "anonymous";
66
+ /**
67
+ * Every uid that has ever meant "nobody is signed in" — newest first.
68
+ *
69
+ * There are two because there were two. The types, the policy compiler, the
70
+ * JavaScript evaluator and the linter were all built on
71
+ * {@link ANONYMOUS_USER_ID}, while the request path scoped unauthenticated
72
+ * callers as `'anon'` — so `policy.authenticated()`, which compiled to
73
+ * `rebase.uid() <> 'anonymous'`, was *true* for an anonymous visitor. The
74
+ * sanctioned way to write "signed in" granted to everyone, and the linter
75
+ * flagged the spelling that actually worked as a foreign convention.
76
+ *
77
+ * The request path now reports {@link ANONYMOUS_USER_ID}. `'anon'` stays here
78
+ * because policies outlive the server that generated them: a database still
79
+ * holding policies from before the fix, or a project whose server has not been
80
+ * upgraded yet, must not become a grant in either direction. Compile against
81
+ * this list, not against a single literal.
82
+ *
83
+ * No real user id is ever one of these, so a match is always "not signed in".
84
+ *
85
+ * @group Models
86
+ */
87
+ var ANONYMOUS_USER_IDS = [ANONYMOUS_USER_ID, "anon"];
88
+ /** @group Models */
89
+ var policy = {
90
+ true: () => ({ kind: "true" }),
91
+ false: () => ({ kind: "false" }),
92
+ and: (...operands) => ({
93
+ kind: "and",
94
+ operands
95
+ }),
96
+ or: (...operands) => ({
97
+ kind: "or",
98
+ operands
99
+ }),
100
+ not: (operand) => ({
101
+ kind: "not",
102
+ operand
103
+ }),
104
+ compare: (left, op, right) => ({
105
+ kind: "compare",
106
+ op,
107
+ left,
108
+ right
109
+ }),
110
+ rolesOverlap: (roles) => ({
111
+ kind: "rolesOverlap",
112
+ roles
113
+ }),
114
+ rolesContain: (roles) => ({
115
+ kind: "rolesContain",
116
+ roles
117
+ }),
118
+ authenticated: () => ({ kind: "authenticated" }),
119
+ serverContext: () => ({ kind: "serverContext" }),
120
+ existsIn: (args) => ({
121
+ kind: "existsIn",
122
+ collection: args.collection,
123
+ where: args.where
124
+ }),
125
+ raw: (sql) => ({
126
+ kind: "raw",
127
+ sql
128
+ }),
129
+ field: (name) => ({
130
+ kind: "field",
131
+ name
132
+ }),
133
+ outerField: (name) => ({
134
+ kind: "outerField",
135
+ name
136
+ }),
137
+ literal: (value) => ({
138
+ kind: "literal",
139
+ value
140
+ }),
141
+ authUid: () => ({ kind: "authUid" }),
142
+ authRoles: () => ({ kind: "authRoles" })
143
+ };
144
+ /**
145
+ * Thrown by {@link resolveClientListLimit} for a `limit` the platform will not
146
+ * serve. Carries an HTTP status so an ingress that speaks HTTP can forward it
147
+ * verbatim, and `maxLimit` so one can be built without re-deriving the ceiling.
148
+ *
149
+ * @group Errors
150
+ */
151
+ var ListLimitError = class ListLimitError extends RebaseApiError {
152
+ /** The ceiling that was exceeded — what the caller should page by instead. */
153
+ maxLimit;
154
+ constructor(message, maxLimit) {
155
+ super(message, {
156
+ status: 400,
157
+ code: "INVALID_LIMIT"
158
+ });
159
+ this.name = "ListLimitError";
160
+ this.maxLimit = maxLimit;
161
+ Object.setPrototypeOf(this, ListLimitError.prototype);
162
+ }
163
+ };
164
+ /**
165
+ * Resolve a client-supplied list `limit` into a safe, always-defined value.
166
+ *
167
+ * - An absent / blank limit falls back to the mode default:
168
+ * `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.
169
+ * - A limit that is present must be an integer in `[1, maxLimit]`. Anything
170
+ * else — `0`, a negative, `1.5`, `abc`, `100000000` — throws
171
+ * {@link ListLimitError} rather than being coerced into range, because every
172
+ * coercion answers a question the caller did not ask with a page it cannot
173
+ * tell apart from the whole collection.
174
+ *
175
+ * The return is never `undefined` — no ingress that routes its client limit
176
+ * through this can produce an unbounded read.
177
+ *
178
+ * @throws {ListLimitError} when a present `limit` is not an integer in range.
179
+ */
180
+ function resolveClientListLimit(rawLimit, opts = {}) {
181
+ const maxLimit = opts.maxLimit ?? 1e3;
182
+ if (rawLimit != null && String(rawLimit).trim() !== "") {
183
+ const parsed = typeof rawLimit === "number" ? rawLimit : Number(String(rawLimit).trim());
184
+ if (!Number.isInteger(parsed) || parsed < 1) throw new ListLimitError(`Invalid \`limit\`: ${String(rawLimit)}. Expected a whole number between 1 and ${maxLimit}.`, maxLimit);
185
+ if (parsed > maxLimit) throw new ListLimitError(`\`limit\` ${parsed} is above the maximum of ${maxLimit}. Ask for at most ${maxLimit} rows per read and page through the rest with \`offset\` — answering with a smaller page would be indistinguishable from there being no more rows.`, maxLimit);
186
+ return parsed;
187
+ }
188
+ return opts.vectorSearch ? opts.vectorDefaultLimit ?? 10 : opts.defaultLimit ?? 50;
189
+ }
190
+ //#endregion
191
+ export { policy as a, ANONYMOUS_USER_IDS as i, resolveClientListLimit as n, ANONYMOUS_USER_ID as r, ListLimitError as t };
192
+
193
+ //# sourceMappingURL=data_driver-ULAyJEi9.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data_driver-ULAyJEi9.js","names":[],"sources":["../../types/src/errors.ts","../../types/src/types/policy.ts","../../types/src/controllers/data_driver.ts"],"sourcesContent":["/**\n * The error codes every route can produce, as `RebaseApiError.code`.\n *\n * These are the defaults on `ApiError`'s static constructors server-side, so\n * any endpoint can answer with one. They are **not** the complete set: routes\n * pass their own more specific codes too (`EMAIL_EXISTS`, `TOKEN_EXPIRED`,\n * `INVALID_BULK_BODY`, …), and auth alone defines a couple of dozen.\n *\n * Hence the union is deliberately open rather than closed. It exists to give\n * autocomplete and to catch a typo in the common cases — `code` was a bare\n * `string`, so `e.code === \"NOT_FOUND\"` and `e.code === \"NOTFOUND\"` were\n * equally valid and only one of them worked. Closing it would be a lie that\n * broke the moment a route added a code.\n *\n * @example\n * if (e instanceof RebaseApiError) {\n * switch (e.code) {\n * case \"NOT_FOUND\": return null; // completed\n * case \"FORBIDDEN\": return redirect();\n * default: throw e; // routes' own codes land here\n * }\n * }\n *\n * @group Errors\n */\nexport type RebaseErrorCode =\n | \"BAD_REQUEST\"\n | \"UNAUTHORIZED\"\n | \"FORBIDDEN\"\n | \"NOT_FOUND\"\n | \"CONFLICT\"\n | \"INTERNAL_ERROR\"\n | \"SERVICE_UNAVAILABLE\"\n | \"DB_PERMISSION_DENIED\"\n | \"SCHEMA_DRIFT\"\n // `string & {}` keeps the union open while preserving completion on the\n // literals above — a bare `| string` would collapse them and offer nothing.\n | (string & {});\n\n/**\n * Structured initializer for {@link RebaseApiError}.\n *\n * @group Errors\n */\nexport interface RebaseErrorInit {\n /**\n * HTTP status code, when the error originated from an HTTP response.\n * Left `undefined` for realtime/WebSocket, network, and client-side\n * logic errors that have no HTTP status.\n */\n status?: number;\n /** Stable, machine-readable error code. See {@link RebaseErrorCode}. */\n code?: RebaseErrorCode;\n /** Structured error payload returned by the server, when present. */\n details?: unknown;\n /** The underlying error this one wraps, if any. */\n cause?: unknown;\n}\n\n/**\n * The single error type thrown across the entire Rebase client surface —\n * HTTP data/control-plane calls, realtime/WebSocket operations, and\n * client-side logic errors (e.g. an unknown collection accessor). A `catch`\n * block only ever needs to check for this one class:\n *\n * ```ts\n * import { RebaseApiError } from \"@rebasepro/client\"; // re-exported\n *\n * try {\n * await client.data.products.update(id, { price: 9 });\n * } catch (e) {\n * if (e instanceof RebaseApiError) {\n * if (e.status === 404) { ... } // HTTP failures carry a status\n * console.error(e.code, e.details);\n * }\n * }\n * ```\n *\n * `status` is present for HTTP failures and `undefined` otherwise, so its\n * presence distinguishes transport-level errors from realtime/logic errors.\n *\n * @group Errors\n */\nexport class RebaseApiError extends Error {\n /** HTTP status code, or `undefined` for non-HTTP errors. */\n readonly status?: number;\n /** Stable machine-readable error code, when the server supplied one. See {@link RebaseErrorCode}. */\n readonly code?: RebaseErrorCode;\n /** Structured error payload from the server, when present. */\n readonly details?: unknown;\n\n constructor(message: string, init: RebaseErrorInit = {}) {\n super(message);\n this.name = \"RebaseApiError\";\n this.status = init.status;\n this.code = init.code;\n this.details = init.details;\n if (init.cause !== undefined) {\n // `cause` is standard on Error but not always in the lib target's type.\n (this as { cause?: unknown }).cause = init.cause;\n }\n }\n}\n\n/**\n * Client-side logic error — raised before any request is made (e.g. accessing\n * an unknown collection accessor when a typed dictionary is configured).\n *\n * A subclass of {@link RebaseApiError} (with no `status`), so a single\n * `catch (e) { if (e instanceof RebaseApiError) ... }` handles it too.\n *\n * @group Errors\n */\nexport class RebaseClientError extends RebaseApiError {\n constructor(message: string) {\n super(message);\n this.name = \"RebaseClientError\";\n }\n}\n","/**\n * Structured, engine-agnostic policy expressions.\n *\n * A {@link PolicyExpression} is the single source of truth for a row-level\n * security condition. It is compiled to Postgres `USING`/`WITH CHECK` SQL\n * (authoritative enforcement) and independently evaluated in JavaScript (to\n * drive the admin UI, and — in future — to enforce on engines without native\n * RLS such as MongoDB). Because both the SQL and the JS decision derive from\n * the *same* expression, the UI matches database enforcement by construction —\n * no drift between two hand-written implementations.\n *\n * The only escape hatch that cannot be evaluated client-side is the\n * {@link RawPolicyExpression} node (`{ kind: \"raw\" }`): it preserves full\n * PostgreSQL power but, being arbitrary SQL, is treated as *unknown* by the\n * JavaScript evaluator (never silently allowed) and reflected exactly in the UI\n * via server-computed capability flags.\n *\n * @group Models\n */\nexport type PolicyExpression =\n | TruePolicyExpression\n | FalsePolicyExpression\n | AndPolicyExpression\n | OrPolicyExpression\n | NotPolicyExpression\n | ComparePolicyExpression\n | RolesOverlapPolicyExpression\n | RolesContainPolicyExpression\n | AuthenticatedPolicyExpression\n | ServerContextPolicyExpression\n | ExistsInPolicyExpression\n | RawPolicyExpression;\n\n/**\n * The id a request without a logged-in user reports as `rebase.uid()`.\n *\n * A user-context request always sets `app.uid`: blank would read back as\n * `NULL`, and `NULL` is how the trusted server context is recognised, so an\n * anonymous visitor would be promoted to server privileges. The driver\n * therefore substitutes this sentinel at the single chokepoint where the GUC\n * is set.\n *\n * The consequence for policy authors is that **`rebase.uid() IS NOT NULL` is a\n * tautology on the user path** — it is true for anonymous visitors too. Use\n * {@link policy.authenticated} to mean \"signed in\", and\n * {@link policy.serverContext} to mean \"the trusted server context\". Do not\n * hand-write the comparison: see {@link ANONYMOUS_USER_IDS} for why one\n * literal is not enough.\n *\n * @group Models\n */\nexport const ANONYMOUS_USER_ID = \"anonymous\";\n\n/**\n * Every uid that has ever meant \"nobody is signed in\" — newest first.\n *\n * There are two because there were two. The types, the policy compiler, the\n * JavaScript evaluator and the linter were all built on\n * {@link ANONYMOUS_USER_ID}, while the request path scoped unauthenticated\n * callers as `'anon'` — so `policy.authenticated()`, which compiled to\n * `rebase.uid() <> 'anonymous'`, was *true* for an anonymous visitor. The\n * sanctioned way to write \"signed in\" granted to everyone, and the linter\n * flagged the spelling that actually worked as a foreign convention.\n *\n * The request path now reports {@link ANONYMOUS_USER_ID}. `'anon'` stays here\n * because policies outlive the server that generated them: a database still\n * holding policies from before the fix, or a project whose server has not been\n * upgraded yet, must not become a grant in either direction. Compile against\n * this list, not against a single literal.\n *\n * No real user id is ever one of these, so a match is always \"not signed in\".\n *\n * @group Models\n */\nexport const ANONYMOUS_USER_IDS: readonly string[] = [ANONYMOUS_USER_ID, \"anon\"];\n\n/**\n * Whether a uid stands for \"no one is signed in\", in any spelling rebase has\n * used. `null`/`undefined` is the trusted server context, not an anonymous\n * caller, and is therefore **not** anonymous — see {@link ANONYMOUS_USER_ID}.\n *\n * @group Models\n */\nexport function isAnonymousUid(uid: string | null | undefined): boolean {\n return typeof uid === \"string\" && ANONYMOUS_USER_IDS.includes(uid);\n}\n\n/** Always allows. Compiles to `true`. @group Models */\nexport interface TruePolicyExpression {\n kind: \"true\";\n}\n\n/** Always denies. Compiles to `false`. @group Models */\nexport interface FalsePolicyExpression {\n kind: \"false\";\n}\n\n/** Logical AND — every operand must pass. @group Models */\nexport interface AndPolicyExpression {\n kind: \"and\";\n operands: readonly PolicyExpression[];\n}\n\n/** Logical OR — at least one operand must pass. @group Models */\nexport interface OrPolicyExpression {\n kind: \"or\";\n operands: readonly PolicyExpression[];\n}\n\n/** Logical negation. @group Models */\nexport interface NotPolicyExpression {\n kind: \"not\";\n operand: PolicyExpression;\n}\n\n/** Comparison operators available to {@link ComparePolicyExpression}. @group Models */\nexport type PolicyCompareOperator = \"eq\" | \"neq\" | \"lt\" | \"lte\" | \"gt\" | \"gte\";\n\n/**\n * Compares two operands, e.g. `owner_id = rebase.uid()`.\n * @group Models\n */\nexport interface ComparePolicyExpression {\n kind: \"compare\";\n op: PolicyCompareOperator;\n left: PolicyOperand;\n right: PolicyOperand;\n}\n\n/**\n * True when the user holds *at least one* of the given application roles.\n * Compiles to `string_to_array(rebase.roles(), ',') && ARRAY[...]`.\n * @group Models\n */\nexport interface RolesOverlapPolicyExpression {\n kind: \"rolesOverlap\";\n roles: readonly string[];\n}\n\n/**\n * True when the user holds *all* of the given application roles.\n * Compiles to `string_to_array(rebase.roles(), ',') @> ARRAY[...]`.\n * @group Models\n */\nexport interface RolesContainPolicyExpression {\n kind: \"rolesContain\";\n roles: readonly string[];\n}\n\n/**\n * True when a signed-in user is making the request. Compiles to\n * `rebase.uid() IS NOT NULL AND rebase.uid() <> 'anonymous'`.\n *\n * Both halves are load-bearing. `IS NOT NULL` excludes the server context;\n * the {@link ANONYMOUS_USER_ID} comparison excludes anonymous visitors, who\n * *do* carry a non-null `rebase.uid()`. Checking only `IS NOT NULL` grants to\n * everyone — see {@link ANONYMOUS_USER_ID}.\n *\n * `policy.not(policy.authenticated())` therefore means \"anonymous visitor or\n * the server context\". To single out the server context, use\n * {@link ServerContextPolicyExpression}.\n * @group Models\n */\nexport interface AuthenticatedPolicyExpression {\n kind: \"authenticated\";\n}\n\n/**\n * True only in the trusted **server context** — the built-in flows that run\n * without a user (signup, migrations, `dataAsAdmin`) set no user GUC, so\n * `rebase.uid()` is `NULL` for them and only for them. Compiles to\n * `rebase.uid() IS NULL`.\n *\n * This is what lets the owner connection satisfy a policy even under FORCE RLS.\n * It is deliberately a primitive rather than `not(authenticated())`: the two\n * meant the same thing while `authenticated` ignored {@link ANONYMOUS_USER_ID},\n * and conflating them is what turns a server-only grant into an anonymous one.\n *\n * The JavaScript evaluator always returns `false` for this node — a client is\n * never the server context.\n * @group Models\n */\nexport interface ServerContextPolicyExpression {\n kind: \"serverContext\";\n}\n\n/**\n * Membership / relational access: true when at least one row exists in another\n * collection (a join/membership table) matching `where`. This is what lets you\n * scope reads to \"rows whose team the caller belongs to\" without an N+1\n * per-row lookup — it compiles to a single correlated `EXISTS` subquery.\n *\n * Inside `where`, {@link FieldPolicyOperand} (`policy.field`) references a column\n * of the joined collection, while {@link OuterFieldPolicyOperand}\n * (`policy.outerField`) references a column of the row being checked (the outer\n * table under RLS). Combine with {@link AuthUidPolicyOperand} to correlate to\n * the caller.\n *\n * @example\n * ```ts\n * // documents visible only to members of the document's team:\n * policy.existsIn({\n * collection: \"team_members\",\n * where: policy.and(\n * policy.compare(policy.field(\"team_id\"), \"eq\", policy.outerField(\"team_id\")),\n * policy.compare(policy.field(\"user_id\"), \"eq\", policy.authUid()),\n * ),\n * })\n * // → EXISTS (SELECT 1 FROM team_members _ex0\n * // WHERE _ex0.team_id = documents.team_id AND _ex0.user_id = rebase.uid())\n * ```\n *\n * Postgres-authoritative: like {@link RawPolicyExpression}, the JavaScript\n * evaluator treats it as *unknown* (it cannot run a subquery client-side), so\n * enforcement is always the database's.\n * @group Models\n */\nexport interface ExistsInPolicyExpression {\n kind: \"existsIn\";\n /** Slug of the collection to search (the join / membership table). */\n collection: string;\n /** Condition evaluated against the joined collection's rows. */\n where: PolicyExpression;\n}\n\n/**\n * A raw PostgreSQL boolean expression — the full-power escape hatch.\n *\n * Columns can be referenced as `{column_name}`. This is Postgres-only and\n * **server-authoritative**: the JavaScript evaluator cannot evaluate arbitrary\n * SQL, so it treats this node as *unknown* rather than guessing.\n * @group Models\n */\nexport interface RawPolicyExpression {\n kind: \"raw\";\n sql: string;\n}\n\n/**\n * An operand referenced by a {@link ComparePolicyExpression}.\n * @group Models\n */\nexport type PolicyOperand =\n | FieldPolicyOperand\n | OuterFieldPolicyOperand\n | LiteralPolicyOperand\n | AuthUidPolicyOperand\n | AuthRolesPolicyOperand;\n\n/** A column value on the row being evaluated. @group Models */\nexport interface FieldPolicyOperand {\n kind: \"field\";\n /** The property/column name (resolved to its DB column when compiled). */\n name: string;\n}\n\n/**\n * A column value on the *outer* row when used inside {@link ExistsInPolicyExpression}\n * — i.e. the row the RLS policy is being evaluated for, referenced from within the\n * subquery. Outside an `existsIn` it is equivalent to {@link FieldPolicyOperand}.\n * @group Models\n */\nexport interface OuterFieldPolicyOperand {\n kind: \"outerField\";\n /** The property/column name on the outer collection. */\n name: string;\n}\n\n/** A constant value. @group Models */\nexport interface LiteralPolicyOperand {\n kind: \"literal\";\n value: string | number | boolean | null;\n}\n\n/** The current user's id — compiles to `rebase.uid()`. @group Models */\nexport interface AuthUidPolicyOperand {\n kind: \"authUid\";\n}\n\n/**\n * The current user's roles as an array — compiles to\n * `string_to_array(rebase.roles(), ',')`.\n * @group Models\n */\nexport interface AuthRolesPolicyOperand {\n kind: \"authRoles\";\n}\n\n// ── Constructor helpers ──────────────────────────────────────────────\n// Small, dependency-free builders so callers (and the desugaring in\n// `@rebasepro/common`) can assemble expressions without object-literal noise.\n\n/** @group Models */\nexport const policy = {\n true: (): TruePolicyExpression => ({ kind: \"true\" }),\n false: (): FalsePolicyExpression => ({ kind: \"false\" }),\n and: (...operands: readonly PolicyExpression[]): AndPolicyExpression => ({ kind: \"and\",\noperands: operands as PolicyExpression[] }),\n or: (...operands: readonly PolicyExpression[]): OrPolicyExpression => ({ kind: \"or\",\noperands: operands as PolicyExpression[] }),\n not: (operand: PolicyExpression): NotPolicyExpression => ({ kind: \"not\",\noperand }),\n compare: (left: PolicyOperand, op: PolicyCompareOperator, right: PolicyOperand): ComparePolicyExpression =>\n ({ kind: \"compare\",\nop,\nleft,\nright }),\n rolesOverlap: (roles: readonly string[]): RolesOverlapPolicyExpression => ({ kind: \"rolesOverlap\",\nroles: roles as string[] }),\n rolesContain: (roles: readonly string[]): RolesContainPolicyExpression => ({ kind: \"rolesContain\",\nroles: roles as string[] }),\n authenticated: (): AuthenticatedPolicyExpression => ({ kind: \"authenticated\" }),\n serverContext: (): ServerContextPolicyExpression => ({ kind: \"serverContext\" }),\n existsIn: (args: { collection: string; where: PolicyExpression }): ExistsInPolicyExpression =>\n ({ kind: \"existsIn\",\ncollection: args.collection,\nwhere: args.where }),\n raw: (sql: string): RawPolicyExpression => ({ kind: \"raw\",\nsql }),\n field: (name: string): FieldPolicyOperand => ({ kind: \"field\",\nname }),\n outerField: (name: string): OuterFieldPolicyOperand => ({ kind: \"outerField\",\nname }),\n literal: (value: string | number | boolean | null): LiteralPolicyOperand => ({ kind: \"literal\",\nvalue }),\n authUid: (): AuthUidPolicyOperand => ({ kind: \"authUid\" }),\n authRoles: (): AuthRolesPolicyOperand => ({ kind: \"authRoles\" })\n};\n","import { RebaseApiError } from \"../errors\";\nimport type { CollectionRegistryController } from \"./collection_registry\";\nimport type { EntityStatus, EntityValues } from \"../types/entities\";\nimport type { CollectionConfig, FilterValues } from \"../types/collections\";\nimport type { RebaseCallContext } from \"../call_context\";\nimport type { LogicalCondition } from \"./data\";\n\n\n/**\n * @internal\n */\nexport interface FetchOneProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n id: string | number;\n databaseId?: string;\n collection?: CollectionConfig<M>\n}\n\n/**\n * @internal\n */\nexport type ListenOneProps<M extends Record<string, unknown> = Record<string, unknown>> =\n FetchOneProps<M>\n & {\n onUpdate: (row: Record<string, unknown> | null) => void,\n onError?: (error: Error) => void,\n }\n\n/**\n * Configuration for vector similarity search queries.\n * Vector search applies an ORDER BY distance expression and optionally\n * filters results by a distance threshold.\n */\nexport interface VectorSearchParams {\n /** Property name containing the vector column */\n property: string;\n /** Query vector to compare against */\n vector: number[];\n /** Distance function (default: \"cosine\") */\n distance?: \"cosine\" | \"l2\" | \"inner_product\";\n /** Only return results within this distance threshold */\n threshold?: number;\n}\n\n// ── List pagination bounds ────────────────────────────────────────────────\n//\n// Client-driven list reads (REST `GET /<collection>` and the WebSocket\n// `subscribe_collection` message) accept a client-supplied `limit`. Without\n// bounds, an ABSENT limit streams the entire table into memory — a trivial\n// OOM/DoS — and `limit=100000000` (or `limit=0`, historically an unlimited\n// bypass) is honoured verbatim. `resolveClientListLimit` is the single shared\n// enforcement point so every untrusted ingress behaves identically. Trusted\n// server-side callers build fetch options directly and are intentionally NOT\n// bounded here (migrations, admin exports, and CDC refetches may need the full\n// set).\n//\n// A limit the platform will not serve is REFUSED, not quietly shrunk. Clamping\n// answers a request for 100 000 rows with 1 000 of them, and a short page is\n// indistinguishable from \"that is all the data there is\" — which is how a CSV\n// export shipped 50 rows of a 100 000-row collection under a filename that read\n// like the whole thing. `meta.total`/`meta.hasMore` make truncation *detectable*\n// on the REST list response, but only for a caller who thinks to compare what it\n// asked for against what it got, and the WebSocket `collection_update` frame\n// carries neither — so signalling cannot be the answer on every surface and\n// rejecting is. An ABSENT limit still defaults: naming no window is not the same\n// as asking for one that cannot be served.\n\n/** Rows returned for a plain / text-search list read when the client sends no `limit`. */\nexport const DEFAULT_LIST_LIMIT = 50;\n/** Rows returned for a vector-search list read when the client sends no `limit`. */\nexport const DEFAULT_VECTOR_LIST_LIMIT = 10;\n/** Largest `limit` a client may ask for on any surface. Above it, the read is refused. */\nexport const MAX_LIST_LIMIT = 1000;\n\n/** Overridable bounds for {@link resolveClientListLimit}. */\nexport interface ListLimitBounds {\n /** Default page size for plain and text-search reads. */\n defaultLimit?: number;\n /** Default page size for vector-search reads. */\n vectorDefaultLimit?: number;\n /** Largest limit a client may ask for. A larger one is rejected, not clamped. */\n maxLimit?: number;\n}\n\n/**\n * Thrown by {@link resolveClientListLimit} for a `limit` the platform will not\n * serve. Carries an HTTP status so an ingress that speaks HTTP can forward it\n * verbatim, and `maxLimit` so one can be built without re-deriving the ceiling.\n *\n * @group Errors\n */\nexport class ListLimitError extends RebaseApiError {\n /** The ceiling that was exceeded — what the caller should page by instead. */\n readonly maxLimit: number;\n\n constructor(message: string, maxLimit: number) {\n super(message, { status: 400, code: \"INVALID_LIMIT\" });\n this.name = \"ListLimitError\";\n this.maxLimit = maxLimit;\n // Keeps `instanceof` working when this is compiled down for an older\n // target, where extending a builtin otherwise loses the prototype.\n Object.setPrototypeOf(this, ListLimitError.prototype);\n }\n}\n\n/**\n * Resolve a client-supplied list `limit` into a safe, always-defined value.\n *\n * - An absent / blank limit falls back to the mode default:\n * `vectorDefaultLimit` for a vector search, otherwise `defaultLimit`.\n * - A limit that is present must be an integer in `[1, maxLimit]`. Anything\n * else — `0`, a negative, `1.5`, `abc`, `100000000` — throws\n * {@link ListLimitError} rather than being coerced into range, because every\n * coercion answers a question the caller did not ask with a page it cannot\n * tell apart from the whole collection.\n *\n * The return is never `undefined` — no ingress that routes its client limit\n * through this can produce an unbounded read.\n *\n * @throws {ListLimitError} when a present `limit` is not an integer in range.\n */\nexport function resolveClientListLimit(\n rawLimit: number | string | null | undefined,\n opts: ListLimitBounds & { vectorSearch?: boolean } = {}\n): number {\n const maxLimit = opts.maxLimit ?? MAX_LIST_LIMIT;\n if (rawLimit != null && String(rawLimit).trim() !== \"\") {\n // `Number`, not `parseInt`: `parseInt(\"50rows\")` is 50, which silently\n // reads a typo as a window the caller never wrote.\n const parsed = typeof rawLimit === \"number\" ? rawLimit : Number(String(rawLimit).trim());\n if (!Number.isInteger(parsed) || parsed < 1) {\n throw new ListLimitError(\n `Invalid \\`limit\\`: ${String(rawLimit)}. Expected a whole number between 1 and ${maxLimit}.`,\n maxLimit\n );\n }\n if (parsed > maxLimit) {\n throw new ListLimitError(\n `\\`limit\\` ${parsed} is above the maximum of ${maxLimit}. Ask for at most ${maxLimit} rows ` +\n \"per read and page through the rest with `offset` — answering with a smaller page would be \" +\n \"indistinguishable from there being no more rows.\",\n maxLimit\n );\n }\n return parsed;\n }\n return opts.vectorSearch\n ? (opts.vectorDefaultLimit ?? DEFAULT_VECTOR_LIST_LIMIT)\n : (opts.defaultLimit ?? DEFAULT_LIST_LIMIT);\n}\n\n/**\n * @internal\n */\nexport interface FetchCollectionProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n collection?: CollectionConfig<M>;\n filter?: FilterValues<Extract<keyof M, string>>,\n /**\n * An `or(...)`/`and(...)` group, applied alongside `filter`.\n *\n * The REST layer parsed `?or=` into this and then had nowhere to put it, so\n * the group was dropped and the read ran unfiltered — returning every row\n * the caller's policies allowed rather than the ones they asked for.\n */\n logical?: LogicalCondition;\n limit?: number;\n offset?: number;\n startAfter?: unknown;\n orderBy?: string;\n searchString?: string;\n /** Ask each row which declared search field matched — populates `_matches`. */\n searchExplain?: boolean;\n order?: \"desc\" | \"asc\";\n /** Vector similarity search configuration */\n vectorSearch?: VectorSearchParams;\n}\n\n/**\n * @internal\n */\nexport type ListenCollectionProps<M extends Record<string, unknown> = Record<string, unknown>> =\n FetchCollectionProps<M> &\n {\n onUpdate: (rows: Record<string, unknown>[]) => void;\n onError?: (error: Error) => void;\n };\n\n/**\n * @internal\n */\nexport interface SaveProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n values: Partial<EntityValues<M>>;\n id?: string | number; // can be empty for new entities\n previousValues?: Partial<EntityValues<M>>;\n collection?: CollectionConfig<M>;\n status: EntityStatus;\n /**\n * Write the row with INSERT ... ON CONFLICT DO UPDATE on the primary key\n * instead of choosing between insert and update up front.\n *\n * One statement, so it does not lose the race a read-then-write can, and it\n * succeeds whether or not the row is already there — what a re-runnable\n * import needs. Requires every primary key column to be present; without\n * them there is no conflict target and the row is inserted normally.\n */\n upsert?: boolean;\n}\n\n/**\n * @internal\n */\nexport interface SaveManyProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n /**\n * The rows to write. A row carrying its primary key updates (or, with\n * `upsert`, inserts-or-updates) that row; one without inserts.\n */\n rows: Partial<EntityValues<M>>[];\n collection?: CollectionConfig<M>;\n /** Apply every row as INSERT ... ON CONFLICT DO UPDATE. See {@link SaveProps.upsert}. */\n upsert?: boolean;\n}\n\n/**\n * @internal\n */\nexport interface UpdateManyProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n /**\n * The rows to update, each named by its address.\n *\n * Distinct from {@link SaveManyProps.rows}, which carries keys *inside* the\n * values and is insert-shaped — `saveMany` passes `status: \"new\"` and no\n * `id`, so it cannot express \"update exactly this row\". This can, and it is\n * why bulk update is a separate driver method rather than a flag on that one.\n */\n updates: { id: string | number; values: Partial<EntityValues<M>> }[];\n collection?: CollectionConfig<M>;\n}\n\n/**\n * @internal\n */\nexport interface DeleteProps<M extends Record<string, unknown> = Record<string, unknown>> {\n row: { id: string | number; path: string; values?: Partial<EntityValues<M>> };\n collection?: CollectionConfig<M>;\n}\n\n/**\n * @internal\n */\nexport interface DeleteManyProps<M extends Record<string, unknown> = Record<string, unknown>> {\n path: string;\n ids: (string | number)[];\n collection?: CollectionConfig<M>;\n}\n\nexport type FilterCombinationValidProps = {\n path: string;\n databaseId?: string;\n collection: CollectionConfig;\n filterValues: FilterValues<string>;\n sortBy?: [string, \"asc\" | \"desc\"];\n};\n\n/**\n * The integration SPI for plugging a data backend into Rebase.\n *\n * Implement this interface to connect a custom backend (or use a built-in\n * driver such as the Firestore one) and register it on\n * `<Rebase dataSources>`. Rebase wraps drivers via `buildRebaseData` and\n * routes collections to them by their `dataSource` key.\n *\n * For *consuming* data in application code, use `RebaseData` /\n * `context.data` instead — this interface is only for providing it.\n *\n * @group Datasource\n */\nexport interface DataDriver {\n\n /**\n * Key that identifies this driver\n */\n key?: string;\n\n /**\n * If the driver has been initialised\n */\n initialised?: boolean;\n\n /**\n * Fetch data from a collection\n * @param props\n * @return Promise of flat rows\n */\n fetchCollection<M extends Record<string, unknown> = Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<Record<string, unknown>[]>;\n\n /**\n * Listen to a collection in a given path. If you don't implement this method\n * `fetchCollection` will be used instead, with no real time updates.\n * @param props\n * @return Function to cancel subscription\n */\n listenCollection?<M extends Record<string, unknown> = Record<string, unknown>>(props: ListenCollectionProps<M>): () => void;\n\n /**\n * Retrieve a single row given a path and a collection\n * @param props\n */\n fetchOne<M extends Record<string, unknown> = Record<string, unknown>>(props: FetchOneProps<M>): Promise<Record<string, unknown> | undefined>;\n\n /**\n * Get realtime updates on one row.\n * @param props\n * @return Function to cancel subscription\n */\n listenOne?<M extends Record<string, unknown> = Record<string, unknown>>(props: ListenOneProps<M>): () => void;\n\n /**\n * Save a row to the specified path\n * @param props\n */\n save<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveProps<M>): Promise<Record<string, unknown>>;\n\n /**\n * Save many rows as one unit of work.\n *\n * Every row runs the same pipeline as {@link save} — callbacks, relations\n * and row-level security all still apply — but they share a single\n * transaction, so the batch either lands whole or not at all. That, and the\n * single round trip, is what makes importing tens of thousands of rows\n * viable without dropping to raw SQL.\n *\n * Optional: drivers that cannot do this leave it undefined and callers fall\n * back to `save` per row.\n */\n saveMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: SaveManyProps<M>): Promise<Record<string, unknown>[]>;\n\n /**\n * Update many rows in one transaction, each addressed by id.\n *\n * Optional for the same reason `saveMany` is: a driver that cannot make the\n * batch atomic should not pretend to. The REST layer reports\n * `BULK_UNSUPPORTED` rather than silently falling back to a loop of single\n * writes, which would be neither atomic nor one round trip — the two things\n * a caller reaches for a batch to get.\n */\n updateMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: UpdateManyProps<M>): Promise<Record<string, unknown>[]>;\n\n /**\n * Delete a entity\n * @param props\n * @return was the whole deletion flow successful\n */\n delete<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteProps<M>): Promise<void>;\n\n /**\n * Delete all entities from a collection.\n * @param path Collection path\n */\n deleteAll?(path: string): Promise<void>;\n\n /**\n * Delete many rows in one transaction, addressed by id.\n *\n * Ids rather than a filter, deliberately — see\n * {@link SDKCollectionClient.deleteMany}. Optional, as `saveMany` is.\n */\n deleteMany?<M extends Record<string, unknown> = Record<string, unknown>>(props: DeleteManyProps<M>): Promise<void>;\n\n /**\n * Check if the given property is unique in the given collection\n * @param path Collection path\n * @param name of the property\n * @param value\n * @param id\n * @param collection\n * @return `true` if there are no other fields besides the given entity\n */\n checkUniqueField(\n path: string,\n name: string,\n value: unknown,\n id?: string | number,\n collection?: CollectionConfig\n ): Promise<boolean>;\n\n /**\n * Count the number of entities in a collection\n */\n count?<M extends Record<string, unknown> = Record<string, unknown>>(props: FetchCollectionProps<M>): Promise<number>;\n\n /**\n * Check if the given filter combination is valid\n * @param props\n */\n isFilterCombinationValid?(props: Omit<FilterCombinationValidProps, \"collection\"> & {\n databaseId?: string\n }): boolean;\n\n /**\n * Get the object to generate the current time in the driver\n */\n currentTime?: () => unknown;\n\n delegateToCMSModel?: (data: unknown) => unknown;\n\n cmsToDelegateModel?: (data: unknown) => unknown;\n\n initTextSearch?: (props: {\n context: RebaseCallContext,\n path: string,\n databaseId?: string,\n collection: CollectionConfig,\n parentCollectionSlugs?: string[];\n parentEntityIds?: string[];\n }) => Promise<boolean>;\n\n /**\n * Flag to indicate if the driver has requested the initialization of the text search index\n */\n needsInitTextSearch?: boolean;\n\n // ── REST fetch capabilities ─────────────────────────────────────────\n\n /**\n * Optional REST-optimised fetch service. When present, the REST API\n * generator uses these methods instead of the generic `fetchOne` /\n * `fetchCollection` pipeline, enabling include-aware eager-loading.\n */\n restFetchService?: RestFetchService;\n\n // ── Admin capabilities ─────────────────────────────────────────────\n //\n // Admin operations are now modelled as capability-specific interfaces\n // (SQLAdmin, DocumentAdmin, SchemaAdmin) in `@rebasepro/types/backend`.\n //\n // Drivers that support admin features should expose them here.\n // Consumers should use the `isSQLAdmin()`, `isSchemaAdmin()` etc.\n // type guards to safely narrow the type before calling methods.\n\n /**\n * Return the admin capabilities of this driver.\n * @see SQLAdmin\n * @see DocumentAdmin\n * @see SchemaAdmin\n */\n admin?: import(\"../types/backend\").DatabaseAdmin;\n\n}\n\n/**\n * REST-optimised fetch service exposed by drivers that support\n * eager-loading of relations via `include`.\n *\n * The methods return flattened rows — exactly the table's columns, under their\n * own names and with the types the database returned — and included relations\n * inlined as plain nested rows. This is the shape served to app developers\n * through the REST API / SDK client.\n *\n * No synthesized `id`: identity is a primary key, which may be named anything\n * and span several columns, so an address is derived by whoever needs one (see\n * `buildCompositeId`) rather than written into the row on top of the data.\n *\n * @group DataDriver\n */\nexport interface RestFetchService {\n /**\n * Fetch a collection of flattened entities with optional relation includes.\n */\n fetchCollectionForRest(\n collectionPath: string,\n options?: {\n filter?: FilterValues<string>;\n /** An `or(...)`/`and(...)` group, applied alongside `filter`. */\n logical?: LogicalCondition;\n orderBy?: string;\n order?: \"desc\" | \"asc\";\n limit?: number;\n offset?: number;\n startAfter?: Record<string, unknown>;\n searchString?: string;\n /** Ask each row which declared search fields matched — populates `_matches`. */\n searchExplain?: boolean;\n databaseId?: string;\n vectorSearch?: VectorSearchParams;\n },\n include?: string[]\n ): Promise<Record<string, unknown>[]>;\n\n /**\n * Fetch a single flattened entity with optional relation includes.\n */\n fetchOneForRest(\n collectionPath: string,\n id: string | number,\n include?: string[],\n databaseId?: string\n ): Promise<Record<string, unknown> | null>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmFA,IAAa,iBAAb,cAAoC,MAAM;;CAEtC;;CAEA;;CAEA;CAEA,YAAY,SAAiB,OAAwB,CAAC,GAAG;EACrD,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,SAAS,KAAK;EACnB,KAAK,OAAO,KAAK;EACjB,KAAK,UAAU,KAAK;EACpB,IAAI,KAAK,UAAU,KAAA,GAEf,KAA8B,QAAQ,KAAK;CAEnD;AACJ;;;;;;;;;;;;;;;;;;;;;ACnDA,IAAa,oBAAoB;;;;;;;;;;;;;;;;;;;;;;AAuBjC,IAAa,qBAAwC,CAAC,mBAAmB,MAAM;;AA2N/E,IAAa,SAAS;CAClB,aAAmC,EAAE,MAAM,OAAO;CAClD,cAAqC,EAAE,MAAM,QAAQ;CACrD,MAAM,GAAG,cAAgE;EAAE,MAAM;EAC3E;CAA+B;CACrC,KAAK,GAAG,cAA+D;EAAE,MAAM;EACzE;CAA+B;CACrC,MAAM,aAAoD;EAAE,MAAM;EACtE;CAAQ;CACJ,UAAU,MAAqB,IAA2B,WACrD;EAAE,MAAM;EACjB;EACA;EACA;CAAM;CACF,eAAe,WAA4D;EAAE,MAAM;EAChF;CAAkB;CACrB,eAAe,WAA4D;EAAE,MAAM;EAChF;CAAkB;CACrB,sBAAqD,EAAE,MAAM,gBAAgB;CAC7E,sBAAqD,EAAE,MAAM,gBAAgB;CAC7E,WAAW,UACN;EAAE,MAAM;EACjB,YAAY,KAAK;EACjB,OAAO,KAAK;CAAM;CACd,MAAM,SAAsC;EAAE,MAAM;EACxD;CAAI;CACA,QAAQ,UAAsC;EAAE,MAAM;EAC1D;CAAK;CACD,aAAa,UAA2C;EAAE,MAAM;EACpE;CAAK;CACD,UAAU,WAAmE;EAAE,MAAM;EACzF;CAAM;CACF,gBAAsC,EAAE,MAAM,UAAU;CACxD,kBAA0C,EAAE,MAAM,YAAY;AAClE;;;;;;;;AC5OA,IAAa,iBAAb,MAAa,uBAAuB,eAAe;;CAE/C;CAEA,YAAY,SAAiB,UAAkB;EAC3C,MAAM,SAAS;GAAE,QAAQ;GAAK,MAAM;EAAgB,CAAC;EACrD,KAAK,OAAO;EACZ,KAAK,WAAW;EAGhB,OAAO,eAAe,MAAM,eAAe,SAAS;CACxD;AACJ;;;;;;;;;;;;;;;;;AAkBA,SAAgB,uBACZ,UACA,OAAqD,CAAC,GAChD;CACN,MAAM,WAAW,KAAK,YAAA;CACtB,IAAI,YAAY,QAAQ,OAAO,QAAQ,CAAC,CAAC,KAAK,MAAM,IAAI;EAGpD,MAAM,SAAS,OAAO,aAAa,WAAW,WAAW,OAAO,OAAO,QAAQ,CAAC,CAAC,KAAK,CAAC;EACvF,IAAI,CAAC,OAAO,UAAU,MAAM,KAAK,SAAS,GACtC,MAAM,IAAI,eACN,sBAAsB,OAAO,QAAQ,EAAE,0CAA0C,SAAS,IAC1F,QACJ;EAEJ,IAAI,SAAS,UACT,MAAM,IAAI,eACN,aAAa,OAAO,2BAA2B,SAAS,oBAAoB,SAAS,qJAGrF,QACJ;EAEJ,OAAO;CACX;CACA,OAAO,KAAK,eACL,KAAK,sBAAA,KACL,KAAK,gBAAA;AAChB"}
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __createRequire } from "module";
2
2
  import "process";
3
3
  __createRequire(import.meta.url);
4
- import { readExistingSchema, t as planCollectionPolicies } from "./ensure-collection-tables-CBQdOETu.js";
4
+ import { readExistingSchema, t as planCollectionPolicies } from "./ensure-collection-tables-DzeTEvMv.js";
5
5
  //#region src/schema/ensure-collection-policies.ts
6
6
  var isCreatePolicy = (statement) => /^\s*CREATE POLICY/i.test(statement);
7
7
  /**
@@ -54,4 +54,4 @@ async function ensureCollectionPolicies(client, collections, log) {
54
54
  //#endregion
55
55
  export { ensureCollectionPolicies };
56
56
 
57
- //# sourceMappingURL=ensure-collection-policies-ViG8XiPn.js.map
57
+ //# sourceMappingURL=ensure-collection-policies-B_JMGa5K.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ensure-collection-policies-ViG8XiPn.js","names":[],"sources":["../src/schema/ensure-collection-policies.ts"],"sourcesContent":["/**\n * Applying a bundle's RLS policies to a database at boot, idempotently.\n *\n * ## Why this exists\n *\n * {@link ensureCollectionTables} creates the collection *tables* a managed\n * runtime boots against, but a table with row-level security disabled and no\n * policies is not servable: authenticated requests run as the restricted\n * `rebase_user` role, so a read with no `SELECT` policy returns nothing (a\n * public collection answered 401) and a write with no `INSERT`/`UPDATE` policy\n * is denied. The policies live in the collections' `securityRules`; nothing at\n * boot applied them. `rebase db push` does — but it drives Atlas against a\n * local `DATABASE_URL`, and a managed tenant's database is reachable only from\n * inside the cluster, by the runtime that is already connected to it. So the\n * runtime is the only thing that *can* apply them, and this is where it does.\n *\n * ## Why this is safe to run on every boot\n *\n * Every statement is idempotent: `ENABLE ROW LEVEL SECURITY` is a no-op once\n * enabled, and each policy is a `DROP POLICY IF EXISTS` immediately followed by\n * a `CREATE POLICY`, so re-applying asserts exactly the declared state. It adds\n * and replaces; it never drops data. (It does not *reconcile* — a policy a\n * previous push left behind under an old name is not removed here; that stays a\n * `db push` / `db migrate` concern, alongside destructive schema changes.)\n *\n * Unlike table creation, a failure here is not fatal: RLS stays enabled, so a\n * table whose policies could not be applied fails **closed** (denies) rather\n * than leaking rows. One collection's policy failing (e.g. a rule that\n * references a table a real migration has not created yet) must not crash-loop\n * the whole deployment and take the other collections' working routes down with\n * it. Failures are reported loudly and per-table so the operator can see\n * exactly which collection is not yet servable and why.\n */\nimport { type CollectionConfig } from \"@rebasepro/types\";\nimport { planCollectionPolicies } from \"./generate-postgres-ddl-logic\";\nimport { readExistingSchema, type Queryable } from \"./ensure-collection-tables\";\n\nexport interface PolicyEnsureResult {\n /** `CREATE POLICY` statements that ran successfully. */\n policiesApplied: number;\n /** Tables that had RLS enabled. */\n tablesSecured: number;\n /** Declared tables absent from the database — left to a real migration. */\n skipped: { table: string; reason: string }[];\n /** Tables whose RLS could not be fully applied (fail closed). */\n failures: { table: string; error: string }[];\n}\n\nconst isCreatePolicy = (statement: string): boolean => /^\\s*CREATE POLICY/i.test(statement);\n\n/**\n * Bring the declared collections' RLS policies up to date. Returns what it did.\n *\n * Only tables that already exist are touched: the boot-time table creator runs\n * first, so anything still missing is a table this additive path is not allowed\n * to create (a junction, or a relation left to a migration). Enabling RLS on a\n * non-existent table would error, so those are recorded as skipped, not failed.\n */\nexport async function ensureCollectionPolicies(\n client: Queryable,\n collections: CollectionConfig[],\n log?: (message: string) => void\n): Promise<PolicyEnsureResult> {\n const result: PolicyEnsureResult = { policiesApplied: 0, tablesSecured: 0, skipped: [], failures: [] };\n\n const plans = planCollectionPolicies(collections);\n if (plans.length === 0) return result;\n\n const schemas = Array.from(new Set(plans.map(p => p.schema)));\n const existing = await readExistingSchema(client, schemas);\n\n for (const plan of plans) {\n if (!existing.tables.has(plan.qualified)) {\n result.skipped.push({\n table: plan.qualified,\n reason: \"table is not present in the database; create it with `rebase db push` / `rebase db migrate`\"\n });\n continue;\n }\n\n try {\n // Enable first: if a later policy statement fails, the table is left\n // locked (deny-all for the user role) rather than open.\n await client.query(plan.enableRls);\n result.tablesSecured++;\n\n let created = 0;\n for (const statement of plan.policyStatements) {\n await client.query(statement);\n if (isCreatePolicy(statement)) {\n result.policiesApplied++;\n created++;\n }\n }\n log?.(`${plan.qualified}: RLS enabled, ${created} policy(ies) applied`);\n } catch (err) {\n result.failures.push({\n table: plan.qualified,\n error: err instanceof Error ? err.message : String(err)\n });\n }\n }\n\n return result;\n}\n"],"mappings":";;;;;AAgDA,IAAM,kBAAkB,cAA+B,qBAAqB,KAAK,SAAS;;;;;;;;;AAU1F,eAAsB,yBAClB,QACA,aACA,KAC2B;CAC3B,MAAM,SAA6B;EAAE,iBAAiB;EAAG,eAAe;EAAG,SAAS,CAAC;EAAG,UAAU,CAAC;CAAE;CAErG,MAAM,QAAQ,uBAAuB,WAAW;CAChD,IAAI,MAAM,WAAW,GAAG,OAAO;CAG/B,MAAM,WAAW,MAAM,mBAAmB,QAD1B,MAAM,KAAK,IAAI,IAAI,MAAM,KAAI,MAAK,EAAE,MAAM,CAAC,CACT,CAAO;CAEzD,KAAK,MAAM,QAAQ,OAAO;EACtB,IAAI,CAAC,SAAS,OAAO,IAAI,KAAK,SAAS,GAAG;GACtC,OAAO,QAAQ,KAAK;IAChB,OAAO,KAAK;IACZ,QAAQ;GACZ,CAAC;GACD;EACJ;EAEA,IAAI;GAGA,MAAM,OAAO,MAAM,KAAK,SAAS;GACjC,OAAO;GAEP,IAAI,UAAU;GACd,KAAK,MAAM,aAAa,KAAK,kBAAkB;IAC3C,MAAM,OAAO,MAAM,SAAS;IAC5B,IAAI,eAAe,SAAS,GAAG;KAC3B,OAAO;KACP;IACJ;GACJ;GACA,MAAM,GAAG,KAAK,UAAU,iBAAiB,QAAQ,qBAAqB;EAC1E,SAAS,KAAK;GACV,OAAO,SAAS,KAAK;IACjB,OAAO,KAAK;IACZ,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;GAC1D,CAAC;EACL;CACJ;CAEA,OAAO;AACX"}
1
+ {"version":3,"file":"ensure-collection-policies-B_JMGa5K.js","names":[],"sources":["../src/schema/ensure-collection-policies.ts"],"sourcesContent":["/**\n * Applying a bundle's RLS policies to a database at boot, idempotently.\n *\n * ## Why this exists\n *\n * {@link ensureCollectionTables} creates the collection *tables* a managed\n * runtime boots against, but a table with row-level security disabled and no\n * policies is not servable: authenticated requests run as the restricted\n * `rebase_user` role, so a read with no `SELECT` policy returns nothing (a\n * public collection answered 401) and a write with no `INSERT`/`UPDATE` policy\n * is denied. The policies live in the collections' `securityRules`; nothing at\n * boot applied them. `rebase db push` does — but it drives Atlas against a\n * local `DATABASE_URL`, and a managed tenant's database is reachable only from\n * inside the cluster, by the runtime that is already connected to it. So the\n * runtime is the only thing that *can* apply them, and this is where it does.\n *\n * ## Why this is safe to run on every boot\n *\n * Every statement is idempotent: `ENABLE ROW LEVEL SECURITY` is a no-op once\n * enabled, and each policy is a `DROP POLICY IF EXISTS` immediately followed by\n * a `CREATE POLICY`, so re-applying asserts exactly the declared state. It adds\n * and replaces; it never drops data. (It does not *reconcile* — a policy a\n * previous push left behind under an old name is not removed here; that stays a\n * `db push` / `db migrate` concern, alongside destructive schema changes.)\n *\n * Unlike table creation, a failure here is not fatal: RLS stays enabled, so a\n * table whose policies could not be applied fails **closed** (denies) rather\n * than leaking rows. One collection's policy failing (e.g. a rule that\n * references a table a real migration has not created yet) must not crash-loop\n * the whole deployment and take the other collections' working routes down with\n * it. Failures are reported loudly and per-table so the operator can see\n * exactly which collection is not yet servable and why.\n */\nimport { type CollectionConfig } from \"@rebasepro/types\";\nimport { planCollectionPolicies } from \"./generate-postgres-ddl-logic\";\nimport { readExistingSchema, type Queryable } from \"./ensure-collection-tables\";\n\nexport interface PolicyEnsureResult {\n /** `CREATE POLICY` statements that ran successfully. */\n policiesApplied: number;\n /** Tables that had RLS enabled. */\n tablesSecured: number;\n /** Declared tables absent from the database — left to a real migration. */\n skipped: { table: string; reason: string }[];\n /** Tables whose RLS could not be fully applied (fail closed). */\n failures: { table: string; error: string }[];\n}\n\nconst isCreatePolicy = (statement: string): boolean => /^\\s*CREATE POLICY/i.test(statement);\n\n/**\n * Bring the declared collections' RLS policies up to date. Returns what it did.\n *\n * Only tables that already exist are touched: the boot-time table creator runs\n * first, so anything still missing is a table this additive path is not allowed\n * to create (a junction, or a relation left to a migration). Enabling RLS on a\n * non-existent table would error, so those are recorded as skipped, not failed.\n */\nexport async function ensureCollectionPolicies(\n client: Queryable,\n collections: CollectionConfig[],\n log?: (message: string) => void\n): Promise<PolicyEnsureResult> {\n const result: PolicyEnsureResult = { policiesApplied: 0, tablesSecured: 0, skipped: [], failures: [] };\n\n const plans = planCollectionPolicies(collections);\n if (plans.length === 0) return result;\n\n const schemas = Array.from(new Set(plans.map(p => p.schema)));\n const existing = await readExistingSchema(client, schemas);\n\n for (const plan of plans) {\n if (!existing.tables.has(plan.qualified)) {\n result.skipped.push({\n table: plan.qualified,\n reason: \"table is not present in the database; create it with `rebase db push` / `rebase db migrate`\"\n });\n continue;\n }\n\n try {\n // Enable first: if a later policy statement fails, the table is left\n // locked (deny-all for the user role) rather than open.\n await client.query(plan.enableRls);\n result.tablesSecured++;\n\n let created = 0;\n for (const statement of plan.policyStatements) {\n await client.query(statement);\n if (isCreatePolicy(statement)) {\n result.policiesApplied++;\n created++;\n }\n }\n log?.(`${plan.qualified}: RLS enabled, ${created} policy(ies) applied`);\n } catch (err) {\n result.failures.push({\n table: plan.qualified,\n error: err instanceof Error ? err.message : String(err)\n });\n }\n }\n\n return result;\n}\n"],"mappings":";;;;;AAgDA,IAAM,kBAAkB,cAA+B,qBAAqB,KAAK,SAAS;;;;;;;;;AAU1F,eAAsB,yBAClB,QACA,aACA,KAC2B;CAC3B,MAAM,SAA6B;EAAE,iBAAiB;EAAG,eAAe;EAAG,SAAS,CAAC;EAAG,UAAU,CAAC;CAAE;CAErG,MAAM,QAAQ,uBAAuB,WAAW;CAChD,IAAI,MAAM,WAAW,GAAG,OAAO;CAG/B,MAAM,WAAW,MAAM,mBAAmB,QAD1B,MAAM,KAAK,IAAI,IAAI,MAAM,KAAI,MAAK,EAAE,MAAM,CAAC,CACT,CAAO;CAEzD,KAAK,MAAM,QAAQ,OAAO;EACtB,IAAI,CAAC,SAAS,OAAO,IAAI,KAAK,SAAS,GAAG;GACtC,OAAO,QAAQ,KAAK;IAChB,OAAO,KAAK;IACZ,QAAQ;GACZ,CAAC;GACD;EACJ;EAEA,IAAI;GAGA,MAAM,OAAO,MAAM,KAAK,SAAS;GACjC,OAAO;GAEP,IAAI,UAAU;GACd,KAAK,MAAM,aAAa,KAAK,kBAAkB;IAC3C,MAAM,OAAO,MAAM,SAAS;IAC5B,IAAI,eAAe,SAAS,GAAG;KAC3B,OAAO;KACP;IACJ;GACJ;GACA,MAAM,GAAG,KAAK,UAAU,iBAAiB,QAAQ,qBAAqB;EAC1E,SAAS,KAAK;GACV,OAAO,SAAS,KAAK;IACjB,OAAO,KAAK;IACZ,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;GAC1D,CAAC;EACL;CACJ;CAEA,OAAO;AACX"}