@rebasepro/server-postgres 0.12.1-canary.gf5f1d39 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PostgresBackendDriver.d.ts +1 -1
- package/dist/PostgresBootstrapper.d.ts +25 -1
- package/dist/auth/services.d.ts +21 -0
- package/dist/backup/backup-service.d.ts +10 -1
- package/dist/backup/pg-tools.d.ts +47 -0
- package/dist/backup-service-CD8o_1Sl.js +8999 -0
- package/dist/backup-service-CD8o_1Sl.js.map +1 -0
- package/dist/cli-helpers.d.ts +39 -0
- package/dist/connection-BuZ97wsr.js +250 -0
- package/dist/connection-BuZ97wsr.js.map +1 -0
- package/dist/connection.d.ts +42 -0
- package/dist/ensure-collection-policies-ViG8XiPn.js +57 -0
- package/dist/ensure-collection-policies-ViG8XiPn.js.map +1 -0
- package/dist/ensure-collection-tables-CBQdOETu.js +650 -0
- package/dist/ensure-collection-tables-CBQdOETu.js.map +1 -0
- package/dist/index.es.js +1231 -10031
- package/dist/index.es.js.map +1 -1
- package/dist/policy-CeA1JcxP.js +105 -0
- package/dist/policy-CeA1JcxP.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +83 -144
- package/dist/schema/ensure-collection-policies.d.ts +60 -0
- package/dist/schema/ensure-collection-tables.d.ts +44 -2
- package/dist/schema/generate-postgres-ddl-logic.d.ts +135 -1
- package/dist/schema/introspect-db-constraints.d.ts +57 -0
- package/dist/schema/introspect-db-logic.d.ts +94 -5
- package/dist/schema/introspect-db-queries.d.ts +119 -0
- package/dist/schema/introspect-db-structure.d.ts +263 -0
- package/dist/schema/introspect-db-types.d.ts +11 -0
- package/dist/services/FetchService.d.ts +4 -1
- package/dist/services/RelationService.d.ts +24 -1
- package/dist/services/channel-bus/index.d.ts +1 -7
- package/dist/services/collection-helpers.d.ts +36 -2
- package/dist/services/dataService.d.ts +3 -1
- package/dist/services/row-pipeline.d.ts +1 -1
- package/dist/{src-BbFOPJ1S.js → src-DlPBctw_.js} +299 -173
- package/dist/src-DlPBctw_.js.map +1 -0
- package/dist/{src-Zqwaw3P5.js → src-DoU9yPqq.js} +3 -159
- package/dist/src-DoU9yPqq.js.map +1 -0
- package/dist/utils/connection-string.d.ts +29 -0
- package/dist/utils/drizzle-conditions.d.ts +5 -4
- package/dist/utils/pg-error-utils.d.ts +35 -0
- package/dist/websocket-B2LsrINK.js +530 -0
- package/dist/websocket-B2LsrINK.js.map +1 -0
- package/package.json +14 -14
- package/src/PostgresAdapter.ts +21 -2
- package/src/PostgresBackendDriver.ts +4 -0
- package/src/PostgresBootstrapper.ts +192 -33
- package/src/auth/ensure-tables.ts +164 -9
- package/src/auth/services.ts +24 -2
- package/src/backup/backup-cli.ts +41 -2
- package/src/backup/backup-service.ts +38 -5
- package/src/backup/pg-tools.ts +96 -3
- package/src/cli-helpers.ts +70 -0
- package/src/cli.ts +44 -26
- package/src/collections/validate-relations.ts +15 -0
- package/src/connection.ts +73 -0
- package/src/data-transformer.ts +9 -3
- package/src/databasePoolManager.ts +5 -2
- package/src/schema/auth-schema.ts +30 -19
- package/src/schema/ensure-collection-policies.ts +105 -0
- package/src/schema/ensure-collection-tables.test.ts +105 -9
- package/src/schema/ensure-collection-tables.ts +220 -32
- package/src/schema/generate-drizzle-schema-logic.ts +42 -6
- package/src/schema/generate-postgres-ddl-logic.ts +382 -19
- package/src/schema/introspect-db-constraints.ts +385 -0
- package/src/schema/introspect-db-inference.ts +18 -8
- package/src/schema/introspect-db-logic.ts +385 -71
- package/src/schema/introspect-db-queries.ts +326 -0
- package/src/schema/introspect-db-structure.ts +670 -0
- package/src/schema/introspect-db-types.ts +56 -0
- package/src/schema/introspect-db.ts +37 -80
- package/src/schema/introspect-runtime.test.ts +56 -8
- package/src/schema/introspect-runtime.ts +31 -9
- package/src/security/policy-drift.test.ts +11 -3
- package/src/services/BranchService.ts +66 -28
- package/src/services/FetchService.ts +90 -14
- package/src/services/PersistService.ts +20 -6
- package/src/services/RelationService.ts +249 -48
- package/src/services/channel-bus/index.ts +0 -9
- package/src/services/collection-helpers.ts +69 -3
- package/src/services/dataService.ts +3 -1
- package/src/services/realtimeService.ts +3 -3
- package/src/services/row-pipeline.ts +1 -1
- package/src/utils/connection-string.ts +58 -0
- package/src/utils/drizzle-conditions.ts +31 -6
- package/src/utils/pg-error-utils.ts +65 -0
- package/src/websocket.ts +18 -9
- package/dist/chunk-DSJWtz9O.js +0 -40
- package/dist/ensure-collection-tables-CNTcZGvn.js +0 -304
- package/dist/ensure-collection-tables-CNTcZGvn.js.map +0 -1
- package/dist/src-BbFOPJ1S.js.map +0 -1
- package/dist/src-Zqwaw3P5.js.map +0 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection-string rewrites for the tools Rebase shells out to.
|
|
3
|
+
*
|
|
4
|
+
* Pure and dependency-free: `pg-tools` builds argument vectors without a live
|
|
5
|
+
* server, and these have to be usable from there.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The `sslmode` values libpq accepts. `no-verify` is conspicuously not one.
|
|
10
|
+
*/
|
|
11
|
+
const LIBPQ_SSLMODES = new Set(["disable", "allow", "prefer", "require", "verify-ca", "verify-full"]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Make a connection string safe to hand to a libpq program.
|
|
15
|
+
*
|
|
16
|
+
* `sslmode=no-verify` is a node-postgres convention — encrypt, but do not check
|
|
17
|
+
* the certificate — and libpq does not have it. It does not degrade or warn:
|
|
18
|
+
*
|
|
19
|
+
* psql: error: invalid sslmode value: "no-verify"
|
|
20
|
+
*
|
|
21
|
+
* which means a `DATABASE_URL` that works perfectly for the app makes `psql`,
|
|
22
|
+
* `pg_dump`, `pg_restore` and Atlas all refuse to start, with an error that
|
|
23
|
+
* points at the value rather than at the convention it comes from. It has cost
|
|
24
|
+
* this project time more than once.
|
|
25
|
+
*
|
|
26
|
+
* `require` is the honest translation: libpq's `require` encrypts and does not
|
|
27
|
+
* verify the certificate either, which is exactly what `no-verify` asks for.
|
|
28
|
+
* Nothing is relaxed by the rewrite — `verify-ca` and `verify-full` are left
|
|
29
|
+
* alone, so a connection string that asked for verification still gets it.
|
|
30
|
+
*
|
|
31
|
+
* Only `sslmode` is touched, and only when it is a value libpq would reject.
|
|
32
|
+
* Anything unparseable is returned as given: a connection that works
|
|
33
|
+
* unverified beats one this corrupted.
|
|
34
|
+
*/
|
|
35
|
+
export function forLibpq(connectionString: string): string {
|
|
36
|
+
let url: URL;
|
|
37
|
+
try {
|
|
38
|
+
url = new URL(connectionString);
|
|
39
|
+
} catch {
|
|
40
|
+
// Key/value DSNs and anything else we cannot parse. A `sslmode` in one
|
|
41
|
+
// of those is space-separated rather than a query parameter, and
|
|
42
|
+
// rewriting it with a regex is how a password containing "sslmode="
|
|
43
|
+
// gets mangled.
|
|
44
|
+
return connectionString;
|
|
45
|
+
}
|
|
46
|
+
if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") return connectionString;
|
|
47
|
+
|
|
48
|
+
const sslmode = url.searchParams.get("sslmode");
|
|
49
|
+
if (sslmode === null || LIBPQ_SSLMODES.has(sslmode)) return connectionString;
|
|
50
|
+
|
|
51
|
+
url.searchParams.set("sslmode", "require");
|
|
52
|
+
// Re-serialize by hand for the reason `pinSearchPath` does: URLSearchParams
|
|
53
|
+
// writes a space as `+`, which node-postgres decodes and libpq does not.
|
|
54
|
+
url.search = Array.from(url.searchParams.entries())
|
|
55
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
56
|
+
.join("&");
|
|
57
|
+
return url.toString();
|
|
58
|
+
}
|
|
@@ -175,10 +175,11 @@ export class DrizzleConditionBuilder {
|
|
|
175
175
|
static buildRelationScopeCondition(
|
|
176
176
|
relation: ResolvedRelation,
|
|
177
177
|
/**
|
|
178
|
-
* Lazy:
|
|
179
|
-
*
|
|
180
|
-
* the parent's *id* alone, and
|
|
181
|
-
* a child listing fail on a
|
|
178
|
+
* Lazy: `via`, `belongsTo`, and a foreign key that points at a
|
|
179
|
+
* `sourceKey` need the parent's own table. A junction and a plain
|
|
180
|
+
* foreign key are expressible from the parent's *id* alone, and
|
|
181
|
+
* requiring the table for them would make a child listing fail on a
|
|
182
|
+
* parent whose table isn't registered.
|
|
182
183
|
*/
|
|
183
184
|
parent: () => { table: PgTable<any>; idColumn: AnyPgColumn },
|
|
184
185
|
parentId: string | number,
|
|
@@ -239,7 +240,16 @@ export class DrizzleConditionBuilder {
|
|
|
239
240
|
`relation '${relation.relationName}'.`
|
|
240
241
|
);
|
|
241
242
|
}
|
|
242
|
-
return eq(fkColumn, parentId);
|
|
243
|
+
if (!relation.sourceKey) return eq(fkColumn, parentId);
|
|
244
|
+
|
|
245
|
+
// A link on a natural key: the foreign key holds a column of the
|
|
246
|
+
// parent row, not its id, so the parent has to be read. As a
|
|
247
|
+
// subquery rather than a prior SELECT, for the same reason
|
|
248
|
+
// `belongsTo` below is — one statement sees one snapshot, and a
|
|
249
|
+
// scope condition that read the key separately could be built
|
|
250
|
+
// from a value the very next statement no longer agrees with.
|
|
251
|
+
const { table, idColumn } = parent();
|
|
252
|
+
return sql`${fkColumn} = (SELECT ${sql.identifier(relation.sourceKey)} FROM ${table} WHERE ${idColumn} = ${parentId})`;
|
|
243
253
|
}
|
|
244
254
|
|
|
245
255
|
case "belongsTo": {
|
|
@@ -385,7 +395,22 @@ export class DrizzleConditionBuilder {
|
|
|
385
395
|
// inverse, so reversing it into a filter is a different problem
|
|
386
396
|
// from the two shapes below rather than a third case of them.
|
|
387
397
|
if (relation && (hasForeignKeyOnTarget(relation) || isManyToMany(relation)) && registry && sourceIdColumn) {
|
|
388
|
-
|
|
398
|
+
// The `EXISTS` correlates the target's foreign key with a column
|
|
399
|
+
// on *this* table, and that is the primary key only when the
|
|
400
|
+
// link joins on it. A `sourceKey` names a different one, and
|
|
401
|
+
// correlating on the id anyway silently matches nothing —
|
|
402
|
+
// "filter by this relation" would quietly return zero rows.
|
|
403
|
+
const correlationColumn = hasForeignKeyOnTarget(relation) && relation.sourceKey
|
|
404
|
+
? columnAt(relation.sourceKey)
|
|
405
|
+
: sourceIdColumn;
|
|
406
|
+
if (!correlationColumn) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
`\`sourceKey: "${(relation as ResolvedForeignKeyOnTarget).sourceKey}"\` on relation ` +
|
|
409
|
+
`'${relation.relationName}' is not a column on '${collectionPath}', so a filter on ` +
|
|
410
|
+
"that relation has nothing to correlate against."
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
return { kind: "relation", relation, registry, sourceIdColumn: correlationColumn };
|
|
389
414
|
}
|
|
390
415
|
}
|
|
391
416
|
|
|
@@ -79,6 +79,25 @@ export function extractPgError(error: unknown): PostgresError | null {
|
|
|
79
79
|
return null;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Whether the failure came back from Postgres rather than from building the
|
|
84
|
+
* query — which decides whether a fallback query is worth issuing.
|
|
85
|
+
*
|
|
86
|
+
* Reads here run inside a transaction (that is where `SET LOCAL ROLE` binds
|
|
87
|
+
* RLS). Once a statement raises, that transaction is aborted, and every later
|
|
88
|
+
* statement on it returns `25P02` — "current transaction is aborted, commands
|
|
89
|
+
* ignored until end of transaction block". So a retry after a database error
|
|
90
|
+
* cannot succeed, and it replaces a precise diagnosis ("invalid input syntax
|
|
91
|
+
* for type uuid") with a generic one. Rethrow instead.
|
|
92
|
+
*
|
|
93
|
+
* A query the driver could not even build — a missing reciprocal relation, say
|
|
94
|
+
* — never reached Postgres, leaves the transaction usable, and is exactly what
|
|
95
|
+
* the fallback paths exist for.
|
|
96
|
+
*/
|
|
97
|
+
export function reachedDatabase(error: unknown): boolean {
|
|
98
|
+
return extractPgError(error) !== null;
|
|
99
|
+
}
|
|
100
|
+
|
|
82
101
|
/**
|
|
83
102
|
* Walk the error cause chain and return the deepest meaningful message.
|
|
84
103
|
*/
|
|
@@ -97,6 +116,52 @@ export function extractCauseMessage(error: unknown): string | null {
|
|
|
97
116
|
return null;
|
|
98
117
|
}
|
|
99
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Codes that mean "this connection will never work as configured".
|
|
121
|
+
*
|
|
122
|
+
* A wrong password or a database that does not exist is a settled fact about
|
|
123
|
+
* the connection string, not a transient fault — retrying produces the same
|
|
124
|
+
* answer forever.
|
|
125
|
+
*/
|
|
126
|
+
const UNRECOVERABLE_CONNECT_CODES = new Set([
|
|
127
|
+
"28P01", // invalid_password
|
|
128
|
+
"28000", // invalid_authorization_specification
|
|
129
|
+
"3D000", // invalid_catalog_name — the database does not exist
|
|
130
|
+
"42501" // insufficient_privilege
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
export interface ConnectFailure {
|
|
134
|
+
/** True when retrying cannot help: the connection string itself is wrong. */
|
|
135
|
+
fatal: boolean;
|
|
136
|
+
/** The deepest message available — the Postgres one where there is one. */
|
|
137
|
+
reason: string;
|
|
138
|
+
/** The `SQLSTATE`, when the failure came from Postgres rather than the socket. */
|
|
139
|
+
code?: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Describe a failed connection attempt in terms a developer can act on.
|
|
144
|
+
*
|
|
145
|
+
* The error a caller catches is Drizzle's wrapper: its message is
|
|
146
|
+
* `Failed query: SELECT 1` and its stack runs through drizzle internals, while
|
|
147
|
+
* the sentence that says what is actually wrong — "password authentication
|
|
148
|
+
* failed for user …", "database … does not exist" — sits in `.cause`. Logging
|
|
149
|
+
* the wrapper, as the bootstrapper used to, tells a developer with a typo in
|
|
150
|
+
* their `DATABASE_URL` nothing at all.
|
|
151
|
+
*/
|
|
152
|
+
export function classifyConnectFailure(error: unknown): ConnectFailure {
|
|
153
|
+
const pgError = extractPgError(error);
|
|
154
|
+
const reason =
|
|
155
|
+
pgError?.message ??
|
|
156
|
+
extractCauseMessage(error) ??
|
|
157
|
+
(error instanceof Error ? error.message : String(error));
|
|
158
|
+
return {
|
|
159
|
+
fatal: Boolean(pgError?.code && UNRECOVERABLE_CONNECT_CODES.has(pgError.code)),
|
|
160
|
+
reason,
|
|
161
|
+
code: pgError?.code
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
100
165
|
/**
|
|
101
166
|
* Detect whether an error is specifically a role-switching permission failure
|
|
102
167
|
* (e.g. "permission denied to set role" or "must be member of role"),
|
package/src/websocket.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { RealtimeService } from "./services/realtimeService";
|
|
2
2
|
import { PostgresBackendDriver } from "./PostgresBackendDriver";
|
|
3
3
|
import type { DataDriver, DeleteProps, FetchCollectionProps, FetchOneProps, SaveProps, TableMetadata, BranchInfo, AuthAdapter } from "@rebasepro/types";
|
|
4
|
-
import { isSQLAdmin, isSchemaAdmin } from "@rebasepro/types";
|
|
4
|
+
import { ANONYMOUS_USER_ID, isSQLAdmin, isSchemaAdmin } from "@rebasepro/types";
|
|
5
5
|
import type { User } from "@rebasepro/types";
|
|
6
6
|
|
|
7
7
|
import { WebSocketServer, WebSocket } from "ws";
|
|
8
8
|
import { Server } from "http";
|
|
9
9
|
import { inspect } from "util";
|
|
10
|
-
import { extractUserFromToken, AccessTokenPayload, safeCompare } from "@rebasepro/server";
|
|
10
|
+
import { extractUserFromToken, AccessTokenPayload, safeCompare, resolveRequireAuth } from "@rebasepro/server";
|
|
11
11
|
import { logger } from "@rebasepro/server";
|
|
12
12
|
|
|
13
13
|
/** Minimal subset of RebaseAuthConfig used by the WebSocket layer. */
|
|
@@ -116,11 +116,20 @@ export function createPostgresWebSocket(
|
|
|
116
116
|
logger.error("❌ [WebSocket Server] Error", { error: err });
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
// The same predicate the HTTP data routes use, from the same function —
|
|
120
|
+
// this socket is the other enforcement point for one product decision, and
|
|
121
|
+
// while it computed the answer itself it computed a different one. See
|
|
122
|
+
// `resolveRequireAuth` for what its local copy got wrong and why a `false`
|
|
123
|
+
// here grants access rather than skipping a check.
|
|
124
|
+
const requireAuth = !!authAdapter || resolveRequireAuth(authConfig as never);
|
|
125
|
+
|
|
126
|
+
if (requireAuth && !authAdapter && !authConfig?.jwtSecret && !authConfig?.serviceKey) {
|
|
127
|
+
logger.warn(
|
|
128
|
+
"🔐 [WebSocket Server] Authentication is required but no adapter, jwtSecret or " +
|
|
129
|
+
"serviceKey is configured — no client can complete AUTH, so every realtime " +
|
|
130
|
+
"message will be refused with UNAUTHORIZED."
|
|
131
|
+
);
|
|
132
|
+
}
|
|
124
133
|
|
|
125
134
|
wss.on("connection", (ws) => {
|
|
126
135
|
const clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
@@ -281,7 +290,7 @@ roles: verifiedUser.roles }
|
|
|
281
290
|
roles: session.user.roles ?? []
|
|
282
291
|
}
|
|
283
292
|
: {
|
|
284
|
-
uid:
|
|
293
|
+
uid: ANONYMOUS_USER_ID,
|
|
285
294
|
displayName: null,
|
|
286
295
|
email: null,
|
|
287
296
|
photoURL: null,
|
|
@@ -620,7 +629,7 @@ colors: true }));
|
|
|
620
629
|
const authContext = session?.user
|
|
621
630
|
? { uid: session.user.uid,
|
|
622
631
|
roles: session.user.roles ?? [] }
|
|
623
|
-
: { uid:
|
|
632
|
+
: { uid: ANONYMOUS_USER_ID,
|
|
624
633
|
roles: ["anon"] };
|
|
625
634
|
// Let RealtimeService handle these messages
|
|
626
635
|
await realtimeService.handleClientMessage(clientId, {
|
package/dist/chunk-DSJWtz9O.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { createRequire as __createRequire } from "module";
|
|
2
|
-
import "process";
|
|
3
|
-
const require = __createRequire(import.meta.url);
|
|
4
|
-
//#region \0rolldown/runtime.js
|
|
5
|
-
var __create = Object.create;
|
|
6
|
-
var __defProp = Object.defineProperty;
|
|
7
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
8
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
9
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
10
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
11
|
-
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
12
|
-
var __exportAll = (all, no_symbols) => {
|
|
13
|
-
let target = {};
|
|
14
|
-
for (var name in all) __defProp(target, name, {
|
|
15
|
-
get: all[name],
|
|
16
|
-
enumerable: true
|
|
17
|
-
});
|
|
18
|
-
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
19
|
-
return target;
|
|
20
|
-
};
|
|
21
|
-
var __copyProps = (to, from, except, desc) => {
|
|
22
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
23
|
-
key = keys[i];
|
|
24
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
25
|
-
get: ((k) => from[k]).bind(null, key),
|
|
26
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
return to;
|
|
30
|
-
};
|
|
31
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
32
|
-
value: mod,
|
|
33
|
-
enumerable: true
|
|
34
|
-
}) : target, mod));
|
|
35
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) {
|
|
36
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
37
|
-
throw Error("Calling `require` for \"" + x + "\" in an environment that doesn't expose the `require` function. See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.");
|
|
38
|
-
});
|
|
39
|
-
//#endregion
|
|
40
|
-
export { __toESM as i, __exportAll as n, __require as r, __commonJSMin as t };
|
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
import { createRequire as __createRequire } from "module";
|
|
2
|
-
import "process";
|
|
3
|
-
__createRequire(import.meta.url);
|
|
4
|
-
import { l as isPostgresCollectionConfig } from "./src-Zqwaw3P5.js";
|
|
5
|
-
import { A as toSnakeCase, g as getTableName, p as findRelation, v as resolveCollectionRelations } from "./src-BbFOPJ1S.js";
|
|
6
|
-
//#region src/schema/generate-postgres-ddl-logic.ts
|
|
7
|
-
var resolveColumnName = (propName, prop) => {
|
|
8
|
-
if (prop && "columnName" in prop && typeof prop.columnName === "string") return prop.columnName;
|
|
9
|
-
return toSnakeCase(propName);
|
|
10
|
-
};
|
|
11
|
-
var getPrimaryKeyProp = (collection) => {
|
|
12
|
-
if (collection.properties) {
|
|
13
|
-
const idPropEntry = Object.entries(collection.properties).find(([_, prop]) => "isId" in prop && Boolean(prop.isId));
|
|
14
|
-
if (idPropEntry) {
|
|
15
|
-
const prop = idPropEntry[1];
|
|
16
|
-
const isUuid = prop.type === "string" && "isId" in prop && prop.isId === "uuid";
|
|
17
|
-
return {
|
|
18
|
-
name: idPropEntry[0],
|
|
19
|
-
type: prop.type === "number" ? "number" : "string",
|
|
20
|
-
isUuid
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
const idProp = collection.properties?.["id"];
|
|
25
|
-
if (idProp?.type === "number") return {
|
|
26
|
-
name: "id",
|
|
27
|
-
type: "number",
|
|
28
|
-
isUuid: false
|
|
29
|
-
};
|
|
30
|
-
return {
|
|
31
|
-
name: "id",
|
|
32
|
-
type: "string",
|
|
33
|
-
isUuid: idProp?.type === "string" && "isId" in idProp && idProp.isId === "uuid"
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
var isIdProperty = (propName, prop, collection) => {
|
|
37
|
-
if ("isId" in prop && Boolean(prop.isId)) return true;
|
|
38
|
-
return !Object.values(collection.properties ?? {}).some((p) => "isId" in p && Boolean(p.isId)) && propName === "id";
|
|
39
|
-
};
|
|
40
|
-
var getSqlColumnType = (propName, prop, collection, collections) => {
|
|
41
|
-
switch (prop.type) {
|
|
42
|
-
case "string": {
|
|
43
|
-
const stringProp = prop;
|
|
44
|
-
if (stringProp.enum) {
|
|
45
|
-
const tableName = getTableName(collection);
|
|
46
|
-
const colName = resolveColumnName(propName, prop);
|
|
47
|
-
return `"${isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public"}"."${tableName}_${colName}"`;
|
|
48
|
-
}
|
|
49
|
-
if (stringProp.isId === "uuid" || stringProp.columnType === "uuid") return "UUID";
|
|
50
|
-
if (stringProp.columnType === "char") return "CHAR(255)";
|
|
51
|
-
if (stringProp.columnType === "varchar") return "VARCHAR(255)";
|
|
52
|
-
return "TEXT";
|
|
53
|
-
}
|
|
54
|
-
case "number": {
|
|
55
|
-
const numProp = prop;
|
|
56
|
-
const isId = isIdProperty(propName, prop, collection);
|
|
57
|
-
if ("isId" in numProp && numProp.isId === "increment") return "INTEGER GENERATED BY DEFAULT AS IDENTITY";
|
|
58
|
-
if (numProp.columnType) {
|
|
59
|
-
if (numProp.columnType === "double precision") return "DOUBLE PRECISION";
|
|
60
|
-
return numProp.columnType.toUpperCase();
|
|
61
|
-
}
|
|
62
|
-
return numProp.validation?.integer || isId ? "INTEGER" : "NUMERIC";
|
|
63
|
-
}
|
|
64
|
-
case "boolean": return "BOOLEAN";
|
|
65
|
-
case "date": {
|
|
66
|
-
const dateProp = prop;
|
|
67
|
-
if (dateProp.columnType === "date") return "DATE";
|
|
68
|
-
if (dateProp.columnType === "time") return "TIME";
|
|
69
|
-
return "TIMESTAMP WITH TIME ZONE";
|
|
70
|
-
}
|
|
71
|
-
case "map": return prop.columnType === "json" ? "JSON" : "JSONB";
|
|
72
|
-
case "array": {
|
|
73
|
-
const arrayProp = prop;
|
|
74
|
-
let colType = arrayProp.columnType;
|
|
75
|
-
if (!colType && arrayProp.of && !Array.isArray(arrayProp.of)) {
|
|
76
|
-
const ofProp = arrayProp.of;
|
|
77
|
-
if (ofProp.type === "string") colType = "text[]";
|
|
78
|
-
else if (ofProp.type === "number") colType = ofProp.validation?.integer ? "integer[]" : "numeric[]";
|
|
79
|
-
else if (ofProp.type === "boolean") colType = "boolean[]";
|
|
80
|
-
}
|
|
81
|
-
if (colType === "json") return "JSON";
|
|
82
|
-
if (colType === "text[]") return "TEXT[]";
|
|
83
|
-
if (colType === "integer[]") return "INTEGER[]";
|
|
84
|
-
if (colType === "boolean[]") return "BOOLEAN[]";
|
|
85
|
-
if (colType === "numeric[]") return "NUMERIC[]";
|
|
86
|
-
return "JSONB";
|
|
87
|
-
}
|
|
88
|
-
case "vector": return `VECTOR(${prop.dimensions})`;
|
|
89
|
-
case "binary": return "BYTEA";
|
|
90
|
-
case "relation": {
|
|
91
|
-
const refProp = prop;
|
|
92
|
-
const relation = findRelation(resolveCollectionRelations(collection), refProp.relation?.relationName ?? propName);
|
|
93
|
-
if (relation?.kind !== "belongsTo") throw new Error(`Relation ${propName} does not put a column on this table (only \`belongsTo\` does)`);
|
|
94
|
-
let targetCollection;
|
|
95
|
-
try {
|
|
96
|
-
targetCollection = relation.target();
|
|
97
|
-
} catch {
|
|
98
|
-
return "TEXT";
|
|
99
|
-
}
|
|
100
|
-
const pkProp = getPrimaryKeyProp(targetCollection);
|
|
101
|
-
return pkProp.type === "number" ? "INTEGER" : pkProp.isUuid ? "UUID" : "TEXT";
|
|
102
|
-
}
|
|
103
|
-
case "reference": {
|
|
104
|
-
const refProp = prop;
|
|
105
|
-
const targetCollection = collections.find((c) => c.slug === refProp.path || getTableName(c) === refProp.path);
|
|
106
|
-
if (!targetCollection) return "TEXT";
|
|
107
|
-
const pkProp = getPrimaryKeyProp(targetCollection);
|
|
108
|
-
return pkProp.type === "number" ? "INTEGER" : pkProp.isUuid ? "UUID" : "TEXT";
|
|
109
|
-
}
|
|
110
|
-
default: return "TEXT";
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
//#endregion
|
|
114
|
-
//#region src/schema/ensure-collection-tables.ts
|
|
115
|
-
/**
|
|
116
|
-
* Bringing a database up to date with a bundle's collections, additively.
|
|
117
|
-
*
|
|
118
|
-
* ## Why this exists
|
|
119
|
-
*
|
|
120
|
-
* A managed runtime boots someone else's compiled project against a database it
|
|
121
|
-
* has never seen. Auth tables are ensured at boot already, but collection tables
|
|
122
|
-
* were not created by anything: the platform ran the app and every `/api/data/*`
|
|
123
|
-
* request answered 500 on a missing relation. `rebase db push` cannot help — it
|
|
124
|
-
* is an Atlas-driven CLI command, and the runtime image ships no CLI.
|
|
125
|
-
*
|
|
126
|
-
* ## Why additive-only, forever
|
|
127
|
-
*
|
|
128
|
-
* This runs unattended, against a database with customers' data in it, with no
|
|
129
|
-
* human reading a diff. So it may only ever do things that cannot lose data:
|
|
130
|
-
* create a missing table, add a missing column, create a missing enum type.
|
|
131
|
-
*
|
|
132
|
-
* It will **never** drop a table or a column, narrow a type, or alter a
|
|
133
|
-
* constraint. A removed field leaves its column behind; a renamed field looks
|
|
134
|
-
* like an addition and the old column stays. That is the correct trade for an
|
|
135
|
-
* automated path — the alternative is an unattended process that can silently
|
|
136
|
-
* destroy a column, which is precisely the failure `db push` was hardened
|
|
137
|
-
* against. Destructive changes stay a deliberate, human-reviewed migration.
|
|
138
|
-
*
|
|
139
|
-
* Because of that, this is safe to run on every boot, and re-running it is a
|
|
140
|
-
* no-op.
|
|
141
|
-
*/
|
|
142
|
-
/** Postgres identifiers this module is willing to interpolate. */
|
|
143
|
-
var SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/;
|
|
144
|
-
function assertSafeIdentifier(value, what) {
|
|
145
|
-
if (!SAFE_IDENTIFIER.test(value)) throw new Error(`Refusing to build SQL with an unsafe ${what}: ${JSON.stringify(value)}`);
|
|
146
|
-
return value;
|
|
147
|
-
}
|
|
148
|
-
function schemaOf(collection) {
|
|
149
|
-
return isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
150
|
-
}
|
|
151
|
-
function qualified(collection) {
|
|
152
|
-
return `${schemaOf(collection)}.${getTableName(collection)}`;
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Enum types a collection's properties require, as `schema.typename`.
|
|
156
|
-
*
|
|
157
|
-
* Named exactly as the DDL generator names them (`<table>_<column>`), because
|
|
158
|
-
* a column added here has to reference the same type the generator would have
|
|
159
|
-
* created — a second, differently-named type for the same field would be a
|
|
160
|
-
* silent schema fork.
|
|
161
|
-
*/
|
|
162
|
-
function requiredEnums(collection) {
|
|
163
|
-
const table = getTableName(collection);
|
|
164
|
-
const schema = schemaOf(collection);
|
|
165
|
-
const out = [];
|
|
166
|
-
for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
|
|
167
|
-
const p = prop;
|
|
168
|
-
if (!("enum" in p) || !p.enum) continue;
|
|
169
|
-
if (p.type !== "string" && p.type !== "number") continue;
|
|
170
|
-
const values = p.enum.map((entry) => entry && typeof entry === "object" && "id" in entry ? String(entry.id) : String(entry)).filter((v) => v.length > 0);
|
|
171
|
-
if (values.length === 0) continue;
|
|
172
|
-
out.push({
|
|
173
|
-
name: `${schema}.${table}_${resolveColumnName(propName, p)}`,
|
|
174
|
-
values
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
return out;
|
|
178
|
-
}
|
|
179
|
-
/** Single-quote escaping for an enum label. */
|
|
180
|
-
function quoteLiteral(value) {
|
|
181
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Decide what to add. Pure — the caller supplies what exists and runs the result.
|
|
185
|
-
*
|
|
186
|
-
* Ordering matters and is deliberate: enum types before the tables and columns
|
|
187
|
-
* that reference them, tables before the columns added to other tables (a new
|
|
188
|
-
* table may be the target of a relation), and nothing is emitted twice.
|
|
189
|
-
*/
|
|
190
|
-
function planCollectionSchemaEnsure(collections, existing) {
|
|
191
|
-
const actions = [];
|
|
192
|
-
const plannedEnums = /* @__PURE__ */ new Set();
|
|
193
|
-
for (const collection of collections) for (const { name, values } of requiredEnums(collection)) {
|
|
194
|
-
if (existing.enums.has(name) || plannedEnums.has(name)) continue;
|
|
195
|
-
plannedEnums.add(name);
|
|
196
|
-
const [schema, typeName] = name.split(".");
|
|
197
|
-
actions.push({
|
|
198
|
-
kind: "create-enum",
|
|
199
|
-
target: name,
|
|
200
|
-
sql: `CREATE TYPE "${schema}"."${typeName}" AS ENUM (${values.map(quoteLiteral).join(", ")});`
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
const created = /* @__PURE__ */ new Set();
|
|
204
|
-
for (const collection of collections) {
|
|
205
|
-
const key = qualified(collection);
|
|
206
|
-
if (existing.tables.has(key) || created.has(key)) continue;
|
|
207
|
-
created.add(key);
|
|
208
|
-
const schema = schemaOf(collection);
|
|
209
|
-
const table = getTableName(collection);
|
|
210
|
-
const idEntry = Object.entries(collection.properties ?? {}).find(([n, p]) => isIdProperty(n, p, collection));
|
|
211
|
-
const idName = idEntry ? resolveColumnName(idEntry[0], idEntry[1]) : "id";
|
|
212
|
-
const idProp = idEntry?.[1];
|
|
213
|
-
let idDef;
|
|
214
|
-
if (idProp?.type === "number") idDef = `"${idName}" BIGSERIAL PRIMARY KEY`;
|
|
215
|
-
else if (idProp && idProp.type === "string" && idProp.isId === "uuid") idDef = `"${idName}" UUID PRIMARY KEY DEFAULT gen_random_uuid()`;
|
|
216
|
-
else idDef = `"${idName}" TEXT PRIMARY KEY`;
|
|
217
|
-
actions.push({
|
|
218
|
-
kind: "create-table",
|
|
219
|
-
target: key,
|
|
220
|
-
sql: `CREATE TABLE IF NOT EXISTS "${schema}"."${table}" (${idDef});`
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
for (const collection of collections) {
|
|
224
|
-
const key = qualified(collection);
|
|
225
|
-
const schema = schemaOf(collection);
|
|
226
|
-
const table = getTableName(collection);
|
|
227
|
-
const present = existing.tables.get(key) ?? /* @__PURE__ */ new Set();
|
|
228
|
-
for (const [propName, prop] of Object.entries(collection.properties ?? {})) {
|
|
229
|
-
const p = prop;
|
|
230
|
-
if (isIdProperty(propName, p, collection)) continue;
|
|
231
|
-
if (p.type === "reference" || p.type === "relation") continue;
|
|
232
|
-
const column = resolveColumnName(propName, p);
|
|
233
|
-
if (present.has(column)) continue;
|
|
234
|
-
const type = getSqlColumnType(propName, p, collection, collections);
|
|
235
|
-
actions.push({
|
|
236
|
-
kind: "add-column",
|
|
237
|
-
target: `${key}.${column}`,
|
|
238
|
-
sql: `ALTER TABLE "${schema}"."${table}" ADD COLUMN IF NOT EXISTS "${column}" ${type};`
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return {
|
|
243
|
-
actions,
|
|
244
|
-
statements: actions.map((a) => a.sql)
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
/** Read what the database has, for the schemas the collections live in. */
|
|
248
|
-
async function readExistingSchema(client, schemas) {
|
|
249
|
-
const tables = /* @__PURE__ */ new Map();
|
|
250
|
-
const enums = /* @__PURE__ */ new Set();
|
|
251
|
-
if (schemas.length === 0) return {
|
|
252
|
-
tables,
|
|
253
|
-
enums
|
|
254
|
-
};
|
|
255
|
-
const inList = schemas.map((schema) => `'${assertSafeIdentifier(schema, "schema name")}'`).join(", ");
|
|
256
|
-
const { rows: columns } = await client.query(`SELECT table_schema, table_name, column_name
|
|
257
|
-
FROM information_schema.columns
|
|
258
|
-
WHERE table_schema IN (${inList})`);
|
|
259
|
-
for (const row of columns) {
|
|
260
|
-
const key = `${row.table_schema}.${row.table_name}`;
|
|
261
|
-
if (!tables.has(key)) tables.set(key, /* @__PURE__ */ new Set());
|
|
262
|
-
tables.get(key).add(row.column_name);
|
|
263
|
-
}
|
|
264
|
-
const { rows: enumRows } = await client.query(`SELECT n.nspname AS schema, t.typname AS name
|
|
265
|
-
FROM pg_type t
|
|
266
|
-
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
267
|
-
WHERE t.typtype = 'e' AND n.nspname IN (${inList})`);
|
|
268
|
-
for (const row of enumRows) enums.add(`${row.schema}.${row.name}`);
|
|
269
|
-
return {
|
|
270
|
-
tables,
|
|
271
|
-
enums
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Bring the database up to date. Returns what it did.
|
|
276
|
-
*
|
|
277
|
-
* Each statement runs on its own rather than in one transaction: they are all
|
|
278
|
-
* independently safe and idempotent, and a single failure (an enum label that
|
|
279
|
-
* cannot be added, say) should not roll back the tables that were created fine.
|
|
280
|
-
* The error is surfaced with the statement that caused it.
|
|
281
|
-
*/
|
|
282
|
-
async function ensureCollectionTables(client, collections, log) {
|
|
283
|
-
const schemas = Array.from(new Set(collections.map(schemaOf)));
|
|
284
|
-
for (const schema of schemas) {
|
|
285
|
-
assertSafeIdentifier(schema, "schema name");
|
|
286
|
-
if (schema !== "public") await client.query(`CREATE SCHEMA IF NOT EXISTS "${schema}";`);
|
|
287
|
-
}
|
|
288
|
-
const plan = planCollectionSchemaEnsure(collections, await readExistingSchema(client, schemas));
|
|
289
|
-
if (plan.actions.length === 0) {
|
|
290
|
-
log?.("Schema is up to date; nothing to create.");
|
|
291
|
-
return plan;
|
|
292
|
-
}
|
|
293
|
-
for (const action of plan.actions) try {
|
|
294
|
-
await client.query(action.sql);
|
|
295
|
-
log?.(`${action.kind}: ${action.target}`);
|
|
296
|
-
} catch (err) {
|
|
297
|
-
throw new Error(`Failed to ${action.kind} ${action.target}: ${err instanceof Error ? err.message : String(err)}\n ${action.sql}`);
|
|
298
|
-
}
|
|
299
|
-
return plan;
|
|
300
|
-
}
|
|
301
|
-
//#endregion
|
|
302
|
-
export { ensureCollectionTables };
|
|
303
|
-
|
|
304
|
-
//# sourceMappingURL=ensure-collection-tables-CNTcZGvn.js.map
|