@rebasepro/server-postgres 0.10.1-canary.a54c057 → 0.10.1-canary.af2af91
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-BVvtkkRm.js +304 -0
- package/dist/ensure-collection-tables-BVvtkkRm.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +1854 -4745
- 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/FetchService.d.ts +21 -8
- package/dist/services/PersistService.d.ts +12 -0
- package/dist/services/RelationService.d.ts +30 -0
- package/dist/services/cdc/CdcListener.d.ts +7 -14
- package/dist/services/cdc/junction-tables.d.ts +38 -0
- 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/nested-path.d.ts +59 -0
- package/dist/services/pg-notify-listener.d.ts +47 -0
- package/dist/services/realtimeService.d.ts +133 -6
- package/dist/src-CBgtrPhJ.js +336 -0
- package/dist/src-CBgtrPhJ.js.map +1 -0
- package/dist/src-lcfUP4xg.js +4106 -0
- package/dist/src-lcfUP4xg.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +41 -0
- package/package.json +8 -9
- package/src/PostgresBootstrapper.ts +82 -4
- package/src/auth/ensure-tables.ts +70 -56
- 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/FetchService.ts +104 -114
- package/src/services/PersistService.ts +127 -85
- package/src/services/RelationService.ts +95 -6
- package/src/services/cdc/CdcListener.ts +27 -91
- package/src/services/cdc/junction-tables.ts +91 -0
- 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/nested-path.ts +130 -0
- package/src/services/pg-notify-listener.ts +137 -0
- package/src/services/realtimeService.ts +443 -14
- package/src/utils/drizzle-conditions.ts +154 -0
|
@@ -232,6 +232,23 @@ export declare function createAuthSchema(usersSchemaName?: string): {
|
|
|
232
232
|
}, {}, {
|
|
233
233
|
$type: Record<string, unknown>;
|
|
234
234
|
}>;
|
|
235
|
+
tokensValidAfter: import("drizzle-orm/pg-core").PgColumn<{
|
|
236
|
+
name: "tokens_valid_after";
|
|
237
|
+
tableName: "users";
|
|
238
|
+
dataType: "date";
|
|
239
|
+
columnType: "PgTimestamp";
|
|
240
|
+
data: Date;
|
|
241
|
+
driverParam: string;
|
|
242
|
+
notNull: false;
|
|
243
|
+
hasDefault: false;
|
|
244
|
+
isPrimaryKey: false;
|
|
245
|
+
isAutoincrement: false;
|
|
246
|
+
hasRuntimeDefault: false;
|
|
247
|
+
enumValues: undefined;
|
|
248
|
+
baseColumn: never;
|
|
249
|
+
identity: undefined;
|
|
250
|
+
generated: undefined;
|
|
251
|
+
}, {}, {}>;
|
|
235
252
|
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
236
253
|
name: "created_at";
|
|
237
254
|
tableName: "users";
|
|
@@ -307,6 +324,23 @@ export declare function createAuthSchema(usersSchemaName?: string): {
|
|
|
307
324
|
identity: undefined;
|
|
308
325
|
generated: undefined;
|
|
309
326
|
}, {}, {}>;
|
|
327
|
+
sessionId: import("drizzle-orm/pg-core").PgColumn<{
|
|
328
|
+
name: "session_id";
|
|
329
|
+
tableName: "refresh_tokens";
|
|
330
|
+
dataType: "string";
|
|
331
|
+
columnType: "PgUUID";
|
|
332
|
+
data: string;
|
|
333
|
+
driverParam: string;
|
|
334
|
+
notNull: true;
|
|
335
|
+
hasDefault: true;
|
|
336
|
+
isPrimaryKey: false;
|
|
337
|
+
isAutoincrement: false;
|
|
338
|
+
hasRuntimeDefault: false;
|
|
339
|
+
enumValues: undefined;
|
|
340
|
+
baseColumn: never;
|
|
341
|
+
identity: undefined;
|
|
342
|
+
generated: undefined;
|
|
343
|
+
}, {}, {}>;
|
|
310
344
|
tokenHash: import("drizzle-orm/pg-core").PgColumn<{
|
|
311
345
|
name: "token_hash";
|
|
312
346
|
tableName: "refresh_tokens";
|
|
@@ -343,6 +377,57 @@ export declare function createAuthSchema(usersSchemaName?: string): {
|
|
|
343
377
|
identity: undefined;
|
|
344
378
|
generated: undefined;
|
|
345
379
|
}, {}, {}>;
|
|
380
|
+
revoked: import("drizzle-orm/pg-core").PgColumn<{
|
|
381
|
+
name: "revoked";
|
|
382
|
+
tableName: "refresh_tokens";
|
|
383
|
+
dataType: "boolean";
|
|
384
|
+
columnType: "PgBoolean";
|
|
385
|
+
data: boolean;
|
|
386
|
+
driverParam: boolean;
|
|
387
|
+
notNull: true;
|
|
388
|
+
hasDefault: true;
|
|
389
|
+
isPrimaryKey: false;
|
|
390
|
+
isAutoincrement: false;
|
|
391
|
+
hasRuntimeDefault: false;
|
|
392
|
+
enumValues: undefined;
|
|
393
|
+
baseColumn: never;
|
|
394
|
+
identity: undefined;
|
|
395
|
+
generated: undefined;
|
|
396
|
+
}, {}, {}>;
|
|
397
|
+
rotatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
398
|
+
name: "rotated_at";
|
|
399
|
+
tableName: "refresh_tokens";
|
|
400
|
+
dataType: "date";
|
|
401
|
+
columnType: "PgTimestamp";
|
|
402
|
+
data: Date;
|
|
403
|
+
driverParam: string;
|
|
404
|
+
notNull: false;
|
|
405
|
+
hasDefault: false;
|
|
406
|
+
isPrimaryKey: false;
|
|
407
|
+
isAutoincrement: false;
|
|
408
|
+
hasRuntimeDefault: false;
|
|
409
|
+
enumValues: undefined;
|
|
410
|
+
baseColumn: never;
|
|
411
|
+
identity: undefined;
|
|
412
|
+
generated: undefined;
|
|
413
|
+
}, {}, {}>;
|
|
414
|
+
sessionStartedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
415
|
+
name: "session_started_at";
|
|
416
|
+
tableName: "refresh_tokens";
|
|
417
|
+
dataType: "date";
|
|
418
|
+
columnType: "PgTimestamp";
|
|
419
|
+
data: Date;
|
|
420
|
+
driverParam: string;
|
|
421
|
+
notNull: true;
|
|
422
|
+
hasDefault: true;
|
|
423
|
+
isPrimaryKey: false;
|
|
424
|
+
isAutoincrement: false;
|
|
425
|
+
hasRuntimeDefault: false;
|
|
426
|
+
enumValues: undefined;
|
|
427
|
+
baseColumn: never;
|
|
428
|
+
identity: undefined;
|
|
429
|
+
generated: undefined;
|
|
430
|
+
}, {}, {}>;
|
|
346
431
|
userAgent: import("drizzle-orm/pg-core").PgColumn<{
|
|
347
432
|
name: "user_agent";
|
|
348
433
|
tableName: "refresh_tokens";
|
|
@@ -1398,6 +1483,23 @@ export declare const users: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
|
1398
1483
|
}, {}, {
|
|
1399
1484
|
$type: Record<string, unknown>;
|
|
1400
1485
|
}>;
|
|
1486
|
+
tokensValidAfter: import("drizzle-orm/pg-core").PgColumn<{
|
|
1487
|
+
name: "tokens_valid_after";
|
|
1488
|
+
tableName: "users";
|
|
1489
|
+
dataType: "date";
|
|
1490
|
+
columnType: "PgTimestamp";
|
|
1491
|
+
data: Date;
|
|
1492
|
+
driverParam: string;
|
|
1493
|
+
notNull: false;
|
|
1494
|
+
hasDefault: false;
|
|
1495
|
+
isPrimaryKey: false;
|
|
1496
|
+
isAutoincrement: false;
|
|
1497
|
+
hasRuntimeDefault: false;
|
|
1498
|
+
enumValues: undefined;
|
|
1499
|
+
baseColumn: never;
|
|
1500
|
+
identity: undefined;
|
|
1501
|
+
generated: undefined;
|
|
1502
|
+
}, {}, {}>;
|
|
1401
1503
|
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
1402
1504
|
name: "created_at";
|
|
1403
1505
|
tableName: "users";
|
|
@@ -1473,6 +1575,23 @@ export declare const refreshTokens: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
1473
1575
|
identity: undefined;
|
|
1474
1576
|
generated: undefined;
|
|
1475
1577
|
}, {}, {}>;
|
|
1578
|
+
sessionId: import("drizzle-orm/pg-core").PgColumn<{
|
|
1579
|
+
name: "session_id";
|
|
1580
|
+
tableName: "refresh_tokens";
|
|
1581
|
+
dataType: "string";
|
|
1582
|
+
columnType: "PgUUID";
|
|
1583
|
+
data: string;
|
|
1584
|
+
driverParam: string;
|
|
1585
|
+
notNull: true;
|
|
1586
|
+
hasDefault: true;
|
|
1587
|
+
isPrimaryKey: false;
|
|
1588
|
+
isAutoincrement: false;
|
|
1589
|
+
hasRuntimeDefault: false;
|
|
1590
|
+
enumValues: undefined;
|
|
1591
|
+
baseColumn: never;
|
|
1592
|
+
identity: undefined;
|
|
1593
|
+
generated: undefined;
|
|
1594
|
+
}, {}, {}>;
|
|
1476
1595
|
tokenHash: import("drizzle-orm/pg-core").PgColumn<{
|
|
1477
1596
|
name: "token_hash";
|
|
1478
1597
|
tableName: "refresh_tokens";
|
|
@@ -1509,6 +1628,57 @@ export declare const refreshTokens: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
1509
1628
|
identity: undefined;
|
|
1510
1629
|
generated: undefined;
|
|
1511
1630
|
}, {}, {}>;
|
|
1631
|
+
revoked: import("drizzle-orm/pg-core").PgColumn<{
|
|
1632
|
+
name: "revoked";
|
|
1633
|
+
tableName: "refresh_tokens";
|
|
1634
|
+
dataType: "boolean";
|
|
1635
|
+
columnType: "PgBoolean";
|
|
1636
|
+
data: boolean;
|
|
1637
|
+
driverParam: boolean;
|
|
1638
|
+
notNull: true;
|
|
1639
|
+
hasDefault: true;
|
|
1640
|
+
isPrimaryKey: false;
|
|
1641
|
+
isAutoincrement: false;
|
|
1642
|
+
hasRuntimeDefault: false;
|
|
1643
|
+
enumValues: undefined;
|
|
1644
|
+
baseColumn: never;
|
|
1645
|
+
identity: undefined;
|
|
1646
|
+
generated: undefined;
|
|
1647
|
+
}, {}, {}>;
|
|
1648
|
+
rotatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
1649
|
+
name: "rotated_at";
|
|
1650
|
+
tableName: "refresh_tokens";
|
|
1651
|
+
dataType: "date";
|
|
1652
|
+
columnType: "PgTimestamp";
|
|
1653
|
+
data: Date;
|
|
1654
|
+
driverParam: string;
|
|
1655
|
+
notNull: false;
|
|
1656
|
+
hasDefault: false;
|
|
1657
|
+
isPrimaryKey: false;
|
|
1658
|
+
isAutoincrement: false;
|
|
1659
|
+
hasRuntimeDefault: false;
|
|
1660
|
+
enumValues: undefined;
|
|
1661
|
+
baseColumn: never;
|
|
1662
|
+
identity: undefined;
|
|
1663
|
+
generated: undefined;
|
|
1664
|
+
}, {}, {}>;
|
|
1665
|
+
sessionStartedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
1666
|
+
name: "session_started_at";
|
|
1667
|
+
tableName: "refresh_tokens";
|
|
1668
|
+
dataType: "date";
|
|
1669
|
+
columnType: "PgTimestamp";
|
|
1670
|
+
data: Date;
|
|
1671
|
+
driverParam: string;
|
|
1672
|
+
notNull: true;
|
|
1673
|
+
hasDefault: true;
|
|
1674
|
+
isPrimaryKey: false;
|
|
1675
|
+
isAutoincrement: false;
|
|
1676
|
+
hasRuntimeDefault: false;
|
|
1677
|
+
enumValues: undefined;
|
|
1678
|
+
baseColumn: never;
|
|
1679
|
+
identity: undefined;
|
|
1680
|
+
generated: undefined;
|
|
1681
|
+
}, {}, {}>;
|
|
1512
1682
|
userAgent: import("drizzle-orm/pg-core").PgColumn<{
|
|
1513
1683
|
name: "user_agent";
|
|
1514
1684
|
tableName: "refresh_tokens";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers that classify an Atlas declarative-apply plan as destructive
|
|
3
|
+
* or not, and decide what `rebase db push` should do about it.
|
|
4
|
+
*
|
|
5
|
+
* `db push` runs `atlas schema apply` to make the live database match the
|
|
6
|
+
* generated `schema.sql`. Removing a collection field compiles to
|
|
7
|
+
* `DROP COLUMN`; renaming compiles to drop-then-add — either destroys data.
|
|
8
|
+
* We first run the apply with `--dry-run` to obtain the planned SQL, scan it
|
|
9
|
+
* here, and refuse to auto-approve anything destructive without an explicit
|
|
10
|
+
* opt-in.
|
|
11
|
+
*
|
|
12
|
+
* Everything in this file is side-effect free so it can be unit-tested
|
|
13
|
+
* without Atlas or a database.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Split a SQL script into individual statements, dropping blank lines and
|
|
17
|
+
* `--` comment lines. Deliberately simple: Atlas emits one plain statement
|
|
18
|
+
* per `;`, without string literals that contain semicolons in a schema DDL
|
|
19
|
+
* plan, so a naive split is safe and keeps this dependency-free.
|
|
20
|
+
*/
|
|
21
|
+
export declare function splitSqlStatements(sql: string): string[];
|
|
22
|
+
export interface DestructiveStatement {
|
|
23
|
+
/** The offending statement (trimmed, without the trailing `;`). */
|
|
24
|
+
statement: string;
|
|
25
|
+
/** Which destructive operation it was flagged for, e.g. "DROP COLUMN". */
|
|
26
|
+
kind: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Scan an Atlas plan (the SQL printed by `schema apply --dry-run`) and return
|
|
30
|
+
* the statements that would destroy data. An empty array means the plan is
|
|
31
|
+
* safe to auto-approve.
|
|
32
|
+
*/
|
|
33
|
+
export declare function detectDestructiveStatements(planSql: string): DestructiveStatement[];
|
|
34
|
+
export type PushDecision = "apply" | "confirm" | "refuse";
|
|
35
|
+
/**
|
|
36
|
+
* Decide how `db push` should proceed given the plan's destructiveness and
|
|
37
|
+
* the invocation context.
|
|
38
|
+
*
|
|
39
|
+
* - No destructive statements → `apply` (safe to auto-approve).
|
|
40
|
+
* - Destructive + `--allow-destructive` → `apply` (operator opted in).
|
|
41
|
+
* - Destructive + interactive TTY → `confirm` (prompt before applying).
|
|
42
|
+
* - Destructive + non-interactive → `refuse` (never silently drop data in
|
|
43
|
+
* CI / scripts / agents).
|
|
44
|
+
*/
|
|
45
|
+
export declare function decidePushSafety(opts: {
|
|
46
|
+
destructiveCount: number;
|
|
47
|
+
allowDestructive: boolean;
|
|
48
|
+
interactive: boolean;
|
|
49
|
+
}): PushDecision;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bringing a database up to date with a bundle's collections, additively.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* A managed runtime boots someone else's compiled project against a database it
|
|
7
|
+
* has never seen. Auth tables are ensured at boot already, but collection tables
|
|
8
|
+
* were not created by anything: the platform ran the app and every `/api/data/*`
|
|
9
|
+
* request answered 500 on a missing relation. `rebase db push` cannot help — it
|
|
10
|
+
* is an Atlas-driven CLI command, and the runtime image ships no CLI.
|
|
11
|
+
*
|
|
12
|
+
* ## Why additive-only, forever
|
|
13
|
+
*
|
|
14
|
+
* This runs unattended, against a database with customers' data in it, with no
|
|
15
|
+
* human reading a diff. So it may only ever do things that cannot lose data:
|
|
16
|
+
* create a missing table, add a missing column, create a missing enum type.
|
|
17
|
+
*
|
|
18
|
+
* It will **never** drop a table or a column, narrow a type, or alter a
|
|
19
|
+
* constraint. A removed field leaves its column behind; a renamed field looks
|
|
20
|
+
* like an addition and the old column stays. That is the correct trade for an
|
|
21
|
+
* automated path — the alternative is an unattended process that can silently
|
|
22
|
+
* destroy a column, which is precisely the failure `db push` was hardened
|
|
23
|
+
* against. Destructive changes stay a deliberate, human-reviewed migration.
|
|
24
|
+
*
|
|
25
|
+
* Because of that, this is safe to run on every boot, and re-running it is a
|
|
26
|
+
* no-op.
|
|
27
|
+
*/
|
|
28
|
+
import { type CollectionConfig } from "@rebasepro/types";
|
|
29
|
+
/**
|
|
30
|
+
* The subset of a database handle this needs: run a statement, get rows back.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately parameterless. Everything here is DDL or catalogue reads keyed by
|
|
33
|
+
* schema name, and schema names are identifiers — they cannot be bound as
|
|
34
|
+
* parameters anyway. They are validated against {@link SAFE_IDENTIFIER} before
|
|
35
|
+
* they reach a statement, so a config that somehow carried a quote is refused
|
|
36
|
+
* rather than concatenated.
|
|
37
|
+
*/
|
|
38
|
+
export interface Queryable {
|
|
39
|
+
query<T = unknown>(sql: string): Promise<{
|
|
40
|
+
rows: T[];
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
/** What the database currently has, as the planner needs it. */
|
|
44
|
+
export interface ExistingSchema {
|
|
45
|
+
/** `schema.table` → set of column names. */
|
|
46
|
+
tables: Map<string, Set<string>>;
|
|
47
|
+
/** `schema.typename` of every enum type that already exists. */
|
|
48
|
+
enums: Set<string>;
|
|
49
|
+
}
|
|
50
|
+
export interface EnsureAction {
|
|
51
|
+
kind: "create-enum" | "create-table" | "add-column";
|
|
52
|
+
/** Qualified target, for logging: `public.posts` or `public.posts.title`. */
|
|
53
|
+
target: string;
|
|
54
|
+
sql: string;
|
|
55
|
+
}
|
|
56
|
+
export interface EnsurePlan {
|
|
57
|
+
actions: EnsureAction[];
|
|
58
|
+
/** Every statement, in dependency order. Empty when the schema is current. */
|
|
59
|
+
statements: string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Decide what to add. Pure — the caller supplies what exists and runs the result.
|
|
63
|
+
*
|
|
64
|
+
* Ordering matters and is deliberate: enum types before the tables and columns
|
|
65
|
+
* that reference them, tables before the columns added to other tables (a new
|
|
66
|
+
* table may be the target of a relation), and nothing is emitted twice.
|
|
67
|
+
*/
|
|
68
|
+
export declare function planCollectionSchemaEnsure(collections: CollectionConfig[], existing: ExistingSchema): EnsurePlan;
|
|
69
|
+
/** Read what the database has, for the schemas the collections live in. */
|
|
70
|
+
export declare function readExistingSchema(client: Queryable, schemas: string[]): Promise<ExistingSchema>;
|
|
71
|
+
/**
|
|
72
|
+
* Bring the database up to date. Returns what it did.
|
|
73
|
+
*
|
|
74
|
+
* Each statement runs on its own rather than in one transaction: they are all
|
|
75
|
+
* independently safe and idempotent, and a single failure (an enum label that
|
|
76
|
+
* cannot be added, say) should not roll back the tables that were created fine.
|
|
77
|
+
* The error is surfaced with the statement that caused it.
|
|
78
|
+
*/
|
|
79
|
+
export declare function ensureCollectionTables(client: Queryable, collections: CollectionConfig[], log?: (message: string) => void): Promise<EnsurePlan>;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { CollectionConfig } from "@rebasepro/types";
|
|
1
|
+
import { CollectionConfig, Property } from "@rebasepro/types";
|
|
2
|
+
export declare const resolveColumnName: (propName: string, prop?: Property | null) => string;
|
|
3
|
+
export declare const isIdProperty: (propName: string, prop: Property, collection: CollectionConfig) => boolean;
|
|
4
|
+
export declare const getSqlColumnType: (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]) => string;
|
|
2
5
|
export declare const generatePostgresDdl: (collections: CollectionConfig[], options?: {
|
|
3
6
|
includePolicies?: boolean;
|
|
4
7
|
}) => Promise<string>;
|
|
@@ -5,6 +5,7 @@ import type { VectorSearchParams } from "@rebasepro/types";
|
|
|
5
5
|
import { RelationService } from "./RelationService";
|
|
6
6
|
import { DrizzleClient } from "../interfaces";
|
|
7
7
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
8
|
+
import { type NestedPathHop } from "./nested-path";
|
|
8
9
|
/**
|
|
9
10
|
* Service for handling all row read operations.
|
|
10
11
|
* Handles fetching, searching, counting, and filtering rows.
|
|
@@ -65,6 +66,22 @@ export declare class FetchService {
|
|
|
65
66
|
* Extract cursor pagination conditions from startAfter options.
|
|
66
67
|
*/
|
|
67
68
|
private buildCursorConditions;
|
|
69
|
+
/**
|
|
70
|
+
* Compile "rows reachable from this parent" into a `WHERE` condition on the
|
|
71
|
+
* target table, so a nested listing can run as an ordinary collection query.
|
|
72
|
+
*/
|
|
73
|
+
private buildRelationScope;
|
|
74
|
+
/**
|
|
75
|
+
* Whether `id` is actually reachable at `collectionPath`.
|
|
76
|
+
*
|
|
77
|
+
* Trivially true for a root path. For a nested one it is a real question:
|
|
78
|
+
* the path resolves to the target collection, and matching on the primary
|
|
79
|
+
* key alone made the parent segment decorative — `authors/1/posts/43`
|
|
80
|
+
* returned post 43 whoever wrote it, and the REST layer's delete then
|
|
81
|
+
* deleted it. A row that is not under this parent is reported as absent,
|
|
82
|
+
* which is what a caller addressing it through the parent should see.
|
|
83
|
+
*/
|
|
84
|
+
private isAddressableUnder;
|
|
68
85
|
/**
|
|
69
86
|
* Fetch a single row by ID
|
|
70
87
|
*/
|
|
@@ -83,6 +100,8 @@ export declare class FetchService {
|
|
|
83
100
|
databaseId?: string;
|
|
84
101
|
vectorSearch?: VectorSearchParams;
|
|
85
102
|
logical?: LogicalCondition;
|
|
103
|
+
/** Narrow to the rows reachable from a parent through a relation. */
|
|
104
|
+
relatedTo?: NestedPathHop;
|
|
86
105
|
}): Promise<Record<string, unknown>[]>;
|
|
87
106
|
/**
|
|
88
107
|
* Fallback path used when db.query is unavailable.
|
|
@@ -118,10 +137,6 @@ export declare class FetchService {
|
|
|
118
137
|
limit?: number;
|
|
119
138
|
databaseId?: string;
|
|
120
139
|
}): Promise<Record<string, unknown>[]>;
|
|
121
|
-
/**
|
|
122
|
-
* Fetch collection from multi-segment path
|
|
123
|
-
*/
|
|
124
|
-
private fetchCollectionFromPath;
|
|
125
140
|
/**
|
|
126
141
|
* Count rows in a collection
|
|
127
142
|
*/
|
|
@@ -130,10 +145,6 @@ export declare class FetchService {
|
|
|
130
145
|
searchString?: string;
|
|
131
146
|
databaseId?: string;
|
|
132
147
|
}): Promise<number>;
|
|
133
|
-
/**
|
|
134
|
-
* Count rows from multi-segment path
|
|
135
|
-
*/
|
|
136
|
-
private countEntitiesFromPath;
|
|
137
148
|
/**
|
|
138
149
|
* Check if a field value is unique
|
|
139
150
|
*/
|
|
@@ -160,6 +171,8 @@ export declare class FetchService {
|
|
|
160
171
|
searchString?: string;
|
|
161
172
|
databaseId?: string;
|
|
162
173
|
vectorSearch?: VectorSearchParams;
|
|
174
|
+
/** Narrow to the rows reachable from a parent through a relation. */
|
|
175
|
+
relatedTo?: NestedPathHop;
|
|
163
176
|
}, include?: string[]): Promise<Record<string, unknown>[]>;
|
|
164
177
|
/**
|
|
165
178
|
* Fetch a single row with optional relation includes for REST API.
|
|
@@ -38,6 +38,18 @@ export declare class PersistService {
|
|
|
38
38
|
* Delete all rows from a collection
|
|
39
39
|
*/
|
|
40
40
|
deleteAll(collectionPath: string, _databaseId?: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* The column on the *target* table that records the parent, for a create
|
|
43
|
+
* under a nested one-to-many path.
|
|
44
|
+
*
|
|
45
|
+
* Returns `undefined` when the link is not a column at all (a multi-hop
|
|
46
|
+
* `joinPath`), so the caller writes the row without stamping anything.
|
|
47
|
+
*
|
|
48
|
+
* `relation.localKey` is deliberately not consulted: it names a column on
|
|
49
|
+
* the *source* table. Falling back to it here — which is what this used to
|
|
50
|
+
* do, and first — stamped the parent's own foreign key onto the child row.
|
|
51
|
+
*/
|
|
52
|
+
private resolveParentForeignKeyColumn;
|
|
41
53
|
/**
|
|
42
54
|
* Save an row (create or update)
|
|
43
55
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DrizzleClient } from "../interfaces";
|
|
2
2
|
import { CollectionConfig, FilterValues, Relation } from "@rebasepro/types";
|
|
3
3
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry";
|
|
4
|
+
import type { NestedPathHop } from "./nested-path";
|
|
4
5
|
/**
|
|
5
6
|
* Service for handling all relation-related operations.
|
|
6
7
|
* Handles fetching, updating, and managing row relations.
|
|
@@ -85,6 +86,35 @@ export declare class RelationService {
|
|
|
85
86
|
filter?: FilterValues<Extract<keyof M, string>>;
|
|
86
87
|
databaseId?: string;
|
|
87
88
|
}): Promise<number>;
|
|
89
|
+
/**
|
|
90
|
+
* Count the target rows a parent reaches through `relation`, narrowed by
|
|
91
|
+
* `additionalFilters` (conditions on the target table).
|
|
92
|
+
*
|
|
93
|
+
* Shared by the public count and by {@link isRelated}, so "how many children
|
|
94
|
+
* does this parent have" and "is this row one of them" are answered by the
|
|
95
|
+
* same join — a membership test that reconstructed the join separately would
|
|
96
|
+
* be free to disagree with the listing it is supposed to gate.
|
|
97
|
+
*/
|
|
98
|
+
private countRelatedRows;
|
|
99
|
+
/**
|
|
100
|
+
* Whether `targetId` is actually reachable from the parent named in `hop`.
|
|
101
|
+
*
|
|
102
|
+
* A nested address like `authors/1/posts/43` used to resolve to the target
|
|
103
|
+
* collection and then match on the primary key alone, so the parent segment
|
|
104
|
+
* decided nothing: the row came back, and was updated or deleted, whoever it
|
|
105
|
+
* belonged to. Reads, updates and deletes now all gate on this.
|
|
106
|
+
*/
|
|
107
|
+
isRelated(hop: NestedPathHop, targetId: string | number): Promise<boolean>;
|
|
108
|
+
/**
|
|
109
|
+
* Remove the junction row linking a parent to `targetId`, leaving the target
|
|
110
|
+
* row itself alone.
|
|
111
|
+
*
|
|
112
|
+
* This is what `DELETE authors/1/tags/5` has to mean for a many-to-many: the
|
|
113
|
+
* target is shared, so deleting the row would remove the tag from every other
|
|
114
|
+
* post that uses it. It used to do exactly that — resolve the path to the
|
|
115
|
+
* `tags` table and delete by primary key.
|
|
116
|
+
*/
|
|
117
|
+
unlinkRelatedEntity(tx: DrizzleClient, hop: NestedPathHop, targetId: string | number): Promise<void>;
|
|
88
118
|
/**
|
|
89
119
|
* Batch fetch related rows for multiple parent rows to avoid N+1 queries
|
|
90
120
|
*/
|
|
@@ -23,19 +23,14 @@ export declare function parseCdcPayload(payload: string): CdcChangeEvent | null;
|
|
|
23
23
|
/**
|
|
24
24
|
* Dedicated Postgres LISTEN client for database-level CDC.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* A {@link PgNotifyListener} — a connection outside the Drizzle pool that stays
|
|
27
|
+
* open and repairs itself — plus the parsing that turns a `rebase_cdc` payload
|
|
28
|
+
* into a change event. Each backend instance runs one, so every instance
|
|
29
|
+
* observes every committed change regardless of which instance (or external
|
|
30
|
+
* process) made the write.
|
|
31
31
|
*/
|
|
32
32
|
export declare class CdcListener {
|
|
33
|
-
private readonly
|
|
34
|
-
private readonly onEvent;
|
|
35
|
-
private client?;
|
|
36
|
-
private running;
|
|
37
|
-
private reconnectTimer?;
|
|
38
|
-
private static readonly RECONNECT_DELAY_MS;
|
|
33
|
+
private readonly listener;
|
|
39
34
|
constructor(connectionString: string, onEvent: (event: CdcChangeEvent) => void | Promise<void>);
|
|
40
35
|
/**
|
|
41
36
|
* Connect and begin listening. Idempotent.
|
|
@@ -44,11 +39,9 @@ export declare class CdcListener {
|
|
|
44
39
|
* established (or `LISTEN` is refused), this rejects so callers — notably
|
|
45
40
|
* `REALTIME_CDC=auto` — can detect an unusable connection and fall back to
|
|
46
41
|
* app-level realtime. Once the initial connection succeeds, later drops
|
|
47
|
-
* self-heal
|
|
42
|
+
* self-heal in the background.
|
|
48
43
|
*/
|
|
49
44
|
start(): Promise<void>;
|
|
50
45
|
/** Stop listening and release the connection. */
|
|
51
46
|
stop(): Promise<void>;
|
|
52
|
-
private connect;
|
|
53
|
-
private scheduleReconnect;
|
|
54
47
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { CollectionConfig } from "@rebasepro/types";
|
|
2
|
+
import { PostgresCollectionRegistry } from "../../collections/PostgresCollectionRegistry";
|
|
3
|
+
/**
|
|
4
|
+
* One end of a many-to-many, as seen from the junction table.
|
|
5
|
+
*
|
|
6
|
+
* A junction table is not a collection, so nothing in the registry maps it to
|
|
7
|
+
* one — which is why a change to it was invisible to change capture. But its
|
|
8
|
+
* rows are exactly the contents of a parent's child list, so a write to it is a
|
|
9
|
+
* change to `<parentSlug>/<sourceId>/<relationKey>` and to nothing else.
|
|
10
|
+
*/
|
|
11
|
+
export interface JunctionLink {
|
|
12
|
+
schema: string;
|
|
13
|
+
/** The junction table itself, e.g. `posts_tags`. */
|
|
14
|
+
table: string;
|
|
15
|
+
/** The collection whose relation this is, e.g. `posts`. */
|
|
16
|
+
parentCollection: CollectionConfig;
|
|
17
|
+
/** The relation's key — the path segment a child list is addressed by. */
|
|
18
|
+
relationKey: string;
|
|
19
|
+
/** Junction column holding the parent's id. */
|
|
20
|
+
sourceColumn: string;
|
|
21
|
+
/** Junction column holding the target's id. */
|
|
22
|
+
targetColumn: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Every junction table reachable from a registered collection, once per
|
|
26
|
+
* relation that uses it.
|
|
27
|
+
*
|
|
28
|
+
* A junction is listed once per *direction* when both sides declare it, because
|
|
29
|
+
* each direction addresses a different child list: `posts/1/tags` and
|
|
30
|
+
* `tags/t/posts` both change when one link is written.
|
|
31
|
+
*/
|
|
32
|
+
export declare function collectJunctionLinks(registry: PostgresCollectionRegistry): JunctionLink[];
|
|
33
|
+
/**
|
|
34
|
+
* Index {@link collectJunctionLinks} by table, under both the qualified and the
|
|
35
|
+
* bare name — a change event carries whatever the trigger reports, and a
|
|
36
|
+
* collection need not declare a schema.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildJunctionLinkMap(registry: PostgresCollectionRegistry): Map<string, JunctionLink[]>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime pieces of the channel bus that are not the contract itself.
|
|
3
|
+
*
|
|
4
|
+
* The interface, the frame shape and the implementer's contract live in
|
|
5
|
+
* `@rebasepro/types` (`types/channel_bus.ts`), so a transport shipped as its own
|
|
6
|
+
* package — a Redis one, say — depends on the contract and not on this database
|
|
7
|
+
* adapter. They are re-exported here for convenience: code already importing
|
|
8
|
+
* from the adapter should not have to know where the types are declared.
|
|
9
|
+
*/
|
|
10
|
+
import type { ChannelBusFrame } from "@rebasepro/types";
|
|
11
|
+
export type { ChannelBus, ChannelBusFrame, ChannelBusHandler, ChannelBusConfig, ChannelBusSetting } from "@rebasepro/types";
|
|
12
|
+
export { isChannelBusInstance } from "@rebasepro/types";
|
|
13
|
+
/**
|
|
14
|
+
* The default: no cross-instance delivery at all.
|
|
15
|
+
*
|
|
16
|
+
* This is what every deployment ran before the bus existed, and what a
|
|
17
|
+
* single-instance deployment should keep running — `publish` resolves without
|
|
18
|
+
* touching the network, so the broadcast path is the same handful of `ws.send`
|
|
19
|
+
* calls it always was.
|
|
20
|
+
*/
|
|
21
|
+
export declare class MemoryChannelBus {
|
|
22
|
+
readonly kind: "memory";
|
|
23
|
+
readonly maxFrameBytes: number;
|
|
24
|
+
start(): Promise<void>;
|
|
25
|
+
publish(): Promise<void>;
|
|
26
|
+
stop(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/** Encoded size of a frame, for a transport's size check. */
|
|
29
|
+
export declare function frameByteLength(frame: ChannelBusFrame): number;
|