@rebasepro/server-postgres 0.10.0 → 0.10.1-canary.0a881d4
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/PostgresBootstrapper.d.ts +7 -3
- package/dist/auth/services.d.ts +43 -4
- package/dist/backup/backup-logic.d.ts +23 -0
- package/dist/backup/backup-service.d.ts +44 -2
- package/dist/backup/pg-tools.d.ts +41 -1
- package/dist/chunk-DSJWtz9O.js +40 -0
- package/dist/cli-helpers.d.ts +33 -1
- package/dist/ensure-collection-tables-C9gy4STB.js +304 -0
- package/dist/ensure-collection-tables-C9gy4STB.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +1480 -4648
- package/dist/index.es.js.map +1 -1
- package/dist/schema/auth-schema.d.ts +170 -0
- package/dist/schema/destructive-sql.d.ts +49 -0
- package/dist/schema/ensure-collection-tables.d.ts +79 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +4 -1
- package/dist/services/cdc/CdcListener.d.ts +7 -14
- package/dist/services/channel-bus/ChannelBus.d.ts +29 -0
- package/dist/services/channel-bus/PostgresChannelBus.d.ts +111 -0
- package/dist/services/channel-bus/index.d.ts +55 -0
- package/dist/services/channel-history.d.ts +11 -0
- package/dist/services/channel-presence.d.ts +66 -0
- package/dist/services/pg-notify-listener.d.ts +47 -0
- package/dist/services/realtimeService.d.ts +114 -6
- package/dist/src-CBgtrPhJ.js +336 -0
- package/dist/src-CBgtrPhJ.js.map +1 -0
- package/dist/src-DG6ZsQQ3.js +4026 -0
- package/dist/src-DG6ZsQQ3.js.map +1 -0
- package/package.json +8 -9
- package/src/PostgresBootstrapper.ts +72 -3
- package/src/auth/ensure-tables.ts +91 -3
- package/src/auth/services.ts +186 -48
- package/src/backup/backup-cli.ts +60 -1
- package/src/backup/backup-cron.ts +24 -1
- package/src/backup/backup-logic.ts +62 -0
- package/src/backup/backup-service.ts +132 -13
- package/src/backup/pg-tools.ts +70 -2
- package/src/cli-helpers.ts +82 -27
- package/src/cli.ts +152 -6
- package/src/index.ts +4 -0
- package/src/schema/auth-schema.ts +41 -3
- package/src/schema/destructive-sql.ts +94 -0
- package/src/schema/doctor.ts +6 -6
- package/src/schema/ensure-collection-tables.test.ts +156 -0
- package/src/schema/ensure-collection-tables.ts +297 -0
- package/src/schema/generate-drizzle-schema-logic.ts +13 -9
- package/src/schema/generate-postgres-ddl-logic.ts +22 -15
- package/src/schema/introspect-db-inference.ts +13 -13
- package/src/schema/introspect-db-logic.ts +6 -6
- package/src/services/cdc/CdcListener.ts +27 -91
- package/src/services/channel-bus/ChannelBus.ts +44 -0
- package/src/services/channel-bus/PostgresChannelBus.ts +299 -0
- package/src/services/channel-bus/index.ts +123 -0
- package/src/services/channel-history.ts +35 -0
- package/src/services/channel-presence.ts +148 -0
- package/src/services/pg-notify-listener.ts +137 -0
- package/src/services/realtimeService.ts +383 -14
|
@@ -26,9 +26,13 @@ export interface PostgresDriverConfig {
|
|
|
26
26
|
*/
|
|
27
27
|
introspectionSchema?: string;
|
|
28
28
|
/**
|
|
29
|
-
* Realtime options
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* Realtime options, both opt-in:
|
|
30
|
+
*
|
|
31
|
+
* - `channels` — retention. Without rules no channel keeps any history and
|
|
32
|
+
* broadcast stays fire-and-forget. See {@link ChannelRetentionRule}.
|
|
33
|
+
* - `bus` — the cross-instance transport for channel broadcast and
|
|
34
|
+
* presence. Defaults to in-process only, which is correct for a single
|
|
35
|
+
* instance and wrong for two. See {@link ChannelBusConfig}.
|
|
32
36
|
*/
|
|
33
37
|
realtime?: RealtimeChannelsConfig;
|
|
34
38
|
}
|
package/dist/auth/services.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
2
|
import type { RebasePgTable } from "../types";
|
|
3
|
-
import { UserRepository, TokenRepository, MfaRepository, AuthRepository, UserData, CreateUserData, RoleData, CreateRoleData, RefreshTokenInfo, PasswordResetTokenInfo, MagicLinkTokenInfo, UserIdentityData, ListUsersOptions, PaginatedUsersResult, MfaFactor, MfaChallengeInfo, RoleData as Role } from "@rebasepro/server";
|
|
3
|
+
import { UserRepository, TokenRepository, MfaRepository, AuthRepository, UserData, CreateUserData, RoleData, CreateRoleData, RefreshTokenInfo, RefreshTokenSession, PasswordResetTokenInfo, MagicLinkTokenInfo, UserIdentityData, ListUsersOptions, PaginatedUsersResult, MfaFactor, MfaChallengeInfo, RoleData as Role } from "@rebasepro/server";
|
|
4
4
|
export type { Role };
|
|
5
5
|
export interface AuthSchemaTables {
|
|
6
6
|
users: RebasePgTable;
|
|
@@ -90,9 +90,38 @@ export declare class UserService implements UserRepository {
|
|
|
90
90
|
export declare class RefreshTokenService {
|
|
91
91
|
private db;
|
|
92
92
|
private refreshTokensTable;
|
|
93
|
+
private usersTable;
|
|
93
94
|
constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>);
|
|
94
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Whether the table actually carries a column, so a host application that
|
|
97
|
+
* supplied its own `refresh_tokens` table — one that predates session
|
|
98
|
+
* grouping — degrades instead of throwing on every sign-in.
|
|
99
|
+
*/
|
|
100
|
+
private has;
|
|
101
|
+
private col;
|
|
102
|
+
/** The columns to read back, narrowed to the ones this table has. */
|
|
103
|
+
private selection;
|
|
104
|
+
createToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void>;
|
|
95
105
|
findByHash(tokenHash: string): Promise<RefreshTokenInfo | null>;
|
|
106
|
+
/**
|
|
107
|
+
* Record that a token was rotated away, keeping the row.
|
|
108
|
+
*
|
|
109
|
+
* The row is what lets `/auth/refresh` distinguish "you already used this,
|
|
110
|
+
* here is a fresh one" from "no idea what this is". Deleting it — which is
|
|
111
|
+
* what this used to do — collapsed both into a 401 and signed the user out
|
|
112
|
+
* for the crime of losing a response.
|
|
113
|
+
*/
|
|
114
|
+
markRotated(tokenHash: string): Promise<void>;
|
|
115
|
+
/** Final kill of one sign-in: logout, or revoking a device remotely. */
|
|
116
|
+
revokeSession(sessionId: string): Promise<void>;
|
|
117
|
+
/**
|
|
118
|
+
* Housekeeping: rotation would otherwise leave a row per refresh forever.
|
|
119
|
+
* Superseded rows are only needed for as long as a straggler might still
|
|
120
|
+
* present them, and expired ones are dead weight everywhere.
|
|
121
|
+
*/
|
|
122
|
+
prune(uid: string, sessionId: string, supersededBefore: Date): Promise<void>;
|
|
123
|
+
getTokensValidAfter(uid: string): Promise<Date | null>;
|
|
124
|
+
setTokensValidAfter(uid: string, at: Date): Promise<void>;
|
|
96
125
|
deleteByHash(tokenHash: string): Promise<void>;
|
|
97
126
|
deleteAllForUser(uid: string): Promise<void>;
|
|
98
127
|
listForUser(uid: string): Promise<RefreshTokenInfo[]>;
|
|
@@ -153,7 +182,12 @@ export declare class PostgresTokenRepository implements TokenRepository {
|
|
|
153
182
|
private passwordResetTokenService;
|
|
154
183
|
private magicLinkTokenService;
|
|
155
184
|
constructor(db: NodePgDatabase, tableOrTables?: RebasePgTable | Partial<AuthSchemaTables>);
|
|
156
|
-
createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
|
|
185
|
+
createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void>;
|
|
186
|
+
markRefreshTokenRotated(tokenHash: string): Promise<void>;
|
|
187
|
+
revokeRefreshTokenSession(sessionId: string): Promise<void>;
|
|
188
|
+
pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise<void>;
|
|
189
|
+
getTokensValidAfter(uid: string): Promise<Date | null>;
|
|
190
|
+
setTokensValidAfter(uid: string, at: Date): Promise<void>;
|
|
157
191
|
findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null>;
|
|
158
192
|
deleteRefreshToken(tokenHash: string): Promise<void>;
|
|
159
193
|
deleteAllRefreshTokensForUser(uid: string): Promise<void>;
|
|
@@ -205,7 +239,12 @@ export declare class PostgresAuthRepository implements AuthRepository {
|
|
|
205
239
|
createRole(_data: CreateRoleData): Promise<RoleData>;
|
|
206
240
|
updateRole(id: string, data: Partial<Omit<RoleData, "id">>): Promise<RoleData | null>;
|
|
207
241
|
deleteRole(_id: string): Promise<void>;
|
|
208
|
-
createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string): Promise<void>;
|
|
242
|
+
createRefreshToken(uid: string, tokenHash: string, expiresAt: Date, userAgent?: string, ipAddress?: string, session?: RefreshTokenSession): Promise<void>;
|
|
243
|
+
markRefreshTokenRotated(tokenHash: string): Promise<void>;
|
|
244
|
+
revokeRefreshTokenSession(sessionId: string): Promise<void>;
|
|
245
|
+
pruneRefreshTokens(uid: string, sessionId: string, supersededBefore: Date): Promise<void>;
|
|
246
|
+
getTokensValidAfter(uid: string): Promise<Date | null>;
|
|
247
|
+
setTokensValidAfter(uid: string, at: Date): Promise<void>;
|
|
209
248
|
findRefreshTokenByHash(tokenHash: string): Promise<RefreshTokenInfo | null>;
|
|
210
249
|
deleteRefreshToken(tokenHash: string): Promise<void>;
|
|
211
250
|
deleteAllRefreshTokensForUser(uid: string): Promise<void>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay a `pg_dumpall --globals-only` script one statement at a time,
|
|
3
|
+
* tolerating per-statement failures. On a same-cluster restore the roles
|
|
4
|
+
* usually already exist (`CREATE ROLE` → "already exists") and on a managed
|
|
5
|
+
* provider an `ALTER ROLE <superuser>` may be refused; neither should abort
|
|
6
|
+
* recreation of the roles that *are* missing. Returns how many statements
|
|
7
|
+
* applied vs were skipped.
|
|
8
|
+
*
|
|
9
|
+
* `runStatement` executes one SQL statement and rejects on error.
|
|
10
|
+
*/
|
|
11
|
+
export declare function applyGlobalsWith(runStatement: (sql: string) => Promise<void>, globalsSql: string, log?: (message: string) => void): Promise<{
|
|
12
|
+
applied: number;
|
|
13
|
+
skipped: number;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Delete each pruned dump together with its `.globals.sql` roles sidecar, so
|
|
17
|
+
* pruning never orphans the roles file. The sidecar is best-effort — older
|
|
18
|
+
* backups predate it, so a missing-sidecar failure is swallowed while a
|
|
19
|
+
* failure deleting the dump itself propagates.
|
|
20
|
+
*
|
|
21
|
+
* `deleteObject` removes one key and rejects if it cannot (e.g. not found).
|
|
22
|
+
*/
|
|
23
|
+
export declare function pruneWith(keys: string[], deleteObject: (key: string) => Promise<void>): Promise<void>;
|
|
@@ -5,8 +5,8 @@ export declare class BackupToolError extends Error {
|
|
|
5
5
|
readonly hint?: string | undefined;
|
|
6
6
|
constructor(message: string, hint?: string | undefined);
|
|
7
7
|
}
|
|
8
|
-
/** Locate `pg_dump` / `pg_restore`, honouring an env override. */
|
|
9
|
-
export declare function resolvePgBinary(tool: "pg_dump" | "pg_restore", env?: Record<string, string | undefined>): string | null;
|
|
8
|
+
/** Locate `pg_dump` / `pg_restore` / `pg_dumpall`, honouring an env override. */
|
|
9
|
+
export declare function resolvePgBinary(tool: "pg_dump" | "pg_restore" | "pg_dumpall", env?: Record<string, string | undefined>): string | null;
|
|
10
10
|
/** Run `<bin> --version` and extract the major version. */
|
|
11
11
|
export declare function detectToolMajor(bin: string): Promise<number | null>;
|
|
12
12
|
/** Query the server for its major version via `server_version_num`. */
|
|
@@ -27,11 +27,22 @@ export interface BackupResult {
|
|
|
27
27
|
localFile: string;
|
|
28
28
|
fileName: string;
|
|
29
29
|
sizeBytes: number;
|
|
30
|
+
/**
|
|
31
|
+
* Absolute path of the `.globals.sql` sidecar holding cluster-wide roles
|
|
32
|
+
* (present unless globals capture was disabled or unavailable).
|
|
33
|
+
*/
|
|
34
|
+
globalsFile?: string;
|
|
35
|
+
globalsSizeBytes?: number;
|
|
30
36
|
}
|
|
31
37
|
/**
|
|
32
38
|
* Produce a custom-format dump on local disk. When `outDir` is omitted the
|
|
33
39
|
* file is written to the OS temp directory (used by the upload path, which
|
|
34
40
|
* cleans it up afterwards).
|
|
41
|
+
*
|
|
42
|
+
* Alongside the `-Fc` dump it writes a `<name>.globals.sql` sidecar via
|
|
43
|
+
* `pg_dumpall --globals-only` so the roles the dump's GRANT/RLS statements
|
|
44
|
+
* depend on can be recreated on restore. Set `includeGlobals: false` to skip
|
|
45
|
+
* it (e.g. when the caller has no privilege to read cluster globals).
|
|
35
46
|
*/
|
|
36
47
|
export declare function createDump(opts: {
|
|
37
48
|
connectionString: string;
|
|
@@ -41,19 +52,50 @@ export declare function createDump(opts: {
|
|
|
41
52
|
excludeSchemas?: string[];
|
|
42
53
|
noOwner?: boolean;
|
|
43
54
|
inheritStdio?: boolean;
|
|
55
|
+
includeGlobals?: boolean;
|
|
44
56
|
env?: Record<string, string | undefined>;
|
|
45
57
|
}): Promise<BackupResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Cheap integrity check on a freshly written dump: it must be non-empty and
|
|
60
|
+
* `pg_restore --list` must parse its table of contents without error. Used
|
|
61
|
+
* before pruning older backups so a corrupt-but-exit-0 dump never becomes
|
|
62
|
+
* the reason the last good backup is deleted.
|
|
63
|
+
*/
|
|
64
|
+
export declare function validateDump(localFile: string, env?: Record<string, string | undefined>): Promise<{
|
|
65
|
+
ok: boolean;
|
|
66
|
+
reason?: string;
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* Replay a `pg_dumpall --globals-only` script to recreate cluster roles
|
|
70
|
+
* before a restore, so the dump's GRANT/RLS statements (which reference
|
|
71
|
+
* `rebase_user` and any owner roles) actually apply. Runs statement by
|
|
72
|
+
* statement and tolerates per-statement failures — on a same-cluster restore
|
|
73
|
+
* the roles usually already exist (`CREATE ROLE` → "already exists"), and on
|
|
74
|
+
* a managed provider an `ALTER ROLE <superuser>` may be refused; neither
|
|
75
|
+
* should abort role recreation. Returns how many statements applied vs were
|
|
76
|
+
* skipped.
|
|
77
|
+
*/
|
|
78
|
+
export declare function applyGlobals(connectionString: string, globalsSql: string, log?: (message: string) => void): Promise<{
|
|
79
|
+
applied: number;
|
|
80
|
+
skipped: number;
|
|
81
|
+
}>;
|
|
46
82
|
/**
|
|
47
83
|
* Restore a custom-format dump into the database named by
|
|
48
84
|
* `connectionString`. Destructive when `clean` is set (drops objects
|
|
49
85
|
* first). Never called automatically — the CLI gates it behind explicit
|
|
50
86
|
* confirmation.
|
|
87
|
+
*
|
|
88
|
+
* Runs with `--exit-on-error` by default: a restore that logs-and-continues
|
|
89
|
+
* past a failed GRANT (because a role was missing) reports success with RLS
|
|
90
|
+
* un-enforced. Callers should recreate roles first (see {@link applyGlobals})
|
|
91
|
+
* and only set `exitOnError: false` deliberately.
|
|
51
92
|
*/
|
|
52
93
|
export declare function restoreDump(opts: {
|
|
53
94
|
connectionString: string;
|
|
54
95
|
inputFile: string;
|
|
55
96
|
clean?: boolean;
|
|
56
97
|
noOwner?: boolean;
|
|
98
|
+
exitOnError?: boolean;
|
|
57
99
|
inheritStdio?: boolean;
|
|
58
100
|
env?: Record<string, string | undefined>;
|
|
59
101
|
}): Promise<void>;
|
|
@@ -99,10 +99,50 @@ export declare function buildPgRestoreArgs(opts: {
|
|
|
99
99
|
inputFile: string;
|
|
100
100
|
/** Drop objects before recreating them (destructive but idempotent). */
|
|
101
101
|
clean?: boolean;
|
|
102
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Abort on the first error instead of logging and continuing. Defaults
|
|
104
|
+
* ON: a restore that silently skips failed GRANT/RLS statements (because
|
|
105
|
+
* a role is missing) "succeeds" with RLS un-enforced — a security hole.
|
|
106
|
+
* Fail loudly instead so the operator knows the restore is incomplete.
|
|
107
|
+
*/
|
|
103
108
|
exitOnError?: boolean;
|
|
104
109
|
noOwner?: boolean;
|
|
105
110
|
}): string[];
|
|
111
|
+
/**
|
|
112
|
+
* Assemble the `pg_restore --list` argument vector. Reading a dump's table
|
|
113
|
+
* of contents parses the whole archive without touching a database, so it is
|
|
114
|
+
* a cheap integrity check that the file isn't truncated or corrupt.
|
|
115
|
+
*/
|
|
116
|
+
export declare function buildPgRestoreListArgs(inputFile: string): string[];
|
|
117
|
+
/**
|
|
118
|
+
* Assemble the `pg_dumpall --globals-only` argument vector. Roles (and other
|
|
119
|
+
* cluster-wide objects) live outside any single database, so a per-database
|
|
120
|
+
* `pg_dump` omits them. Without the `rebase_user` role the RLS GRANT
|
|
121
|
+
* statements in the main dump fail on restore and RLS is silently lost — so
|
|
122
|
+
* every backup captures the globals into a sidecar `.globals.sql`.
|
|
123
|
+
*
|
|
124
|
+
* `--no-role-passwords` keeps role secrets out of the artifact (backups may
|
|
125
|
+
* be shipped off-box); roles are recreated password-less and re-secured by
|
|
126
|
+
* the operator.
|
|
127
|
+
*/
|
|
128
|
+
export declare function buildPgDumpallGlobalsArgs(opts: {
|
|
129
|
+
connectionString: string;
|
|
130
|
+
outFile: string;
|
|
131
|
+
}): string[];
|
|
132
|
+
/**
|
|
133
|
+
* Derive the globals sidecar path/key for a given `.dump` file. Keeps the
|
|
134
|
+
* two artifacts adjacent so listing, uploading and pruning can find one from
|
|
135
|
+
* the other. A name that doesn't end in `.dump` is returned unchanged with a
|
|
136
|
+
* `.globals.sql` suffix appended.
|
|
137
|
+
*/
|
|
138
|
+
export declare function globalsFileForDump(dumpPath: string): string;
|
|
139
|
+
/**
|
|
140
|
+
* Split a `pg_dumpall --globals-only` script into individual statements.
|
|
141
|
+
* Used when replaying globals on restore so each `CREATE ROLE` / `GRANT`
|
|
142
|
+
* can run independently and a benign "role already exists" on one doesn't
|
|
143
|
+
* abort the rest. Drops `--` comment lines and blank statements.
|
|
144
|
+
*/
|
|
145
|
+
export declare function splitGlobalsStatements(sql: string): string[];
|
|
106
146
|
/**
|
|
107
147
|
* Resolve the Postgres connection string the backup commands should use,
|
|
108
148
|
* mirroring the precedence the branch command already relies on.
|
|
@@ -0,0 +1,40 @@
|
|
|
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 };
|
package/dist/cli-helpers.d.ts
CHANGED
|
@@ -4,4 +4,36 @@ export declare function getTableIncludesFromCollections(collections: CollectionC
|
|
|
4
4
|
export declare function getTableIncludes(collectionsPath: string): Promise<string[]>;
|
|
5
5
|
export declare function getDevDatabaseUrl(databaseUrl: string): string;
|
|
6
6
|
export declare function ensureDevDatabaseExists(databaseUrl: string, devDatabaseUrl: string): Promise<void>;
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Query the live database for every user table/view outside the system
|
|
9
|
+
* catalogs. Separated from {@link getTableExcludes} so its failure mode can
|
|
10
|
+
* be handled explicitly (fail closed) and so tests can inject a stub.
|
|
11
|
+
*/
|
|
12
|
+
export declare function queryExistingTables(databaseUrl: string): Promise<string[]>;
|
|
13
|
+
/**
|
|
14
|
+
* Raised when the exclude list could not be built. `db push` MUST abort on
|
|
15
|
+
* this rather than continue: the exclude list is the only thing shielding
|
|
16
|
+
* non-collection (user/system) tables from the auto-approved declarative
|
|
17
|
+
* apply. A partial list — the old fail-open behaviour — meant a transient
|
|
18
|
+
* introspection hiccup dropped every table not present in `schema.sql`.
|
|
19
|
+
*/
|
|
20
|
+
export declare class ExcludeIntrospectionError extends Error {
|
|
21
|
+
readonly cause?: unknown | undefined;
|
|
22
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build the `--exclude` list that protects tables Rebase doesn't manage from
|
|
26
|
+
* the declarative apply. Anything not backing a collection (or its M2M
|
|
27
|
+
* junctions) is excluded so Atlas never drops it.
|
|
28
|
+
*
|
|
29
|
+
* Fails **closed**: if the database can't be introspected we cannot know
|
|
30
|
+
* which tables to protect, so we throw {@link ExcludeIntrospectionError}
|
|
31
|
+
* instead of returning a near-empty list and letting the caller drop
|
|
32
|
+
* everything.
|
|
33
|
+
*
|
|
34
|
+
* `deps` is injectable for tests; production uses the real pg-backed queries.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getTableExcludes(databaseUrl: string, collectionsPath: string, deps?: {
|
|
37
|
+
queryExistingTables?: (databaseUrl: string) => Promise<string[]>;
|
|
38
|
+
getIncludes?: (collectionsPath: string) => Promise<string[]>;
|
|
39
|
+
}): Promise<string[]>;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "module";
|
|
2
|
+
import "process";
|
|
3
|
+
__createRequire(import.meta.url);
|
|
4
|
+
import { l as isPostgresCollectionConfig } from "./src-CBgtrPhJ.js";
|
|
5
|
+
import { g as getTableName, k as toSnakeCase, p as findRelation, v as resolveCollectionRelations } from "./src-DG6ZsQQ3.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.relationName ?? propName);
|
|
93
|
+
if (!relation || relation.direction !== "owning" || relation.cardinality !== "one") throw new Error(`Relation ${propName} is not an owning one-to-one/many-to-one relation`);
|
|
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-C9gy4STB.js.map
|