@rebasepro/server-postgres 0.23.0 → 0.24.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/{BranchService-DGPL6G_C.js → BranchService-BRt78gfa.js} +35 -18
- package/dist/BranchService-BRt78gfa.js.map +1 -0
- package/dist/PostgresBackendDriver.d.ts +84 -12
- package/dist/auth/services.d.ts +72 -14
- package/dist/{auth-users-columns-D2LBFrMH.js → auth-users-columns-C72EMoDJ.js} +17 -1
- package/dist/{auth-users-columns-D2LBFrMH.js.map → auth-users-columns-C72EMoDJ.js.map} +1 -1
- package/dist/{backup-cli-24v4OlSp.js → backup-cli-DW5p9_zv.js} +2 -2
- package/dist/{backup-cli-24v4OlSp.js.map → backup-cli-DW5p9_zv.js.map} +1 -1
- package/dist/{backup-service-HQ9GC4tN.js → backup-service-EwcDVG-8.js} +7 -9
- package/dist/{backup-service-HQ9GC4tN.js.map → backup-service-EwcDVG-8.js.map} +1 -1
- package/dist/{cli-errors-B8qHg02P.js → cli-errors-DsA-K9uP.js} +96 -1
- package/dist/{cli-errors-B8qHg02P.js.map → cli-errors-DsA-K9uP.js.map} +1 -1
- package/dist/cli-errors.d.ts +42 -0
- package/dist/cli-helpers.d.ts +12 -12
- package/dist/cli.js +91 -37
- package/dist/cli.js.map +1 -1
- package/dist/{column-plan-helpers-DF-8dTVa.js → column-plan-helpers-1-LQD0yI.js} +34 -24
- package/dist/column-plan-helpers-1-LQD0yI.js.map +1 -0
- package/dist/data-transformer.d.ts +0 -8
- package/dist/{doctor-Cb2thZ8s.js → doctor-C-sYWbmt.js} +224 -43
- package/dist/doctor-C-sYWbmt.js.map +1 -0
- package/dist/{ensure-collection-policies-RHUcEp4v.js → ensure-collection-policies-B1ureSIV.js} +73 -18
- package/dist/ensure-collection-policies-B1ureSIV.js.map +1 -0
- package/dist/{ensure-collection-tables-BEEjn5cn.js → ensure-collection-tables-kkkHk8oo.js} +76 -20
- package/dist/ensure-collection-tables-kkkHk8oo.js.map +1 -0
- package/dist/{ensure-tables-Dhn9KM3B.js → ensure-tables-BmI_tRxc.js} +10 -3
- package/dist/ensure-tables-BmI_tRxc.js.map +1 -0
- package/dist/{generate-drizzle-schema-yzY_BLhr.js → generate-drizzle-schema-93M0lUxK.js} +2 -2
- package/dist/{generate-drizzle-schema-yzY_BLhr.js.map → generate-drizzle-schema-93M0lUxK.js.map} +1 -1
- package/dist/{generate-drizzle-schema-logic-BfzK7UQd.js → generate-drizzle-schema-logic-sSFDp6LR.js} +26 -8
- package/dist/generate-drizzle-schema-logic-sSFDp6LR.js.map +1 -0
- package/dist/generate-postgres-ddl-logic-BJsLaVNX.js +152 -0
- package/dist/generate-postgres-ddl-logic-BJsLaVNX.js.map +1 -0
- package/dist/generated-sql.d.ts +28 -0
- package/dist/index.es.js +3415 -917
- package/dist/index.es.js.map +1 -1
- package/dist/{introspect-db-logic-WMuAfxvw.js → introspect-db-logic-kCETE8TY.js} +523 -349
- package/dist/introspect-db-logic-kCETE8TY.js.map +1 -0
- package/dist/introspect-db-queries-C_Q5VgQw.js +317 -0
- package/dist/introspect-db-queries-C_Q5VgQw.js.map +1 -0
- package/dist/{plan-schema-C0fxM8dY.js → plan-schema-DU9exq6C.js} +139 -504
- package/dist/plan-schema-DU9exq6C.js.map +1 -0
- package/dist/{policy-drift-DljYdrpW.js → policy-drift-B-J2hhm0.js} +3 -3
- package/dist/policy-drift-B-J2hhm0.js.map +1 -0
- package/dist/{generate-postgres-ddl-logic-Bt2d2mRH.js → render-ddl-Ds2t_d9V.js} +11 -149
- package/dist/render-ddl-Ds2t_d9V.js.map +1 -0
- package/dist/{rls-bootstrap-sql-B8EclDyM.js → rls-bootstrap-sql-_KNnjanK.js} +263 -39
- package/dist/rls-bootstrap-sql-_KNnjanK.js.map +1 -0
- package/dist/{rls-enforcement-C6Xk0lA6.js → rls-enforcement-CfXOJJaW.js} +10 -2
- package/dist/rls-enforcement-CfXOJJaW.js.map +1 -0
- package/dist/schema/auth-schema.d.ts +170 -0
- package/dist/schema/classify-change.d.ts +29 -1
- package/dist/schema/column-plan-helpers.d.ts +33 -21
- package/dist/schema/destructive-sql.d.ts +20 -1
- package/dist/schema/doctor-cli.js +4 -4
- package/dist/schema/doctor.d.ts +34 -1
- package/dist/schema/ensure-collection-policies.d.ts +22 -0
- package/dist/schema/generate-drizzle-schema.js +1 -1
- package/dist/schema/generate-postgres-ddl-logic.d.ts +5 -5
- package/dist/schema/generate-postgres-ddl.js +1 -1
- package/dist/schema/generate-schema-commit.d.ts +12 -0
- package/dist/schema/introspect-db-logic.d.ts +31 -0
- package/dist/schema/introspect-db-queries.d.ts +1 -1
- package/dist/schema/introspect-db-search.d.ts +22 -0
- package/dist/schema/introspect-db-storage.d.ts +83 -0
- package/dist/schema/introspect-db.js +15 -3
- package/dist/schema/introspect-db.js.map +1 -1
- package/dist/schema/plan/diff-plan.d.ts +1 -1
- package/dist/schema/plan/plan-schema.d.ts +16 -8
- package/dist/schema/plan/render-ddl.d.ts +6 -0
- package/dist/schema/plan/types.d.ts +37 -11
- package/dist/search-column-BM-GV6vH.js +442 -0
- package/dist/search-column-BM-GV6vH.js.map +1 -0
- package/dist/security/policy-drift.d.ts +1 -1
- package/dist/security/rls-enforcement.d.ts +7 -0
- package/dist/services/BranchService.d.ts +22 -1
- package/dist/services/FetchService.d.ts +5 -4
- package/dist/services/RelationService.d.ts +18 -0
- package/dist/services/cdc/CdcListener.d.ts +9 -4
- package/dist/services/cdc/identity-columns.d.ts +19 -0
- package/dist/services/cdc/trigger-cdc.d.ts +45 -7
- package/dist/services/channel-bus/PostgresChannelBus.d.ts +12 -7
- package/dist/services/channel-history.d.ts +17 -1
- package/dist/services/collection-helpers.d.ts +11 -1
- package/dist/services/pg-notify-listener.d.ts +88 -6
- package/dist/services/realtimeService.d.ts +159 -45
- package/dist/services/socket-liveness.d.ts +45 -0
- package/dist/services/soft-delete.d.ts +12 -0
- package/dist/services/sql-script.d.ts +71 -0
- package/dist/services/write-depth.d.ts +16 -0
- package/dist/services/write-transaction-scope.d.ts +6 -3
- package/dist/utils/drizzle-conditions.d.ts +42 -2
- package/dist/utils/sql-redaction.d.ts +21 -0
- package/dist/websocket.d.ts +50 -18
- package/package.json +7 -7
- package/dist/BranchService-DGPL6G_C.js.map +0 -1
- package/dist/column-plan-helpers-DF-8dTVa.js.map +0 -1
- package/dist/doctor-Cb2thZ8s.js.map +0 -1
- package/dist/ensure-collection-policies-RHUcEp4v.js.map +0 -1
- package/dist/ensure-collection-tables-BEEjn5cn.js.map +0 -1
- package/dist/ensure-tables-Dhn9KM3B.js.map +0 -1
- package/dist/generate-drizzle-schema-logic-BfzK7UQd.js.map +0 -1
- package/dist/generate-postgres-ddl-logic-Bt2d2mRH.js.map +0 -1
- package/dist/introspect-db-logic-WMuAfxvw.js.map +0 -1
- package/dist/plan-schema-C0fxM8dY.js.map +0 -1
- package/dist/policy-drift-DljYdrpW.js.map +0 -1
- package/dist/rls-bootstrap-sql-B8EclDyM.js.map +0 -1
- package/dist/rls-enforcement-C6Xk0lA6.js.map +0 -1
|
@@ -5,7 +5,7 @@ import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
|
5
5
|
import { RealtimeProvider, CollectionSubscriptionConfig, SingleSubscriptionConfig } from "../interfaces.js";
|
|
6
6
|
import { PostgresCollectionRegistry } from "../collections/PostgresCollectionRegistry.js";
|
|
7
7
|
import { ChannelBus } from "./channel-bus/index.js";
|
|
8
|
-
import type { ChannelRetentionRule } from "@rebasepro/types";
|
|
8
|
+
import type { ChannelRetentionRule, RealtimeListenerHealth } from "@rebasepro/types";
|
|
9
9
|
/**
|
|
10
10
|
* Auth context stored per-subscription so real-time refetches respect RLS.
|
|
11
11
|
* Mirrors the session variables set by PostgresBackendDriver.withAuth().
|
|
@@ -133,7 +133,11 @@ export type DataDriverSubscriptionRequest = {
|
|
|
133
133
|
* last delivery *started* the last one *delivered*.
|
|
134
134
|
*/
|
|
135
135
|
type Subscription = {
|
|
136
|
+
/** The id the client named it by — what every frame for it carries. */
|
|
137
|
+
subscriptionId: string;
|
|
136
138
|
clientId: string;
|
|
139
|
+
/** The {@link SubscriptionGroup} whose refetch answers it. */
|
|
140
|
+
groupKey: string;
|
|
137
141
|
type: "collection" | "single";
|
|
138
142
|
path: string;
|
|
139
143
|
id?: string | number;
|
|
@@ -150,6 +154,24 @@ type Subscription = {
|
|
|
150
154
|
/** The highest started-sequence that has already reached the subscriber. */
|
|
151
155
|
delivered: number;
|
|
152
156
|
};
|
|
157
|
+
/**
|
|
158
|
+
* The default ceiling on subscriptions one socket may hold.
|
|
159
|
+
*
|
|
160
|
+
* Each collection subscription is a refetch on every write to its collection;
|
|
161
|
+
* without a ceiling one socket could open tens of thousands and turn every
|
|
162
|
+
* write anyone made into as many transactions. The SDK shares identical
|
|
163
|
+
* subscriptions on a socket, so a real page holds one per distinct list or
|
|
164
|
+
* record on screen — the admin panel's tables, forms and reference previews
|
|
165
|
+
* stay well under this. See `REALTIME_MAX_SUBSCRIPTIONS_PER_SOCKET`.
|
|
166
|
+
*/
|
|
167
|
+
export declare const DEFAULT_MAX_SUBSCRIPTIONS_PER_SOCKET = 1000;
|
|
168
|
+
/**
|
|
169
|
+
* Read a subscription ceiling, refusing anything that is not a positive whole
|
|
170
|
+
* number. A typo here does not fall back to a default: the ceiling is a limit
|
|
171
|
+
* someone chose, and silently running with another one is how it stops
|
|
172
|
+
* meaning anything.
|
|
173
|
+
*/
|
|
174
|
+
export declare function parseMaxSubscriptionsPerSocket(value: unknown, source: string): number;
|
|
153
175
|
/**
|
|
154
176
|
* PostgreSQL-specific realtime service.
|
|
155
177
|
* Handles WebSocket connections and subscriptions for real-time row updates.
|
|
@@ -188,6 +210,8 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
188
210
|
* wait on each other.
|
|
189
211
|
*/
|
|
190
212
|
private channelSendQueues;
|
|
213
|
+
/** The receiving half of the same ordering: retained bus frames per channel, one at a time. */
|
|
214
|
+
private channelReceiveQueues;
|
|
191
215
|
/**
|
|
192
216
|
* Cross-instance transport for channel frames and presence.
|
|
193
217
|
*
|
|
@@ -233,21 +257,46 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
233
257
|
/** How often stale roster rows from other instances are reaped. */
|
|
234
258
|
private static readonly PRESENCE_SWEEP_INTERVAL_MS;
|
|
235
259
|
private dataService;
|
|
260
|
+
/**
|
|
261
|
+
* Every subscription, by {@link subscriptionKey} — the client's own id
|
|
262
|
+
* qualified by the client that chose it.
|
|
263
|
+
*
|
|
264
|
+
* Keyed by the bare id, this was one namespace for every socket: two raw
|
|
265
|
+
* clients that both counted from `"sub-1"` replaced each other's
|
|
266
|
+
* subscription without a word, and either one's `unsubscribe` ended the
|
|
267
|
+
* other's.
|
|
268
|
+
*/
|
|
236
269
|
private _subscriptions;
|
|
270
|
+
/** In-process listeners' callbacks, by subscription key. */
|
|
237
271
|
private subscriptionCallbacks;
|
|
272
|
+
/** Each client's subscriptions, by key — for its ceiling and its disconnect. */
|
|
273
|
+
private subscriptionsByClient;
|
|
274
|
+
/** Subscriptions one refetch answers — see {@link SubscriptionGroup}. */
|
|
275
|
+
private groups;
|
|
276
|
+
/**
|
|
277
|
+
* The collection groups on each path, and the single-row groups at each
|
|
278
|
+
* `path` + id: what a change looks up, instead of walking every
|
|
279
|
+
* subscription on the server once per changed row.
|
|
280
|
+
*/
|
|
281
|
+
private collectionGroupsByPath;
|
|
282
|
+
private singleGroupsByAddress;
|
|
283
|
+
/**
|
|
284
|
+
* Every path a subscription holds, with the collection it lands on and how
|
|
285
|
+
* many hold it — the candidates `aliasPaths` considers, resolved once.
|
|
286
|
+
*/
|
|
287
|
+
private subscribedPaths;
|
|
288
|
+
/**
|
|
289
|
+
* How many subscriptions one socket may hold; set from config at boot. See
|
|
290
|
+
* {@link DEFAULT_MAX_SUBSCRIPTIONS_PER_SOCKET}.
|
|
291
|
+
*/
|
|
292
|
+
maxSubscriptionsPerSocket: number;
|
|
238
293
|
private driver?;
|
|
239
294
|
/** Unique identifier for this process instance, used to skip own notifications. */
|
|
240
295
|
private readonly instanceId;
|
|
241
|
-
/**
|
|
242
|
-
private
|
|
243
|
-
/** Connection string used for reconnecting the LISTEN client. */
|
|
244
|
-
private listenConnectionString?;
|
|
296
|
+
/** The LISTEN connection on {@link PG_NOTIFY_CHANNEL}, outside the Drizzle pool. */
|
|
297
|
+
private crossInstanceListener?;
|
|
245
298
|
/** Whether cross-instance broadcasting is active. */
|
|
246
299
|
private broadcasting;
|
|
247
|
-
/** Reconnection timer handle. */
|
|
248
|
-
private reconnectTimer?;
|
|
249
|
-
/** Debounce timers for collection refetches to prevent refetch storms. */
|
|
250
|
-
private refetchTimers;
|
|
251
300
|
/** Debounce window (ms) for coalescing rapid row updates into a single correctness refetch. */
|
|
252
301
|
private static readonly REFETCH_DEBOUNCE_MS;
|
|
253
302
|
/** Dedicated LISTEN client for DB-level change events (undefined unless CDC is enabled). */
|
|
@@ -301,7 +350,50 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
301
350
|
*/
|
|
302
351
|
private debugLog;
|
|
303
352
|
setDataDriver(driver: DataDriver): void;
|
|
304
|
-
|
|
353
|
+
/**
|
|
354
|
+
* The live subscriptions by the id their client gave them — for tests and
|
|
355
|
+
* diagnostics. Two clients may use one id; this view shows the last.
|
|
356
|
+
*/
|
|
357
|
+
get subscriptions(): ReadonlyMap<string, Subscription>;
|
|
358
|
+
/** The internal key of `subscriptionId` as `clientId` named it. */
|
|
359
|
+
private subscriptionKey;
|
|
360
|
+
/**
|
|
361
|
+
* The group a subscription's refetch belongs to. Socket subscriptions that
|
|
362
|
+
* would run the same read as the same principal share one; an in-process
|
|
363
|
+
* listener is always alone — see {@link SubscriptionGroup}.
|
|
364
|
+
*/
|
|
365
|
+
private groupKeyFor;
|
|
366
|
+
/**
|
|
367
|
+
* Register (or replace) a subscription under `key`, in its group and in
|
|
368
|
+
* every index that finds it.
|
|
369
|
+
*/
|
|
370
|
+
private registerSubscription;
|
|
371
|
+
/** Remove the subscription under `key` from everything that holds it. */
|
|
372
|
+
private dropSubscription;
|
|
373
|
+
/** Take `subscription` out of its group and path count; a group left empty goes, timer and all. */
|
|
374
|
+
private leaveGroup;
|
|
375
|
+
/**
|
|
376
|
+
* Ask for a refetch of `group` once changes have been quiet for the
|
|
377
|
+
* debounce window — for every member, or only for `member`.
|
|
378
|
+
*
|
|
379
|
+
* The window is measured from the last change, as a debounce is, but the
|
|
380
|
+
* timer is not re-armed per change: a 10k-row statement is 10k changes,
|
|
381
|
+
* and re-arming a timer per change per group was work in proportion to
|
|
382
|
+
* both. A change only moves the timestamp; the timer, when it fires early,
|
|
383
|
+
* waits out the rest.
|
|
384
|
+
*/
|
|
385
|
+
private scheduleGroupRefetch;
|
|
386
|
+
private armGroupTimer;
|
|
387
|
+
/**
|
|
388
|
+
* One read for the group, delivered to each member it is for.
|
|
389
|
+
*
|
|
390
|
+
* Each member still claims its own delivery slot before the read and
|
|
391
|
+
* checks it after (see {@link beginDelivery}), so a member that left, was
|
|
392
|
+
* replaced or was overtaken while the read ran is skipped exactly as it
|
|
393
|
+
* was when it had a read of its own. A socket frame is serialised once and
|
|
394
|
+
* only its subscription id differs per member.
|
|
395
|
+
*/
|
|
396
|
+
private refetchGroup;
|
|
305
397
|
/**
|
|
306
398
|
* Claim a delivery slot for a subscription, before doing the work.
|
|
307
399
|
*
|
|
@@ -355,13 +447,27 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
355
447
|
*/
|
|
356
448
|
subscribeToOne(subscriptionId: string, config: SingleSubscriptionConfig, callback?: (row: Record<string, unknown> | null) => void, onError?: (error: unknown) => void): void;
|
|
357
449
|
/**
|
|
358
|
-
* Unsubscribe
|
|
450
|
+
* Unsubscribe an in-process subscription (RealtimeProvider interface).
|
|
451
|
+
*
|
|
452
|
+
* By id alone, because the in-process callers hold nothing else — so a
|
|
453
|
+
* socket's subscription under the same id is not this one, and is left
|
|
454
|
+
* alone. A socket ends its own through the `unsubscribe` frame.
|
|
359
455
|
*/
|
|
360
456
|
unsubscribe(subscriptionId: string): void;
|
|
361
457
|
addClient(clientId: string, ws: WebSocket): void;
|
|
362
458
|
handleClientMessage(clientId: string, message: WebSocketMessage, authContext?: SubscriptionAuthContext): Promise<void>;
|
|
363
459
|
removeClient(clientId: string): Promise<void>;
|
|
364
460
|
private handleMessage;
|
|
461
|
+
/**
|
|
462
|
+
* Refuse a new subscription past the socket's ceiling, and say so.
|
|
463
|
+
*
|
|
464
|
+
* Every collection subscription is a refetch on every write to its
|
|
465
|
+
* collection, so a socket allowed to open them without limit could turn
|
|
466
|
+
* each write anyone makes into tens of thousands of transactions. Replacing
|
|
467
|
+
* a subscription the socket already holds — the same id again — is not a
|
|
468
|
+
* new one and always passes. Returns whether the subscription may go ahead.
|
|
469
|
+
*/
|
|
470
|
+
private admitSubscription;
|
|
365
471
|
private handleCollectionSubscription;
|
|
366
472
|
private handleEntitySubscription;
|
|
367
473
|
/**
|
|
@@ -397,6 +503,10 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
397
503
|
* again, when one is installed, and left when it refuses.
|
|
398
504
|
*/
|
|
399
505
|
rescopeClient(clientId: string, authContext: SubscriptionAuthContext): Promise<void>;
|
|
506
|
+
/**
|
|
507
|
+
* End a socket's own subscription. Only its own: the id is the client's,
|
|
508
|
+
* so another socket's subscription under the same id is a different one.
|
|
509
|
+
*/
|
|
400
510
|
private handleUnsubscribe;
|
|
401
511
|
/**
|
|
402
512
|
* Enhanced notification method that handles nested relation updates.
|
|
@@ -446,15 +556,6 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
446
556
|
* whether this subscriber may see the row at all.
|
|
447
557
|
*/
|
|
448
558
|
private notifyPathUpdate;
|
|
449
|
-
/**
|
|
450
|
-
* Debounce a collection refetch for a WebSocket subscription.
|
|
451
|
-
* Coalesces rapid row mutations into a single database query.
|
|
452
|
-
*/
|
|
453
|
-
private debouncedCollectionRefetch;
|
|
454
|
-
/**
|
|
455
|
-
* Debounce a collection refetch for a DataDriver callback subscription.
|
|
456
|
-
*/
|
|
457
|
-
private debouncedDriverRefetch;
|
|
458
559
|
/**
|
|
459
560
|
* Tell an in-process listener its refetch failed, through the slot the
|
|
460
561
|
* rows would have used.
|
|
@@ -484,19 +585,17 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
484
585
|
* the total describes the same set they came from.
|
|
485
586
|
*/
|
|
486
587
|
private collectionMetaWithAuth;
|
|
487
|
-
/**
|
|
488
|
-
* Debounce an row refetch for a WebSocket subscription.
|
|
489
|
-
*/
|
|
490
|
-
private debouncedSingleRefetch;
|
|
491
|
-
/**
|
|
492
|
-
* Debounce an row refetch for a Driver callback subscription.
|
|
493
|
-
*/
|
|
494
|
-
private debouncedSingleDriverRefetch;
|
|
495
588
|
/**
|
|
496
589
|
* Fetch a single row with optional RLS auth context.
|
|
497
590
|
*/
|
|
498
591
|
private fetchEntityWithAuth;
|
|
499
592
|
private sendCollectionUpdate;
|
|
593
|
+
/**
|
|
594
|
+
* Everything in a `collection_update` frame after its subscription id,
|
|
595
|
+
* serialised once for every member of a group: the same rows, keys and
|
|
596
|
+
* meta as {@link sendCollectionUpdate} sends, in the same order.
|
|
597
|
+
*/
|
|
598
|
+
private collectionFrameTail;
|
|
500
599
|
private sendSingleUpdate;
|
|
501
600
|
/**
|
|
502
601
|
* Send a lightweight row-level patch to a collection subscriber.
|
|
@@ -524,6 +623,15 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
524
623
|
*/
|
|
525
624
|
private sendError;
|
|
526
625
|
private sendMessage;
|
|
626
|
+
/**
|
|
627
|
+
* Send an already-serialised frame to a client, if it is still connected.
|
|
628
|
+
*
|
|
629
|
+
* Every frame to a socket goes through here, so this is where a client
|
|
630
|
+
* that does not read what it is sent is let go of — see
|
|
631
|
+
* {@link terminateIfBacklogged}. Terminated, it closes like any other
|
|
632
|
+
* socket, and {@link removeClient} drops what it held.
|
|
633
|
+
*/
|
|
634
|
+
private sendRaw;
|
|
527
635
|
/**
|
|
528
636
|
* Extract parent paths from a nested path like "posts/70/tags"
|
|
529
637
|
* Returns ["posts", "posts/70"] for the example above
|
|
@@ -668,6 +776,7 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
668
776
|
* its own `sid`.
|
|
669
777
|
*/
|
|
670
778
|
private handleBusFrame;
|
|
779
|
+
private deliverBusFrame;
|
|
671
780
|
/**
|
|
672
781
|
* Install retention rules and create the tables they need.
|
|
673
782
|
*
|
|
@@ -757,8 +866,16 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
757
866
|
* does not answer the close frame is dropped after a short grace.
|
|
758
867
|
*/
|
|
759
868
|
destroy(): Promise<void>;
|
|
760
|
-
/**
|
|
869
|
+
/**
|
|
870
|
+
* Whether database-level change capture is the source and is listening.
|
|
871
|
+
*
|
|
872
|
+
* Configured is not enough: a CDC connection that went half-open used to
|
|
873
|
+
* leave this `true` while nothing arrived. False while the connection is
|
|
874
|
+
* down and being replaced.
|
|
875
|
+
*/
|
|
761
876
|
isCdcActive(): boolean;
|
|
877
|
+
/** The LISTEN connections this service depends on — see `RealtimeProvider.health`. */
|
|
878
|
+
health(): RealtimeListenerHealth[];
|
|
762
879
|
/**
|
|
763
880
|
* Enable database-level change capture as the realtime source.
|
|
764
881
|
*
|
|
@@ -802,8 +919,8 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
802
919
|
/**
|
|
803
920
|
* Route a captured database change into the realtime pipeline.
|
|
804
921
|
*
|
|
805
|
-
* Delivery is RLS-safe by construction: the
|
|
806
|
-
* NOT forwarded to subscribers. Instead the change is marked invalidated, so
|
|
922
|
+
* Delivery is RLS-safe by construction: the event carries the changed row's
|
|
923
|
+
* key and nothing else, and even that is NOT forwarded to subscribers. Instead the change is marked invalidated, so
|
|
807
924
|
* every matching subscription re-reads the row under its own auth context via
|
|
808
925
|
* {@link fetchCollectionWithAuth} / {@link fetchEntityWithAuth}. A subscriber
|
|
809
926
|
* therefore only ever receives rows its RLS policies permit — filtering is per
|
|
@@ -846,8 +963,15 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
846
963
|
private consumeAppEmit;
|
|
847
964
|
/**
|
|
848
965
|
* Enable cross-instance realtime broadcasting via Postgres LISTEN/NOTIFY.
|
|
849
|
-
*
|
|
850
|
-
*
|
|
966
|
+
* Listens on a dedicated connection (outside the Drizzle pool) for the
|
|
967
|
+
* changes other instances publish, through a {@link PgNotifyListener}: it
|
|
968
|
+
* proves itself with a heartbeat, is replaced when it drops or stops
|
|
969
|
+
* answering, and every subscription is refetched once it is back, since
|
|
970
|
+
* whatever was published meanwhile reached nobody here.
|
|
971
|
+
*
|
|
972
|
+
* A database that cannot be reached at boot does not fail this call: the
|
|
973
|
+
* listener reports itself down (see {@link health}) and keeps dialling.
|
|
974
|
+
* There is no other cross-instance path to fall back to.
|
|
851
975
|
*
|
|
852
976
|
* This is an **optional** feature — if never called, the backend operates
|
|
853
977
|
* in single-instance mode (the default, perfectly fine for most setups).
|
|
@@ -864,18 +988,8 @@ export declare class RealtimeService extends EventEmitter implements RealtimePro
|
|
|
864
988
|
* Uses the main Drizzle connection (pooled) — NOT the LISTEN client.
|
|
865
989
|
*/
|
|
866
990
|
private broadcastChange;
|
|
867
|
-
/**
|
|
868
|
-
|
|
869
|
-
*
|
|
870
|
-
* @param reconnect Set when the client is coming back from a drop: every
|
|
871
|
-
* notification published in the gap is gone, so the subscriptions
|
|
872
|
-
* are refetched once it is listening again.
|
|
873
|
-
*/
|
|
874
|
-
private connectListenClient;
|
|
875
|
-
/**
|
|
876
|
-
* Schedule a reconnection attempt with a fixed 3s delay.
|
|
877
|
-
*/
|
|
878
|
-
private scheduleReconnect;
|
|
991
|
+
/** One change another instance published on {@link PG_NOTIFY_CHANNEL}. */
|
|
992
|
+
private handleCrossInstanceNotification;
|
|
879
993
|
}
|
|
880
994
|
/**
|
|
881
995
|
* Alias for RealtimeService for consistent naming with other database implementations.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Letting go of client sockets the server can no longer reach.
|
|
3
|
+
*
|
|
4
|
+
* A phone that loses signal, a laptop that sleeps, a NAT that forgets the
|
|
5
|
+
* connection: the peer is gone, and nothing tells the server. No close frame
|
|
6
|
+
* arrives and no FIN, so the socket stays open until the operating system gives
|
|
7
|
+
* up on the TCP connection — about two hours on Linux. Meanwhile every
|
|
8
|
+
* subscription it holds is refetched on every write to its collection, and
|
|
9
|
+
* every frame for it is serialised into a send buffer nobody drains.
|
|
10
|
+
*
|
|
11
|
+
* Two checks catch it. The socket must answer a ping: one that has not answered
|
|
12
|
+
* by the next is terminated. And it must read what it is sent: one whose unsent
|
|
13
|
+
* backlog passes {@link MAX_SOCKET_BUFFERED_BYTES} is terminated at the next
|
|
14
|
+
* frame for it. A terminated socket closes like any other, so the realtime
|
|
15
|
+
* service drops its subscriptions, channels and presence on its `close`.
|
|
16
|
+
*/
|
|
17
|
+
import { WebSocket, type WebSocketServer } from "ws";
|
|
18
|
+
/**
|
|
19
|
+
* How often every socket is pinged. A socket that has not answered one ping by
|
|
20
|
+
* the next is terminated, so a peer that vanished is let go of within two
|
|
21
|
+
* intervals. Every browser and `ws` client answers a ping by itself.
|
|
22
|
+
*/
|
|
23
|
+
export declare const SOCKET_PING_INTERVAL_MS = 30000;
|
|
24
|
+
/**
|
|
25
|
+
* The unsent bytes a socket may have queued before it is terminated.
|
|
26
|
+
*
|
|
27
|
+
* A healthy client drains its queue as fast as the network allows; one whose
|
|
28
|
+
* backlog keeps growing is not reading, or is so far behind that what it would
|
|
29
|
+
* read is stale. Set well above a single frame — the largest body the data API
|
|
30
|
+
* accepts is 10 MiB, and a list frame can approach it — so a burst of large
|
|
31
|
+
* frames to a client that is keeping up does not trip it. A terminated client
|
|
32
|
+
* reconnects and resubscribes, which brings it up to date with one frame per
|
|
33
|
+
* subscription instead of the backlog.
|
|
34
|
+
*/
|
|
35
|
+
export declare const MAX_SOCKET_BUFFERED_BYTES: number;
|
|
36
|
+
/**
|
|
37
|
+
* Ping every socket of `wss` each `intervalMs`, and terminate the ones that did
|
|
38
|
+
* not answer the previous ping. Returns the function that stops it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function reapSilentSockets(wss: WebSocketServer, intervalMs?: number): () => void;
|
|
41
|
+
/**
|
|
42
|
+
* Terminate `ws` if its unsent backlog has passed {@link MAX_SOCKET_BUFFERED_BYTES}.
|
|
43
|
+
* Returns whether it did; the caller then sends nothing.
|
|
44
|
+
*/
|
|
45
|
+
export declare function terminateIfBacklogged(ws: WebSocket, clientId: string): boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SQL } from "drizzle-orm";
|
|
2
2
|
import type { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
3
3
|
import type { CollectionConfig } from "@rebasepro/types";
|
|
4
|
+
import { ApiError } from "@rebasepro/server";
|
|
4
5
|
/**
|
|
5
6
|
* Soft delete, in one place.
|
|
6
7
|
*
|
|
@@ -69,3 +70,14 @@ export declare function withSoftDelete(conditions: SQL[], collection: Collection
|
|
|
69
70
|
* For the call sites that hold one composed condition rather than a list.
|
|
70
71
|
*/
|
|
71
72
|
export declare function andSoftDelete(where: SQL | undefined, collection: CollectionConfig | undefined, table: PgTable, withDeleted?: WithDeleted): SQL | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* The refusal of an upsert whose key belongs to a row in the trash.
|
|
75
|
+
*
|
|
76
|
+
* The key is taken — the stamped row still holds it — so the write cannot be a
|
|
77
|
+
* create, and an upsert is not the operation that brings a deleted row back:
|
|
78
|
+
* that is the restore, an update setting the field to `null`, which goes
|
|
79
|
+
* through the update gate and is what history records as one. Left to the
|
|
80
|
+
* statement, `INSERT … ON CONFLICT DO UPDATE` wrote the new values into the
|
|
81
|
+
* hidden row and the caller was told "created", with the row still in the trash.
|
|
82
|
+
*/
|
|
83
|
+
export declare function rowInTrashError(path: string, key: string, softDelete: SoftDeleteField): ApiError;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Pool, PoolClient } from "pg";
|
|
2
|
+
import type { SqlScriptResult } from "@rebasepro/types";
|
|
3
|
+
/**
|
|
4
|
+
* `INSERT 0 1` → `INSERT`, 1. `CREATE TABLE` → `CREATE TABLE`, no count.
|
|
5
|
+
*
|
|
6
|
+
* Only the commands whose tag ends in a count have one; the numbers are
|
|
7
|
+
* stripped from the end of every other tag, of which there are none.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseCommandTag(tag: string | undefined): {
|
|
10
|
+
command?: string;
|
|
11
|
+
rowCount?: number;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* What puts a session back after caller SQL: the session user and role, then
|
|
15
|
+
* every setting. Two statements, because the extended protocol refuses a
|
|
16
|
+
* multi-command string. Pinned settings (`search_path` from the connection's
|
|
17
|
+
* startup options) come back as they were pinned.
|
|
18
|
+
*/
|
|
19
|
+
export declare const RESET_SESSION_STATEMENTS: readonly ["SET SESSION AUTHORIZATION DEFAULT", "RESET ALL"];
|
|
20
|
+
/**
|
|
21
|
+
* A connection's transaction state, as its last ReadyForQuery reported it:
|
|
22
|
+
* `I` idle, `T` in a transaction, `E` in a failed one. `null` when the client
|
|
23
|
+
* does not say (node-postgres has since 8.16; the package requires 8.22).
|
|
24
|
+
*/
|
|
25
|
+
export declare function transactionStatus(connection: object): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* SQL a person ran began a transaction and did not end it.
|
|
28
|
+
*
|
|
29
|
+
* Each run is a session of its own, so a transaction cannot outlive the run
|
|
30
|
+
* that began it: a `BEGIN` alone, then an `UPDATE` in the next run, then a
|
|
31
|
+
* `ROLLBACK` in a third, committed the UPDATE and reported three successes.
|
|
32
|
+
* Refused instead, with the transaction rolled back.
|
|
33
|
+
*/
|
|
34
|
+
export declare class SqlTransactionLeftOpenError extends Error {
|
|
35
|
+
readonly code = "SQL_TRANSACTION_LEFT_OPEN";
|
|
36
|
+
constructor();
|
|
37
|
+
}
|
|
38
|
+
export interface OwnSessionOptions {
|
|
39
|
+
/**
|
|
40
|
+
* Put the session in the role the SQL runs as. Called once, before it, at
|
|
41
|
+
* session level — so the role holds for every statement, a `COMMIT` in the
|
|
42
|
+
* middle included. `SET LOCAL ROLE` inside a wrapping transaction did not:
|
|
43
|
+
* the caller's `COMMIT` ended that transaction, and every statement after
|
|
44
|
+
* it ran as the connection owner.
|
|
45
|
+
*/
|
|
46
|
+
assumeRole?: (connection: PoolClient) => Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Run `work` — SQL a person wrote — on a connection of its own from `pool`,
|
|
50
|
+
* and put the connection back the way it was handed out.
|
|
51
|
+
*
|
|
52
|
+
* A transaction the SQL left open is rolled back, and the run refused with
|
|
53
|
+
* {@link SqlTransactionLeftOpenError}; a run that failed with one open says
|
|
54
|
+
* that it was rolled back. The session is then reset — role, session
|
|
55
|
+
* authorization, settings — and destroyed instead when it cannot be. Last,
|
|
56
|
+
* `afterReset` reads what it needs on the clean session, as the owner.
|
|
57
|
+
*/
|
|
58
|
+
export declare function onOwnSession<T>(pool: Pool, options: OwnSessionOptions, work: (connection: PoolClient) => Promise<T>, afterReset?: (connection: PoolClient, value: T) => Promise<T>): Promise<T>;
|
|
59
|
+
/**
|
|
60
|
+
* Run `sql` on a connection of its own from `pool` and return what the last
|
|
61
|
+
* statement returned, with each column's provenance and what the database
|
|
62
|
+
* said along the way — `there is no transaction in progress` for a ROLLBACK
|
|
63
|
+
* with nothing to roll back, which used to read as success.
|
|
64
|
+
*/
|
|
65
|
+
export declare function runSqlScriptOnPool(pool: Pool, sql: string, options?: OwnSessionOptions): Promise<SqlScriptResult>;
|
|
66
|
+
/**
|
|
67
|
+
* The same shape for rows that came back parsed, from a handle that is not a
|
|
68
|
+
* pool (PGlite in process): every value as text, and no column with a source —
|
|
69
|
+
* nothing to say where a value came from, so nothing the console will edit.
|
|
70
|
+
*/
|
|
71
|
+
export declare function sqlScriptResultFromRows(rows: Record<string, unknown>[]): SqlScriptResult;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep enough for any chain that ends (a hook writing another collection whose
|
|
3
|
+
* hook writes a third is depth 3), shallow enough that a loop stops at once.
|
|
4
|
+
*/
|
|
5
|
+
export declare const MAX_WRITE_DEPTH = 16;
|
|
6
|
+
/** One write in progress, and which of its hooks is running. */
|
|
7
|
+
export interface WriteFrame {
|
|
8
|
+
readonly path: string;
|
|
9
|
+
readonly operation: "save" | "delete";
|
|
10
|
+
readonly depth: number;
|
|
11
|
+
readonly parent: WriteFrame | undefined;
|
|
12
|
+
/** The hook running right now — what a nested write was started from. */
|
|
13
|
+
stage: string | undefined;
|
|
14
|
+
}
|
|
15
|
+
/** Run one write as a frame, refusing it when the chain is already too deep. */
|
|
16
|
+
export declare function inWriteFrame<T>(path: string, operation: "save" | "delete", fn: (frame: WriteFrame) => Promise<T>): Promise<T>;
|
|
@@ -66,9 +66,12 @@ export declare class WriteTransactionScope implements AmbientTransaction {
|
|
|
66
66
|
*
|
|
67
67
|
* So one statement is asked of the transaction before its commit. On an
|
|
68
68
|
* aborted one it fails with `25P02`, and the write is refused and rolled
|
|
69
|
-
* back instead.
|
|
70
|
-
*
|
|
71
|
-
*
|
|
69
|
+
* back instead. The database refusing a `context.data` create, update or
|
|
70
|
+
* delete is not caught by this: each of those statements runs in a
|
|
71
|
+
* savepoint, is undone on its own, and leaves the transaction usable —
|
|
72
|
+
* which is what makes catching one safe. (Deletes ran without one, so a
|
|
73
|
+
* caught refused delete landed here, under a message saying catching it
|
|
74
|
+
* was safe.)
|
|
72
75
|
*/
|
|
73
76
|
assertCommittable(): Promise<void>;
|
|
74
77
|
/**
|
|
@@ -48,6 +48,18 @@ export type UnknownFilterFieldsMode = "error" | "warn";
|
|
|
48
48
|
* documented input.
|
|
49
49
|
*/
|
|
50
50
|
export declare const escapeLikePattern: (value: string) => string;
|
|
51
|
+
/**
|
|
52
|
+
* A table's column by the name a `joinPath` step gives it — the SQL column — or
|
|
53
|
+
* by its Drizzle property key.
|
|
54
|
+
*
|
|
55
|
+
* A column has two names. A generated schema keys a collection table's columns
|
|
56
|
+
* by property (`orderId: uuid("order_id")`) while a step names the column
|
|
57
|
+
* (`from: "order_id"`), so a lookup by key alone found nothing on a real app,
|
|
58
|
+
* and the hand-written fixtures — keyed by column name — never showed it.
|
|
59
|
+
* Junction tables are generated keyed by column name, which is why a
|
|
60
|
+
* many-to-many was unaffected.
|
|
61
|
+
*/
|
|
62
|
+
export declare function joinColumn(table: PgTable<any>, name: string): AnyPgColumn | undefined;
|
|
51
63
|
/** Set the process-wide behaviour for unresolvable filter fields. */
|
|
52
64
|
export declare function configureUnknownFilterFields(mode: UnknownFilterFieldsMode): void;
|
|
53
65
|
/** The process-wide behaviour for unresolvable filter fields. */
|
|
@@ -171,11 +183,24 @@ export declare class DrizzleConditionBuilder {
|
|
|
171
183
|
* foreign key are expressible from the parent's *id* alone, and
|
|
172
184
|
* requiring the table for them would make a child listing fail on a
|
|
173
185
|
* parent whose table isn't registered.
|
|
186
|
+
*
|
|
187
|
+
* `key` is the parent row's key, every column with its value: those
|
|
188
|
+
* three shapes find the parent row itself, and a lookup on the first
|
|
189
|
+
* column of a composite key finds every row that shares it.
|
|
174
190
|
*/
|
|
175
191
|
parent: () => {
|
|
176
192
|
table: PgTable<any>;
|
|
177
|
-
|
|
178
|
-
|
|
193
|
+
key: {
|
|
194
|
+
column: AnyPgColumn;
|
|
195
|
+
value: string | number;
|
|
196
|
+
}[];
|
|
197
|
+
},
|
|
198
|
+
/**
|
|
199
|
+
* The value a plain foreign key or a junction column holds for this
|
|
200
|
+
* parent. One column, so it reaches a single-column key only —
|
|
201
|
+
* `findRelationDefects` refuses those links into a composite one.
|
|
202
|
+
*/
|
|
203
|
+
parentId: string | number, targetTable: PgTable<any>, targetIdColumn: AnyPgColumn, registry: PostgresCollectionRegistry): SQL;
|
|
179
204
|
/**
|
|
180
205
|
* `EXISTS` for an explicit `joinPath`.
|
|
181
206
|
*
|
|
@@ -663,6 +688,21 @@ export declare class DrizzleConditionBuilder {
|
|
|
663
688
|
/**
|
|
664
689
|
* Helper method to extract table names from columns
|
|
665
690
|
*/
|
|
691
|
+
/**
|
|
692
|
+
* The two tables one step of a join path joins, by the rule `JoinStep`
|
|
693
|
+
* documents: `on.from` is a column of the previous table — the source
|
|
694
|
+
* collection's for the first step — and `on.to` is a column of
|
|
695
|
+
* `step.table`. A `table.column` spelling names its table outright.
|
|
696
|
+
*
|
|
697
|
+
* The tables used to come from that spelling alone, so a step written the
|
|
698
|
+
* documented way, `{ from: "id", to: "product_id" }`, named no table at
|
|
699
|
+
* all: every count and include over such a `via` threw "Join tables not
|
|
700
|
+
* found for step: from to ", and since a record read at a nested address
|
|
701
|
+
* is gated on that count, opening an order from a product's Orders tab
|
|
702
|
+
* failed. The tab's own listing worked — it walks the path forward from
|
|
703
|
+
* the parent, by position — so only the record behind it broke.
|
|
704
|
+
*/
|
|
705
|
+
private static joinStepTableNames;
|
|
666
706
|
static getTableNamesFromColumns(columns: string | string[]): string[];
|
|
667
707
|
/**
|
|
668
708
|
* Helper method to extract column names from columns
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL a person ran, as the audit log may keep it: every string literal masked.
|
|
3
|
+
*
|
|
4
|
+
* The SQL console's audit line wrote the statement verbatim, so `ALTER ROLE app
|
|
5
|
+
* PASSWORD 'hunter2'`, `CREATE USER MAPPING … OPTIONS (password 'hunter2')` and
|
|
6
|
+
* `dblink_connect('host=db password=hunter2')` put the password in the
|
|
7
|
+
* production logs, where the logger's redaction — by key name — never looks.
|
|
8
|
+
*
|
|
9
|
+
* A literal is a value the statement carries inline, which is exactly what the
|
|
10
|
+
* line already declines to write when it is bound as a parameter: the
|
|
11
|
+
* statement's shape is the audit signal, the values are whatever the operator
|
|
12
|
+
* was touching. So every literal goes, not only the ones beside a word that
|
|
13
|
+
* looks like `password` — a secret has no reliable neighbour. Identifiers,
|
|
14
|
+
* keywords, numbers and comments stay.
|
|
15
|
+
*
|
|
16
|
+
* Read with Postgres's own lexical rules, not a regular expression over the
|
|
17
|
+
* text: a quote inside a comment, a doubled quote, an `E'…\'…'` escape or a
|
|
18
|
+
* `$tag$` body must not shift where a literal is thought to end, or the mask
|
|
19
|
+
* covers the statement and leaves the secret.
|
|
20
|
+
*/
|
|
21
|
+
export declare function redactSqlLiterals(sql: string): string;
|