@rebasepro/server-mongo 0.21.0 → 0.21.1
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/README.md +9 -9
- package/dist/index.es.js +126 -15
- package/dist/index.es.js.map +1 -1
- package/dist/services/MongoDriver.d.ts +11 -10
- package/dist/services/MongoRealtimeService.d.ts +34 -2
- package/dist/{websocket-B3LiQfFN.js → websocket-C3iT6srl.js} +100 -38
- package/dist/websocket-C3iT6srl.js.map +1 -0
- package/package.json +5 -5
- package/dist/websocket-B3LiQfFN.js.map +0 -1
package/README.md
CHANGED
|
@@ -9,26 +9,26 @@ pnpm add @rebasepro/server-mongo
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
ESM-only: `"type": "module"` with no CommonJS build, so it is loaded with
|
|
12
|
-
`import`.
|
|
13
|
-
`require(esm)
|
|
12
|
+
`import`. It needs Node `>=22.22.0` (its `engines` floor), where `require()`
|
|
13
|
+
of it resolves too: Node has supported `require(esm)` since 22.12.
|
|
14
14
|
|
|
15
15
|
## What This Package Does
|
|
16
16
|
|
|
17
|
-
Implements the Rebase `BackendBootstrapper` and backend interfaces for MongoDB. Provides a complete data driver, change-stream-based realtime,
|
|
17
|
+
Implements the Rebase `BackendBootstrapper` and backend interfaces for MongoDB. Provides a complete data driver, change-stream-based realtime, entity history, auth repositories, and WebSocket support. Plug it into `@rebasepro/server` via `createMongoBootstrapper()`, or use the standalone `createMongoBackend()` factory for direct access.
|
|
18
18
|
|
|
19
19
|
## Key Exports
|
|
20
20
|
|
|
21
21
|
| Export | Description |
|
|
22
22
|
|--------|-------------|
|
|
23
23
|
| `createMongoBootstrapper(config)` | Creates a `BackendBootstrapper` for use with `initializeRebaseBackend({ bootstrappers: [...] })`. |
|
|
24
|
-
| `createMongoBackend(config)` | Standalone factory — returns a `MongoBackendInstance` with driver,
|
|
24
|
+
| `createMongoBackend(config)` | Standalone factory — returns a `MongoBackendInstance` with driver, data service, realtime, admin, and lifecycle methods. |
|
|
25
25
|
| `createMongoDelegate(db)` | Convenience factory for just the `MongoDriver` (DataDriver). |
|
|
26
26
|
| `createMongoRealtimeService(db)` | Creates a MongoDB change-stream-based realtime provider. |
|
|
27
|
-
| `
|
|
27
|
+
| `createMongoEntityRepository(db)` | Creates a `DataRepository` for direct CRUD. |
|
|
28
28
|
| `createMongoDBConnection(url, dbName)` | Connects to MongoDB and returns a `MongoDBConnection` wrapper. |
|
|
29
29
|
| `MongoDBConnection` | `DatabaseConnection` implementation wrapping `MongoClient` + `Db`. |
|
|
30
30
|
| `MongoDriver` | The `DataDriver` implementation for MongoDB. |
|
|
31
|
-
| `
|
|
31
|
+
| `MongoDataService` | Low-level entity CRUD service (the `DataRepository` implementation). |
|
|
32
32
|
| `MongoRealtimeService` | Change-stream-based `RealtimeProvider`. |
|
|
33
33
|
| `MongoCollectionRegistry` | In-memory collection registry. |
|
|
34
34
|
| `isMongoBackendConfig(config)` | Type guard for `MongoBackendConfig`. |
|
|
@@ -74,11 +74,11 @@ const backend = createMongoBackend({
|
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
// Use directly
|
|
77
|
-
const health = await backend.healthCheck();
|
|
78
|
-
const
|
|
77
|
+
const health = await backend.healthCheck?.();
|
|
78
|
+
const users = await backend.dataService.fetchCollection("users", {});
|
|
79
79
|
|
|
80
80
|
// Cleanup
|
|
81
|
-
await backend.destroy();
|
|
81
|
+
await backend.destroy?.();
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
## Related Packages
|
package/dist/index.es.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MongoClient, ObjectId } from "mongodb";
|
|
2
2
|
import { ANONYMOUS_USER_ID, EntityReference, ListLimitError, getCollectionDataPath, isAnonymousUid, resolveClientListLimit } from "@rebasepro/types";
|
|
3
3
|
import { buildPropertyCallbacks, buildSdkData, callbackRefusal, checkOperation, normalizeDriverOrderBy, normalizeEmail, requireCallbackClient, securityRuleToConditions, toCallbackError, toFilterTuples, updateDateAutoValues } from "@rebasepro/common";
|
|
4
|
-
import { ApiError, logger } from "@rebasepro/server";
|
|
4
|
+
import { ApiError, declaredErrorAnswer, logger } from "@rebasepro/server";
|
|
5
5
|
import { mergeDeep } from "@rebasepro/utils";
|
|
6
6
|
//#region \0rolldown/runtime.js
|
|
7
7
|
var __defProp = Object.defineProperty;
|
|
@@ -511,6 +511,44 @@ var MongoDataService = class {
|
|
|
511
511
|
* Provides real-time subscriptions to collection and row changes.
|
|
512
512
|
*/
|
|
513
513
|
/**
|
|
514
|
+
* The answer a failed fetch chose for its caller, when it chose one.
|
|
515
|
+
*
|
|
516
|
+
* A deliberate 4xx — the server's `ApiError`, or the `RebaseApiError` a
|
|
517
|
+
* collection callback throws because a collection file cannot import the
|
|
518
|
+
* server — was decided about this caller, and its message and code are written
|
|
519
|
+
* for them. It is recognised by `declaredErrorAnswer`, the predicate REST and
|
|
520
|
+
* this socket's request frames use. A declared 5xx is not one: its message is
|
|
521
|
+
* about the server, and the Postgres subscription path masks it too.
|
|
522
|
+
*/
|
|
523
|
+
var refusalOf = (error) => {
|
|
524
|
+
const answer = declaredErrorAnswer(error);
|
|
525
|
+
return answer && answer.status >= 400 && answer.status < 500 ? answer : void 0;
|
|
526
|
+
};
|
|
527
|
+
/**
|
|
528
|
+
* The frame that tells a subscriber its fetch failed, addressed by the
|
|
529
|
+
* subscription id the client keys its listeners on — the shape of the
|
|
530
|
+
* `INVALID_LIMIT` refusal below.
|
|
531
|
+
*
|
|
532
|
+
* A refusal keeps its message, code and details. Anything else is a fault
|
|
533
|
+
* whose text can name internals (hosts, collections, a document's values), so
|
|
534
|
+
* the subscriber reads a generic message, the same words the Postgres path
|
|
535
|
+
* uses, and the full error goes to the log.
|
|
536
|
+
*/
|
|
537
|
+
function subscriptionErrorFrame(subscriptionId, path, error) {
|
|
538
|
+
const refusal = refusalOf(error);
|
|
539
|
+
const message = refusal?.message ?? `Could not load data for "${path ?? ""}". Check server logs for details.`;
|
|
540
|
+
return {
|
|
541
|
+
type: "ERROR",
|
|
542
|
+
subscriptionId,
|
|
543
|
+
payload: { error: {
|
|
544
|
+
message,
|
|
545
|
+
code: refusal?.code ?? "INTERNAL_ERROR",
|
|
546
|
+
...refusal?.details !== void 0 && { details: refusal.details }
|
|
547
|
+
} },
|
|
548
|
+
error: message
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
514
552
|
* The query half of a subscription config — everything except who is watching.
|
|
515
553
|
*
|
|
516
554
|
* Spread into the re-fetch rather than re-listed field by field. Re-listing is
|
|
@@ -564,20 +602,53 @@ var MongoRealtimeService = class {
|
|
|
564
602
|
* fetch behind it is the newest thing known about the row, so it must also
|
|
565
603
|
* be the thing that closes the door on an older fetch still in flight —
|
|
566
604
|
* otherwise the deleted row reappears a moment after it vanished.
|
|
605
|
+
*
|
|
606
|
+
* `mayReportFailure()` is the same check for reporting this delivery's
|
|
607
|
+
* failure, except that it also answers yes to the delivery that already
|
|
608
|
+
* claimed the slot. That is the send itself failing (the socket closure's
|
|
609
|
+
* `JSON.stringify` on a row that will not serialise) after the check passed
|
|
610
|
+
* and before anything reached the subscriber. Same rule as the Postgres
|
|
611
|
+
* service's `beginDelivery`.
|
|
567
612
|
*/
|
|
568
613
|
beginDelivery(subscriptionId, subscription) {
|
|
569
614
|
const seq = ++subscription.started;
|
|
570
|
-
|
|
615
|
+
const canDeliver = () => {
|
|
571
616
|
if (this.subscriptions.get(subscriptionId) !== subscription) return false;
|
|
572
617
|
if (seq <= subscription.delivered) return false;
|
|
573
618
|
subscription.delivered = seq;
|
|
574
619
|
return true;
|
|
575
620
|
};
|
|
621
|
+
const mayReportFailure = () => canDeliver() || this.subscriptions.get(subscriptionId) === subscription && subscription.delivered === seq;
|
|
622
|
+
return Object.assign(canDeliver, { mayReportFailure });
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Tell the subscriber a fetch failed, through the slot the rows would have
|
|
626
|
+
* used.
|
|
627
|
+
*
|
|
628
|
+
* The slot matters as much for an error as for rows. Without it, a fetch
|
|
629
|
+
* that a newer delivery has overtaken would mark a view showing current
|
|
630
|
+
* data as failed, and one whose subscription was cancelled or replaced
|
|
631
|
+
* under the same id would fail a different subscription's view.
|
|
632
|
+
*/
|
|
633
|
+
reportFetchFailure(subscriptionId, subscription, canDeliver, error) {
|
|
634
|
+
const target = subscription.type === "single" ? "row" : "collection";
|
|
635
|
+
const refusal = refusalOf(error);
|
|
636
|
+
if (refusal) {
|
|
637
|
+
const line = `[API ${refusal.status} ${refusal.code}] fetching ${target} for subscription ${subscriptionId}: ${refusal.message}`;
|
|
638
|
+
if (refusal.expected) logger.debug(line);
|
|
639
|
+
else logger.warn(`⚠️ ${line}`);
|
|
640
|
+
} else logger.error(`Error fetching ${target} for subscription ${subscriptionId}`, { error });
|
|
641
|
+
if (!subscription.onError || !canDeliver.mayReportFailure()) return;
|
|
642
|
+
try {
|
|
643
|
+
subscription.onError(error);
|
|
644
|
+
} catch (reportError) {
|
|
645
|
+
logger.error(`Could not report a failed fetch to subscription ${subscriptionId}`, { error: reportError });
|
|
646
|
+
}
|
|
576
647
|
}
|
|
577
648
|
/**
|
|
578
649
|
* Subscribe to collection changes
|
|
579
650
|
*/
|
|
580
|
-
subscribeToCollection(subscriptionId, config, callback) {
|
|
651
|
+
subscribeToCollection(subscriptionId, config, callback, onError) {
|
|
581
652
|
this.unsubscribe(subscriptionId);
|
|
582
653
|
const collectionName = this.getCollectionName(config.path);
|
|
583
654
|
const collection = this.db.collection(collectionName);
|
|
@@ -595,6 +666,7 @@ var MongoRealtimeService = class {
|
|
|
595
666
|
config,
|
|
596
667
|
changeStream,
|
|
597
668
|
callback,
|
|
669
|
+
onError,
|
|
598
670
|
started: 0,
|
|
599
671
|
delivered: 0
|
|
600
672
|
};
|
|
@@ -612,6 +684,7 @@ var MongoRealtimeService = class {
|
|
|
612
684
|
type: "collection",
|
|
613
685
|
config,
|
|
614
686
|
callback,
|
|
687
|
+
onError,
|
|
615
688
|
started: 0,
|
|
616
689
|
delivered: 0
|
|
617
690
|
};
|
|
@@ -635,7 +708,7 @@ var MongoRealtimeService = class {
|
|
|
635
708
|
});
|
|
636
709
|
if (callback && canDeliver()) callback(rows);
|
|
637
710
|
} catch (error) {
|
|
638
|
-
|
|
711
|
+
this.reportFetchFailure(subscriptionId, subscription, canDeliver, error);
|
|
639
712
|
}
|
|
640
713
|
}
|
|
641
714
|
/**
|
|
@@ -648,14 +721,19 @@ var MongoRealtimeService = class {
|
|
|
648
721
|
if (!this.driver) throw new Error("MongoRealtimeService has no data driver — subscriptions cannot be authorized");
|
|
649
722
|
const user = {
|
|
650
723
|
uid: authContext?.uid ?? ANONYMOUS_USER_ID,
|
|
651
|
-
roles: authContext?.roles ?? []
|
|
724
|
+
roles: authContext?.roles ?? [],
|
|
725
|
+
isAnonymous: authContext?.isAnonymous === true,
|
|
726
|
+
displayName: null,
|
|
727
|
+
email: null,
|
|
728
|
+
photoURL: null,
|
|
729
|
+
providerId: "realtime"
|
|
652
730
|
};
|
|
653
731
|
return this.driver.withAuth(user);
|
|
654
732
|
}
|
|
655
733
|
/**
|
|
656
734
|
* Subscribe to single row changes
|
|
657
735
|
*/
|
|
658
|
-
subscribeToOne(subscriptionId, config, callback) {
|
|
736
|
+
subscribeToOne(subscriptionId, config, callback, onError) {
|
|
659
737
|
this.unsubscribe(subscriptionId);
|
|
660
738
|
const collectionName = this.getCollectionName(config.path);
|
|
661
739
|
const collection = this.db.collection(collectionName);
|
|
@@ -675,6 +753,7 @@ var MongoRealtimeService = class {
|
|
|
675
753
|
config,
|
|
676
754
|
changeStream,
|
|
677
755
|
callback,
|
|
756
|
+
onError,
|
|
678
757
|
started: 0,
|
|
679
758
|
delivered: 0
|
|
680
759
|
};
|
|
@@ -695,6 +774,7 @@ var MongoRealtimeService = class {
|
|
|
695
774
|
type: "single",
|
|
696
775
|
config,
|
|
697
776
|
callback,
|
|
777
|
+
onError,
|
|
698
778
|
started: 0,
|
|
699
779
|
delivered: 0
|
|
700
780
|
};
|
|
@@ -718,7 +798,7 @@ var MongoRealtimeService = class {
|
|
|
718
798
|
});
|
|
719
799
|
if (callback && canDeliver()) callback(row || null);
|
|
720
800
|
} catch (error) {
|
|
721
|
-
|
|
801
|
+
this.reportFetchFailure(subscriptionId, subscription, canDeliver, error);
|
|
722
802
|
}
|
|
723
803
|
}
|
|
724
804
|
/**
|
|
@@ -787,7 +867,8 @@ var MongoRealtimeService = class {
|
|
|
787
867
|
if (!ws) return;
|
|
788
868
|
const authContext = _authContext ? {
|
|
789
869
|
uid: _authContext.uid,
|
|
790
|
-
roles: (_authContext.roles ?? []).map(String)
|
|
870
|
+
roles: (_authContext.roles ?? []).map(String),
|
|
871
|
+
isAnonymous: _authContext.isAnonymous === true
|
|
791
872
|
} : void 0;
|
|
792
873
|
switch (message.type) {
|
|
793
874
|
case "subscribe_collection": {
|
|
@@ -829,6 +910,8 @@ var MongoRealtimeService = class {
|
|
|
829
910
|
subscriptionId,
|
|
830
911
|
rows
|
|
831
912
|
}));
|
|
913
|
+
}, (error) => {
|
|
914
|
+
ws.send(JSON.stringify(subscriptionErrorFrame(subscriptionId, message.payload?.path, error)));
|
|
832
915
|
});
|
|
833
916
|
break;
|
|
834
917
|
}
|
|
@@ -846,6 +929,8 @@ var MongoRealtimeService = class {
|
|
|
846
929
|
subscriptionId,
|
|
847
930
|
row
|
|
848
931
|
}));
|
|
932
|
+
}, (error) => {
|
|
933
|
+
ws.send(JSON.stringify(subscriptionErrorFrame(subscriptionId, message.payload?.path, error)));
|
|
849
934
|
});
|
|
850
935
|
break;
|
|
851
936
|
}
|
|
@@ -1239,6 +1324,18 @@ function callContext(driver, user, data, client) {
|
|
|
1239
1324
|
};
|
|
1240
1325
|
}
|
|
1241
1326
|
/**
|
|
1327
|
+
* A listener's `onError`, in the shape the realtime service calls it with.
|
|
1328
|
+
*
|
|
1329
|
+
* Without it a failed fetch behind an in-process subscription was logged, and
|
|
1330
|
+
* the listener heard nothing and kept waiting for rows. The error arrives as
|
|
1331
|
+
* thrown, not masked the way a socket frame is: this is trusted server code,
|
|
1332
|
+
* so an `afterRead` refusal reaches it as the `RebaseApiError` it is.
|
|
1333
|
+
*/
|
|
1334
|
+
function fetchErrorListener(onError) {
|
|
1335
|
+
if (!onError) return void 0;
|
|
1336
|
+
return (error) => onError(error instanceof Error ? error : new Error(String(error)));
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1242
1339
|
* MongoDB DataDriver Delegate
|
|
1243
1340
|
*
|
|
1244
1341
|
* Implements the DataDriver interface for Rebase.
|
|
@@ -1360,7 +1457,7 @@ var MongoDriver = class {
|
|
|
1360
1457
|
clientId: "driver",
|
|
1361
1458
|
...query,
|
|
1362
1459
|
authContext
|
|
1363
|
-
}, callback);
|
|
1460
|
+
}, callback, fetchErrorListener(onError));
|
|
1364
1461
|
return () => {
|
|
1365
1462
|
this.realtimeService.unsubscribe(subscriptionId);
|
|
1366
1463
|
};
|
|
@@ -1414,7 +1511,7 @@ var MongoDriver = class {
|
|
|
1414
1511
|
path,
|
|
1415
1512
|
id,
|
|
1416
1513
|
authContext
|
|
1417
|
-
}, callback);
|
|
1514
|
+
}, callback, fetchErrorListener(onError));
|
|
1418
1515
|
return () => {
|
|
1419
1516
|
this.realtimeService.unsubscribe(subscriptionId);
|
|
1420
1517
|
};
|
|
@@ -1570,9 +1667,15 @@ var MongoDriver = class {
|
|
|
1570
1667
|
*/
|
|
1571
1668
|
async delete({ row, collection }) {
|
|
1572
1669
|
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, row.path);
|
|
1670
|
+
const stored = await this.fetchOne({
|
|
1671
|
+
path: row.path,
|
|
1672
|
+
id: row.id,
|
|
1673
|
+
collection: resolvedCollection
|
|
1674
|
+
});
|
|
1675
|
+
if (!stored) throw ApiError.notFound(`No row "${row.id}" in "${row.path}" to delete.`);
|
|
1573
1676
|
const callbackRow = {
|
|
1574
1677
|
id: row.id,
|
|
1575
|
-
...
|
|
1678
|
+
...stored
|
|
1576
1679
|
};
|
|
1577
1680
|
const contextForCallback = callContext(this, this.user, this.data, this.client);
|
|
1578
1681
|
try {
|
|
@@ -1638,7 +1741,7 @@ var MongoDriver = class {
|
|
|
1638
1741
|
action: "delete",
|
|
1639
1742
|
id: String(row.id),
|
|
1640
1743
|
tableName: row.path,
|
|
1641
|
-
previousValues:
|
|
1744
|
+
previousValues: stored,
|
|
1642
1745
|
updatedBy: this.user?.uid
|
|
1643
1746
|
}).catch((err) => {
|
|
1644
1747
|
logger.error(`Failed to record history for ${row.path}/${row.id}`, { error: err });
|
|
@@ -1760,11 +1863,19 @@ var AuthenticatedMongoDriver = class {
|
|
|
1760
1863
|
listenCollection(props) {
|
|
1761
1864
|
return this.delegate.listenCollection(props, this.authContext());
|
|
1762
1865
|
}
|
|
1763
|
-
/**
|
|
1866
|
+
/**
|
|
1867
|
+
* The acting user, in the shape the realtime subscriptions carry.
|
|
1868
|
+
*
|
|
1869
|
+
* `isAnonymous` included: a guest has a real uid, and without the flag
|
|
1870
|
+
* every fetch a listener's subscription makes reads as an account, so
|
|
1871
|
+
* `policy.registered()` handed a guest's listener what it withholds from a
|
|
1872
|
+
* guest's `fetchCollection` on this same driver.
|
|
1873
|
+
*/
|
|
1764
1874
|
authContext() {
|
|
1765
1875
|
return {
|
|
1766
1876
|
uid: this.user.uid,
|
|
1767
|
-
roles: this.user.roles ?? []
|
|
1877
|
+
roles: this.user.roles ?? [],
|
|
1878
|
+
isAnonymous: this.user.isAnonymous === true
|
|
1768
1879
|
};
|
|
1769
1880
|
}
|
|
1770
1881
|
/**
|
|
@@ -2963,7 +3074,7 @@ function createMongoBootstrapper(mongoConfig) {
|
|
|
2963
3074
|
},
|
|
2964
3075
|
mountRoutes() {},
|
|
2965
3076
|
async initializeWebsockets(server, realtimeService, driver, config, authAdapter) {
|
|
2966
|
-
const { createMongoWebSocket } = await import("./websocket-
|
|
3077
|
+
const { createMongoWebSocket } = await import("./websocket-C3iT6srl.js");
|
|
2967
3078
|
createMongoWebSocket(server, realtimeService, driver, config, cachedAdmin, authAdapter);
|
|
2968
3079
|
}
|
|
2969
3080
|
};
|