@terreno/api 0.14.0 → 0.14.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.
@@ -2140,6 +2140,914 @@ var createMockSocket = function (decodedToken) {
2140
2140
  // ─────────────────────────────────────────────────────────────────────────────
2141
2141
  // redactCredentials — Redis URL logging
2142
2142
  // ─────────────────────────────────────────────────────────────────────────────
2143
+ // ─────────────────────────────────────────────────────────────────────────────
2144
+ // ensureApiId
2145
+ // ─────────────────────────────────────────────────────────────────────────────
2146
+ (0, bun_test_1.describe)("ensureApiId", function () {
2147
+ (0, bun_test_1.it)("returns null as-is", function () {
2148
+ (0, bun_test_1.expect)((0, changeStreamWatcher_1.ensureApiId)(null)).toBeNull();
2149
+ });
2150
+ (0, bun_test_1.it)("returns undefined as-is", function () {
2151
+ (0, bun_test_1.expect)((0, changeStreamWatcher_1.ensureApiId)(undefined)).toBeUndefined();
2152
+ });
2153
+ (0, bun_test_1.it)("returns arrays as-is", function () {
2154
+ var arr = [1, 2, 3];
2155
+ (0, bun_test_1.expect)((0, changeStreamWatcher_1.ensureApiId)(arr)).toBe(arr);
2156
+ });
2157
+ (0, bun_test_1.it)("returns primitive values as-is (non-object)", function () {
2158
+ (0, bun_test_1.expect)((0, changeStreamWatcher_1.ensureApiId)("string")).toBe("string");
2159
+ });
2160
+ (0, bun_test_1.it)("adds id from _id when id is missing", function () {
2161
+ (0, bun_test_1.expect)((0, changeStreamWatcher_1.ensureApiId)({ _id: "abc" })).toEqual({ _id: "abc", id: "abc" });
2162
+ });
2163
+ (0, bun_test_1.it)("does not overwrite existing id", function () {
2164
+ (0, bun_test_1.expect)((0, changeStreamWatcher_1.ensureApiId)({ _id: "abc", id: "existing" })).toEqual({ _id: "abc", id: "existing" });
2165
+ });
2166
+ (0, bun_test_1.it)("returns object without _id unchanged", function () {
2167
+ var obj = { name: "test" };
2168
+ (0, bun_test_1.expect)((0, changeStreamWatcher_1.ensureApiId)(obj)).toBe(obj);
2169
+ });
2170
+ });
2171
+ // ─────────────────────────────────────────────────────────────────────────────
2172
+ // startChangeStreamWatcher & stopChangeStreamWatcher
2173
+ // ─────────────────────────────────────────────────────────────────────────────
2174
+ (0, bun_test_1.describe)("startChangeStreamWatcher & stopChangeStreamWatcher", function () {
2175
+ var makeMockIo = function () {
2176
+ var emissions = [];
2177
+ var rooms = new Map();
2178
+ var sockets = new Map();
2179
+ return {
2180
+ emissions: emissions,
2181
+ sockets: {
2182
+ adapter: { rooms: rooms },
2183
+ sockets: sockets,
2184
+ },
2185
+ to: function (_room) { return ({
2186
+ emit: function () { },
2187
+ }); },
2188
+ };
2189
+ };
2190
+ (0, bun_test_1.afterEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
2191
+ return __generator(this, function (_a) {
2192
+ switch (_a.label) {
2193
+ case 0: return [4 /*yield*/, (0, changeStreamWatcher_1.stopChangeStreamWatcher)()];
2194
+ case 1:
2195
+ _a.sent();
2196
+ (0, registry_1.clearRealtimeRegistry)();
2197
+ return [2 /*return*/];
2198
+ }
2199
+ });
2200
+ }); });
2201
+ (0, bun_test_1.it)("starts and stops without error when MongoDB is connected", function () { return __awaiter(void 0, void 0, void 0, function () {
2202
+ var io;
2203
+ return __generator(this, function (_a) {
2204
+ switch (_a.label) {
2205
+ case 0:
2206
+ io = makeMockIo();
2207
+ (0, bun_test_1.expect)(function () { return (0, changeStreamWatcher_1.startChangeStreamWatcher)(io, {}, false); }).not.toThrow();
2208
+ return [4 /*yield*/, (0, changeStreamWatcher_1.stopChangeStreamWatcher)()];
2209
+ case 1:
2210
+ _a.sent();
2211
+ return [2 /*return*/];
2212
+ }
2213
+ });
2214
+ }); });
2215
+ (0, bun_test_1.it)("starts with debug mode enabled", function () { return __awaiter(void 0, void 0, void 0, function () {
2216
+ var io;
2217
+ return __generator(this, function (_a) {
2218
+ switch (_a.label) {
2219
+ case 0:
2220
+ io = makeMockIo();
2221
+ (0, bun_test_1.expect)(function () { return (0, changeStreamWatcher_1.startChangeStreamWatcher)(io, {}, true); }).not.toThrow();
2222
+ return [4 /*yield*/, (0, changeStreamWatcher_1.stopChangeStreamWatcher)()];
2223
+ case 1:
2224
+ _a.sent();
2225
+ return [2 /*return*/];
2226
+ }
2227
+ });
2228
+ }); });
2229
+ (0, bun_test_1.it)("starts with custom config options", function () { return __awaiter(void 0, void 0, void 0, function () {
2230
+ var io;
2231
+ return __generator(this, function (_a) {
2232
+ switch (_a.label) {
2233
+ case 0:
2234
+ io = makeMockIo();
2235
+ (0, bun_test_1.expect)(function () {
2236
+ return (0, changeStreamWatcher_1.startChangeStreamWatcher)(io, {
2237
+ batchSize: 10,
2238
+ fullDocument: "whenAvailable",
2239
+ ignoredCollections: ["logs"],
2240
+ ignoredOperations: ["delete"],
2241
+ }, false);
2242
+ }).not.toThrow();
2243
+ return [4 /*yield*/, (0, changeStreamWatcher_1.stopChangeStreamWatcher)()];
2244
+ case 1:
2245
+ _a.sent();
2246
+ return [2 /*return*/];
2247
+ }
2248
+ });
2249
+ }); });
2250
+ (0, bun_test_1.it)("stopChangeStreamWatcher is safe to call when no watcher is active", function () { return __awaiter(void 0, void 0, void 0, function () {
2251
+ return __generator(this, function (_a) {
2252
+ switch (_a.label) {
2253
+ case 0: return [4 /*yield*/, (0, bun_test_1.expect)((0, changeStreamWatcher_1.stopChangeStreamWatcher)()).resolves.toBeUndefined()];
2254
+ case 1:
2255
+ _a.sent();
2256
+ return [2 /*return*/];
2257
+ }
2258
+ });
2259
+ }); });
2260
+ (0, bun_test_1.it)("stopChangeStreamWatcher can be called multiple times", function () { return __awaiter(void 0, void 0, void 0, function () {
2261
+ var io;
2262
+ return __generator(this, function (_a) {
2263
+ switch (_a.label) {
2264
+ case 0:
2265
+ io = makeMockIo();
2266
+ (0, changeStreamWatcher_1.startChangeStreamWatcher)(io, {}, false);
2267
+ return [4 /*yield*/, (0, changeStreamWatcher_1.stopChangeStreamWatcher)()];
2268
+ case 1:
2269
+ _a.sent();
2270
+ return [4 /*yield*/, (0, changeStreamWatcher_1.stopChangeStreamWatcher)()];
2271
+ case 2:
2272
+ _a.sent();
2273
+ return [2 /*return*/];
2274
+ }
2275
+ });
2276
+ }); });
2277
+ });
2278
+ // Change streams require a MongoDB replica set. CI (api-ci.yml) runs standalone MongoDB,
2279
+ // so these tests are skipped when replica sets are not available.
2280
+ var hasReplicaSet = function () { return __awaiter(void 0, void 0, void 0, function () {
2281
+ var mongoose, admin, status_1, _a;
2282
+ return __generator(this, function (_b) {
2283
+ switch (_b.label) {
2284
+ case 0:
2285
+ _b.trys.push([0, 2, , 3]);
2286
+ mongoose = require("mongoose");
2287
+ admin = mongoose.connection.db.admin();
2288
+ return [4 /*yield*/, admin.command({ replSetGetStatus: 1 })];
2289
+ case 1:
2290
+ status_1 = _b.sent();
2291
+ return [2 /*return*/, !!status_1.ok];
2292
+ case 2:
2293
+ _a = _b.sent();
2294
+ return [2 /*return*/, false];
2295
+ case 3: return [2 /*return*/];
2296
+ }
2297
+ });
2298
+ }); };
2299
+ (0, bun_test_1.describe)("startChangeStreamWatcher — change event integration", function () {
2300
+ var mongoose = require("mongoose");
2301
+ var replicaSetAvailable = false;
2302
+ var realtimeTestSchema = new mongoose.Schema({
2303
+ deleted: { default: false, type: Boolean },
2304
+ name: { type: String },
2305
+ ownerId: { type: String },
2306
+ }, { collection: "realtimetests", strict: "throw" });
2307
+ var RealtimeTestModel;
2308
+ try {
2309
+ RealtimeTestModel = mongoose.model("RealtimeTest");
2310
+ }
2311
+ catch (_a) {
2312
+ RealtimeTestModel = mongoose.model("RealtimeTest", realtimeTestSchema);
2313
+ }
2314
+ var makeTrackedIo = function () {
2315
+ var emissions = [];
2316
+ var rooms = new Map();
2317
+ var sockets = new Map();
2318
+ var addSocketToRoom = function (room, decodedToken) {
2319
+ var _a;
2320
+ if (decodedToken === void 0) { decodedToken = { admin: true, id: "admin" }; }
2321
+ var socketId = "socket-".concat(Math.random().toString(36).slice(2, 9));
2322
+ if (!rooms.has(room)) {
2323
+ rooms.set(room, new Set());
2324
+ }
2325
+ (_a = rooms.get(room)) === null || _a === void 0 ? void 0 : _a.add(socketId);
2326
+ sockets.set(socketId, {
2327
+ decodedToken: decodedToken,
2328
+ emit: function (event, payload) {
2329
+ emissions.push({ event: event, payload: payload, room: room, socketId: socketId });
2330
+ },
2331
+ id: socketId,
2332
+ });
2333
+ };
2334
+ return {
2335
+ addSocketToRoom: addSocketToRoom,
2336
+ emissions: emissions,
2337
+ sockets: {
2338
+ adapter: { rooms: rooms },
2339
+ sockets: sockets,
2340
+ },
2341
+ to: function (room) { return ({
2342
+ emit: function (event, payload) {
2343
+ emissions.push({ event: event, payload: payload, room: room });
2344
+ },
2345
+ }); },
2346
+ };
2347
+ };
2348
+ (0, bun_test_1.beforeAll)(function () { return __awaiter(void 0, void 0, void 0, function () {
2349
+ return __generator(this, function (_a) {
2350
+ switch (_a.label) {
2351
+ case 0: return [4 /*yield*/, hasReplicaSet()];
2352
+ case 1:
2353
+ replicaSetAvailable = _a.sent();
2354
+ return [2 /*return*/];
2355
+ }
2356
+ });
2357
+ }); });
2358
+ (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
2359
+ return __generator(this, function (_a) {
2360
+ switch (_a.label) {
2361
+ case 0:
2362
+ (0, registry_1.clearRealtimeRegistry)();
2363
+ (0, queryStore_1.clearQueryStore)();
2364
+ return [4 /*yield*/, RealtimeTestModel.deleteMany({})];
2365
+ case 1:
2366
+ _a.sent();
2367
+ return [2 /*return*/];
2368
+ }
2369
+ });
2370
+ }); });
2371
+ (0, bun_test_1.afterEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
2372
+ return __generator(this, function (_a) {
2373
+ switch (_a.label) {
2374
+ case 0: return [4 /*yield*/, (0, changeStreamWatcher_1.stopChangeStreamWatcher)()];
2375
+ case 1:
2376
+ _a.sent();
2377
+ (0, registry_1.clearRealtimeRegistry)();
2378
+ (0, queryStore_1.clearQueryStore)();
2379
+ return [4 /*yield*/, RealtimeTestModel.deleteMany({})];
2380
+ case 2:
2381
+ _a.sent();
2382
+ return [2 /*return*/];
2383
+ }
2384
+ });
2385
+ }); });
2386
+ (0, bun_test_1.it)("processes insert events from MongoDB change stream", function () { return __awaiter(void 0, void 0, void 0, function () {
2387
+ var io, createEmissions;
2388
+ return __generator(this, function (_a) {
2389
+ switch (_a.label) {
2390
+ case 0:
2391
+ if (!replicaSetAvailable) {
2392
+ return [2 /*return*/];
2393
+ }
2394
+ (0, registry_1.registerRealtime)({
2395
+ collectionName: "realtimetests",
2396
+ config: { methods: ["create", "update", "delete"], roomStrategy: "model" },
2397
+ modelName: "RealtimeTest",
2398
+ options: {
2399
+ permissions: {
2400
+ create: [function () { return true; }],
2401
+ delete: [function () { return true; }],
2402
+ list: [function () { return true; }],
2403
+ read: [function () { return true; }],
2404
+ update: [function () { return true; }],
2405
+ },
2406
+ },
2407
+ routePath: "/realtimetests",
2408
+ });
2409
+ io = makeTrackedIo();
2410
+ io.addSocketToRoom("model:realtimetests");
2411
+ (0, changeStreamWatcher_1.startChangeStreamWatcher)(io, {}, true);
2412
+ return [4 /*yield*/, RealtimeTestModel.create({ name: "test-item", ownerId: "user-1" })];
2413
+ case 1:
2414
+ _a.sent();
2415
+ // Wait for the change stream event to be processed
2416
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 1500); })];
2417
+ case 2:
2418
+ // Wait for the change stream event to be processed
2419
+ _a.sent();
2420
+ createEmissions = io.emissions.filter(function (e) { var _a; return e.event === "sync" && ((_a = e.payload) === null || _a === void 0 ? void 0 : _a.method) === "create"; });
2421
+ (0, bun_test_1.expect)(createEmissions.length).toBeGreaterThanOrEqual(1);
2422
+ return [4 /*yield*/, (0, changeStreamWatcher_1.stopChangeStreamWatcher)()];
2423
+ case 3:
2424
+ _a.sent();
2425
+ return [2 /*return*/];
2426
+ }
2427
+ });
2428
+ }); });
2429
+ (0, bun_test_1.it)("processes update events from MongoDB change stream", function () { return __awaiter(void 0, void 0, void 0, function () {
2430
+ var doc, io, updateEmissions;
2431
+ return __generator(this, function (_a) {
2432
+ switch (_a.label) {
2433
+ case 0:
2434
+ if (!replicaSetAvailable) {
2435
+ return [2 /*return*/];
2436
+ }
2437
+ (0, registry_1.registerRealtime)({
2438
+ collectionName: "realtimetests",
2439
+ config: { methods: ["create", "update", "delete"], roomStrategy: "model" },
2440
+ modelName: "RealtimeTest",
2441
+ options: {
2442
+ permissions: {
2443
+ create: [function () { return true; }],
2444
+ delete: [function () { return true; }],
2445
+ list: [function () { return true; }],
2446
+ read: [function () { return true; }],
2447
+ update: [function () { return true; }],
2448
+ },
2449
+ },
2450
+ routePath: "/realtimetests",
2451
+ });
2452
+ return [4 /*yield*/, RealtimeTestModel.create({ name: "item-to-update", ownerId: "user-1" })];
2453
+ case 1:
2454
+ doc = _a.sent();
2455
+ io = makeTrackedIo();
2456
+ io.addSocketToRoom("model:realtimetests");
2457
+ (0, changeStreamWatcher_1.startChangeStreamWatcher)(io, {}, true);
2458
+ return [4 /*yield*/, RealtimeTestModel.updateOne({ _id: doc._id }, { $set: { name: "updated-item" } })];
2459
+ case 2:
2460
+ _a.sent();
2461
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 1500); })];
2462
+ case 3:
2463
+ _a.sent();
2464
+ updateEmissions = io.emissions.filter(function (e) { var _a; return e.event === "sync" && ((_a = e.payload) === null || _a === void 0 ? void 0 : _a.method) === "update"; });
2465
+ (0, bun_test_1.expect)(updateEmissions.length).toBeGreaterThanOrEqual(1);
2466
+ return [4 /*yield*/, (0, changeStreamWatcher_1.stopChangeStreamWatcher)()];
2467
+ case 4:
2468
+ _a.sent();
2469
+ return [2 /*return*/];
2470
+ }
2471
+ });
2472
+ }); });
2473
+ (0, bun_test_1.it)("processes hard delete events from MongoDB change stream", function () { return __awaiter(void 0, void 0, void 0, function () {
2474
+ var doc, io, deleteEmissions;
2475
+ return __generator(this, function (_a) {
2476
+ switch (_a.label) {
2477
+ case 0:
2478
+ if (!replicaSetAvailable) {
2479
+ return [2 /*return*/];
2480
+ }
2481
+ (0, registry_1.registerRealtime)({
2482
+ collectionName: "realtimetests",
2483
+ config: { methods: ["create", "update", "delete"], roomStrategy: "model" },
2484
+ modelName: "RealtimeTest",
2485
+ options: {
2486
+ permissions: {
2487
+ create: [function () { return true; }],
2488
+ delete: [function () { return true; }],
2489
+ list: [function () { return true; }],
2490
+ read: [function () { return true; }],
2491
+ update: [function () { return true; }],
2492
+ },
2493
+ },
2494
+ routePath: "/realtimetests",
2495
+ });
2496
+ return [4 /*yield*/, RealtimeTestModel.create({ name: "item-to-delete" })];
2497
+ case 1:
2498
+ doc = _a.sent();
2499
+ io = makeTrackedIo();
2500
+ io.addSocketToRoom("model:realtimetests");
2501
+ (0, changeStreamWatcher_1.startChangeStreamWatcher)(io, {}, true);
2502
+ return [4 /*yield*/, RealtimeTestModel.deleteOne({ _id: doc._id })];
2503
+ case 2:
2504
+ _a.sent();
2505
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 1500); })];
2506
+ case 3:
2507
+ _a.sent();
2508
+ deleteEmissions = io.emissions.filter(function (e) { var _a; return e.event === "sync" && ((_a = e.payload) === null || _a === void 0 ? void 0 : _a.method) === "delete"; });
2509
+ (0, bun_test_1.expect)(deleteEmissions.length).toBeGreaterThanOrEqual(1);
2510
+ return [4 /*yield*/, (0, changeStreamWatcher_1.stopChangeStreamWatcher)()];
2511
+ case 4:
2512
+ _a.sent();
2513
+ return [2 /*return*/];
2514
+ }
2515
+ });
2516
+ }); });
2517
+ (0, bun_test_1.it)("processes soft delete events from MongoDB change stream", function () { return __awaiter(void 0, void 0, void 0, function () {
2518
+ var doc, io, deleteEmissions;
2519
+ return __generator(this, function (_a) {
2520
+ switch (_a.label) {
2521
+ case 0:
2522
+ if (!replicaSetAvailable) {
2523
+ return [2 /*return*/];
2524
+ }
2525
+ (0, registry_1.registerRealtime)({
2526
+ collectionName: "realtimetests",
2527
+ config: { methods: ["create", "update", "delete"], roomStrategy: "model" },
2528
+ modelName: "RealtimeTest",
2529
+ options: {
2530
+ permissions: {
2531
+ create: [function () { return true; }],
2532
+ delete: [function () { return true; }],
2533
+ list: [function () { return true; }],
2534
+ read: [function () { return true; }],
2535
+ update: [function () { return true; }],
2536
+ },
2537
+ },
2538
+ routePath: "/realtimetests",
2539
+ });
2540
+ return [4 /*yield*/, RealtimeTestModel.create({ name: "item-to-soft-delete" })];
2541
+ case 1:
2542
+ doc = _a.sent();
2543
+ io = makeTrackedIo();
2544
+ io.addSocketToRoom("model:realtimetests");
2545
+ (0, changeStreamWatcher_1.startChangeStreamWatcher)(io, {}, true);
2546
+ return [4 /*yield*/, RealtimeTestModel.updateOne({ _id: doc._id }, { $set: { deleted: true } })];
2547
+ case 2:
2548
+ _a.sent();
2549
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 1500); })];
2550
+ case 3:
2551
+ _a.sent();
2552
+ deleteEmissions = io.emissions.filter(function (e) { var _a; return e.event === "sync" && ((_a = e.payload) === null || _a === void 0 ? void 0 : _a.method) === "delete"; });
2553
+ (0, bun_test_1.expect)(deleteEmissions.length).toBeGreaterThanOrEqual(1);
2554
+ return [4 /*yield*/, (0, changeStreamWatcher_1.stopChangeStreamWatcher)()];
2555
+ case 4:
2556
+ _a.sent();
2557
+ return [2 /*return*/];
2558
+ }
2559
+ });
2560
+ }); });
2561
+ (0, bun_test_1.it)("includes updatedFields and emits to document rooms", function () { return __awaiter(void 0, void 0, void 0, function () {
2562
+ var doc, docId, io, updateEmissions;
2563
+ return __generator(this, function (_a) {
2564
+ switch (_a.label) {
2565
+ case 0:
2566
+ if (!replicaSetAvailable) {
2567
+ return [2 /*return*/];
2568
+ }
2569
+ (0, registry_1.registerRealtime)({
2570
+ collectionName: "realtimetests",
2571
+ config: { methods: ["create", "update", "delete"], roomStrategy: "model" },
2572
+ modelName: "RealtimeTest",
2573
+ options: {
2574
+ permissions: {
2575
+ create: [function () { return true; }],
2576
+ delete: [function () { return true; }],
2577
+ list: [function () { return true; }],
2578
+ read: [function () { return true; }],
2579
+ update: [function () { return true; }],
2580
+ },
2581
+ },
2582
+ routePath: "/realtimetests",
2583
+ });
2584
+ return [4 /*yield*/, RealtimeTestModel.create({ name: "fields-test" })];
2585
+ case 1:
2586
+ doc = _a.sent();
2587
+ docId = doc._id.toString();
2588
+ io = makeTrackedIo();
2589
+ io.addSocketToRoom("model:realtimetests");
2590
+ io.addSocketToRoom("document:realtimetests:".concat(docId));
2591
+ (0, changeStreamWatcher_1.startChangeStreamWatcher)(io, {}, true);
2592
+ return [4 /*yield*/, RealtimeTestModel.updateOne({ _id: doc._id }, { $set: { name: "fields-updated" } })];
2593
+ case 2:
2594
+ _a.sent();
2595
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 1500); })];
2596
+ case 3:
2597
+ _a.sent();
2598
+ updateEmissions = io.emissions.filter(function (e) { var _a; return e.event === "sync" && ((_a = e.payload) === null || _a === void 0 ? void 0 : _a.method) === "update"; });
2599
+ (0, bun_test_1.expect)(updateEmissions.length).toBeGreaterThanOrEqual(1);
2600
+ if (updateEmissions.length > 0) {
2601
+ (0, bun_test_1.expect)(updateEmissions[0].payload.updatedFields).toBeDefined();
2602
+ (0, bun_test_1.expect)(updateEmissions[0].payload.updatedFields).toContain("name");
2603
+ }
2604
+ return [4 /*yield*/, (0, changeStreamWatcher_1.stopChangeStreamWatcher)()];
2605
+ case 4:
2606
+ _a.sent();
2607
+ return [2 /*return*/];
2608
+ }
2609
+ });
2610
+ }); });
2611
+ });
2612
+ // ─────────────────────────────────────────────────────────────────────────────
2613
+ // emitToDocumentAndQueryRooms — no-entry path
2614
+ // ─────────────────────────────────────────────────────────────────────────────
2615
+ (0, bun_test_1.describe)("emitToDocumentAndQueryRooms — no registry entry", function () {
2616
+ var makeIoSimple = function () {
2617
+ var emissions = [];
2618
+ return {
2619
+ emissions: emissions,
2620
+ sockets: {
2621
+ adapter: { rooms: new Map() },
2622
+ sockets: new Map(),
2623
+ },
2624
+ to: function (room) { return ({
2625
+ emit: function (event, payload) {
2626
+ emissions.push({ event: event, payload: payload, room: room });
2627
+ },
2628
+ }); },
2629
+ };
2630
+ };
2631
+ (0, bun_test_1.beforeEach)(function () {
2632
+ (0, queryStore_1.clearQueryStore)();
2633
+ });
2634
+ (0, bun_test_1.afterEach)(function () {
2635
+ (0, queryStore_1.clearQueryStore)();
2636
+ });
2637
+ (0, bun_test_1.it)("emits to document room via io.to when no entry is provided", function () { return __awaiter(void 0, void 0, void 0, function () {
2638
+ var io, event;
2639
+ return __generator(this, function (_a) {
2640
+ switch (_a.label) {
2641
+ case 0:
2642
+ io = makeIoSimple();
2643
+ event = {
2644
+ collection: "items",
2645
+ id: "doc-1",
2646
+ method: "update",
2647
+ model: "Item",
2648
+ timestamp: 1,
2649
+ };
2650
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToDocumentAndQueryRooms)(io, "items", event, {}, function () { })];
2651
+ case 1:
2652
+ _a.sent();
2653
+ (0, bun_test_1.expect)(io.emissions.some(function (e) { return e.room === "document:items:doc-1"; })).toBe(true);
2654
+ return [2 /*return*/];
2655
+ }
2656
+ });
2657
+ }); });
2658
+ (0, bun_test_1.it)("emits hard deletes to query rooms via io.to when no entry", function () { return __awaiter(void 0, void 0, void 0, function () {
2659
+ var queryId, io, event;
2660
+ return __generator(this, function (_a) {
2661
+ switch (_a.label) {
2662
+ case 0:
2663
+ queryId = (0, queryStore_1.computeQueryId)("items", { status: "active" });
2664
+ (0, queryStore_1.addQuerySubscription)("socket-a", "items", { status: "active" }, queryId);
2665
+ io = makeIoSimple();
2666
+ event = {
2667
+ collection: "items",
2668
+ id: "doc-1",
2669
+ method: "delete",
2670
+ model: "Item",
2671
+ timestamp: 1,
2672
+ };
2673
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToDocumentAndQueryRooms)(io, "items", event, undefined, function () { })];
2674
+ case 1:
2675
+ _a.sent();
2676
+ (0, bun_test_1.expect)(io.emissions.some(function (e) { return e.room === "query:".concat(queryId); })).toBe(true);
2677
+ return [2 /*return*/];
2678
+ }
2679
+ });
2680
+ }); });
2681
+ (0, bun_test_1.it)("emits soft delete to query rooms via io.to when no entry and doc matches", function () { return __awaiter(void 0, void 0, void 0, function () {
2682
+ var queryId, io, event;
2683
+ return __generator(this, function (_a) {
2684
+ switch (_a.label) {
2685
+ case 0:
2686
+ queryId = (0, queryStore_1.computeQueryId)("items", { status: "active" });
2687
+ (0, queryStore_1.addQuerySubscription)("socket-a", "items", { status: "active" }, queryId);
2688
+ io = makeIoSimple();
2689
+ event = {
2690
+ collection: "items",
2691
+ id: "doc-1",
2692
+ method: "delete",
2693
+ model: "Item",
2694
+ timestamp: 1,
2695
+ };
2696
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToDocumentAndQueryRooms)(io, "items", event, { status: "active" }, function () { })];
2697
+ case 1:
2698
+ _a.sent();
2699
+ (0, bun_test_1.expect)(io.emissions.some(function (e) { return e.room === "query:".concat(queryId); })).toBe(true);
2700
+ return [2 /*return*/];
2701
+ }
2702
+ });
2703
+ }); });
2704
+ (0, bun_test_1.it)("emits create events to query rooms via io.to when no entry and doc matches", function () { return __awaiter(void 0, void 0, void 0, function () {
2705
+ var queryId, io, event;
2706
+ return __generator(this, function (_a) {
2707
+ switch (_a.label) {
2708
+ case 0:
2709
+ queryId = (0, queryStore_1.computeQueryId)("items", { status: "active" });
2710
+ (0, queryStore_1.addQuerySubscription)("socket-a", "items", { status: "active" }, queryId);
2711
+ io = makeIoSimple();
2712
+ event = {
2713
+ collection: "items",
2714
+ id: "doc-1",
2715
+ method: "create",
2716
+ model: "Item",
2717
+ timestamp: 1,
2718
+ };
2719
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToDocumentAndQueryRooms)(io, "items", event, { status: "active" }, function () { })];
2720
+ case 1:
2721
+ _a.sent();
2722
+ (0, bun_test_1.expect)(io.emissions.some(function (e) { return e.room === "query:".concat(queryId); })).toBe(true);
2723
+ return [2 /*return*/];
2724
+ }
2725
+ });
2726
+ }); });
2727
+ (0, bun_test_1.it)("emits update events to query rooms via io.to when no entry and doc matches", function () { return __awaiter(void 0, void 0, void 0, function () {
2728
+ var queryId, io, event;
2729
+ return __generator(this, function (_a) {
2730
+ switch (_a.label) {
2731
+ case 0:
2732
+ queryId = (0, queryStore_1.computeQueryId)("items", { status: "active" });
2733
+ (0, queryStore_1.addQuerySubscription)("socket-a", "items", { status: "active" }, queryId);
2734
+ io = makeIoSimple();
2735
+ event = {
2736
+ collection: "items",
2737
+ id: "doc-1",
2738
+ method: "update",
2739
+ model: "Item",
2740
+ timestamp: 1,
2741
+ };
2742
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToDocumentAndQueryRooms)(io, "items", event, { status: "active" }, function () { })];
2743
+ case 1:
2744
+ _a.sent();
2745
+ (0, bun_test_1.expect)(io.emissions.some(function (e) { return e.room === "query:".concat(queryId); })).toBe(true);
2746
+ return [2 /*return*/];
2747
+ }
2748
+ });
2749
+ }); });
2750
+ (0, bun_test_1.it)("emits delete to query rooms via io.to when update no longer matches and no entry", function () { return __awaiter(void 0, void 0, void 0, function () {
2751
+ var queryId, io, event, queryEmissions;
2752
+ return __generator(this, function (_a) {
2753
+ switch (_a.label) {
2754
+ case 0:
2755
+ queryId = (0, queryStore_1.computeQueryId)("items", { status: "active" });
2756
+ (0, queryStore_1.addQuerySubscription)("socket-a", "items", { status: "active" }, queryId);
2757
+ io = makeIoSimple();
2758
+ event = {
2759
+ collection: "items",
2760
+ id: "doc-1",
2761
+ method: "update",
2762
+ model: "Item",
2763
+ timestamp: 1,
2764
+ };
2765
+ return [4 /*yield*/, (0, changeStreamWatcher_1.emitToDocumentAndQueryRooms)(io, "items", event, { status: "inactive" }, function () { })];
2766
+ case 1:
2767
+ _a.sent();
2768
+ queryEmissions = io.emissions.filter(function (e) { return e.room === "query:".concat(queryId); });
2769
+ (0, bun_test_1.expect)(queryEmissions.length).toBe(1);
2770
+ (0, bun_test_1.expect)(queryEmissions[0].payload).toMatchObject({ method: "delete" });
2771
+ return [2 /*return*/];
2772
+ }
2773
+ });
2774
+ }); });
2775
+ });
2776
+ // ─────────────────────────────────────────────────────────────────────────────
2777
+ // RealtimeApp — onServerCreated, setupAdapter, close
2778
+ // ─────────────────────────────────────────────────────────────────────────────
2779
+ (0, bun_test_1.describe)("RealtimeApp — onServerCreated and setupAdapter", function () {
2780
+ var originalEnv = process.env;
2781
+ (0, bun_test_1.beforeEach)(function () {
2782
+ process.env = __assign(__assign({}, originalEnv), { TOKEN_SECRET: "test-secret" });
2783
+ });
2784
+ (0, bun_test_1.afterEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
2785
+ return __generator(this, function (_a) {
2786
+ process.env = originalEnv;
2787
+ (0, registry_1.clearRealtimeRegistry)();
2788
+ return [2 /*return*/];
2789
+ });
2790
+ }); });
2791
+ (0, bun_test_1.it)("register adds /realtime/health endpoint with debug flag", function () { return __awaiter(void 0, void 0, void 0, function () {
2792
+ var expressApp, app, supertest, st, res;
2793
+ return __generator(this, function (_a) {
2794
+ switch (_a.label) {
2795
+ case 0:
2796
+ expressApp = (0, express_1.default)();
2797
+ app = new realtimeApp_1.RealtimeApp({ debug: true });
2798
+ app.register(expressApp);
2799
+ return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require("supertest")); })];
2800
+ case 1:
2801
+ supertest = _a.sent();
2802
+ st = supertest.default(expressApp);
2803
+ return [4 /*yield*/, st.get("/realtime/health").expect(200)];
2804
+ case 2:
2805
+ res = _a.sent();
2806
+ (0, bun_test_1.expect)(res.body.status).toBe("not_started");
2807
+ (0, bun_test_1.expect)(res.body.debug).toBe(true);
2808
+ (0, bun_test_1.expect)(res.body.clients).toBe(0);
2809
+ return [2 /*return*/];
2810
+ }
2811
+ });
2812
+ }); });
2813
+ (0, bun_test_1.it)("onServerCreated sets up Socket.io with JWT auth", function () { return __awaiter(void 0, void 0, void 0, function () {
2814
+ var http, app, expressApp, server;
2815
+ return __generator(this, function (_a) {
2816
+ switch (_a.label) {
2817
+ case 0: return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require("node:http")); })];
2818
+ case 1:
2819
+ http = _a.sent();
2820
+ app = new realtimeApp_1.RealtimeApp({ debug: true, tokenSecret: "test-secret" });
2821
+ expressApp = (0, express_1.default)();
2822
+ app.register(expressApp);
2823
+ server = http.createServer(expressApp);
2824
+ app.onServerCreated(server);
2825
+ (0, bun_test_1.expect)(app.getIo()).not.toBeNull();
2826
+ return [4 /*yield*/, app.close()];
2827
+ case 2:
2828
+ _a.sent();
2829
+ server.close();
2830
+ return [2 /*return*/];
2831
+ }
2832
+ });
2833
+ }); });
2834
+ (0, bun_test_1.it)("onServerCreated throws when TOKEN_SECRET is missing", function () {
2835
+ var http = require("node:http");
2836
+ var origSecret = process.env.TOKEN_SECRET;
2837
+ process.env.TOKEN_SECRET = "";
2838
+ var app = new realtimeApp_1.RealtimeApp({});
2839
+ var expressApp = (0, express_1.default)();
2840
+ app.register(expressApp);
2841
+ var server = http.createServer(expressApp);
2842
+ (0, bun_test_1.expect)(function () { return app.onServerCreated(server); }).toThrow("TOKEN_SECRET is required");
2843
+ process.env.TOKEN_SECRET = origSecret;
2844
+ server.close();
2845
+ });
2846
+ (0, bun_test_1.it)("onServerCreated uses default TOKEN_SECRET from env", function () { return __awaiter(void 0, void 0, void 0, function () {
2847
+ var http, app, expressApp, server;
2848
+ return __generator(this, function (_a) {
2849
+ switch (_a.label) {
2850
+ case 0: return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require("node:http")); })];
2851
+ case 1:
2852
+ http = _a.sent();
2853
+ process.env.TOKEN_SECRET = "env-secret";
2854
+ app = new realtimeApp_1.RealtimeApp({ debug: false });
2855
+ expressApp = (0, express_1.default)();
2856
+ app.register(expressApp);
2857
+ server = http.createServer(expressApp);
2858
+ app.onServerCreated(server);
2859
+ (0, bun_test_1.expect)(app.getIo()).not.toBeNull();
2860
+ return [4 /*yield*/, app.close()];
2861
+ case 2:
2862
+ _a.sent();
2863
+ server.close();
2864
+ return [2 /*return*/];
2865
+ }
2866
+ });
2867
+ }); });
2868
+ (0, bun_test_1.it)("setupAdapter logs info for redis adapter with URL", function () { return __awaiter(void 0, void 0, void 0, function () {
2869
+ var http, app, expressApp, server;
2870
+ return __generator(this, function (_a) {
2871
+ switch (_a.label) {
2872
+ case 0: return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require("node:http")); })];
2873
+ case 1:
2874
+ http = _a.sent();
2875
+ app = new realtimeApp_1.RealtimeApp({
2876
+ adapter: "redis",
2877
+ debug: true,
2878
+ redisUrl: "redis://user:pass@localhost:6379",
2879
+ tokenSecret: "test-secret",
2880
+ });
2881
+ expressApp = (0, express_1.default)();
2882
+ app.register(expressApp);
2883
+ server = http.createServer(expressApp);
2884
+ app.onServerCreated(server);
2885
+ (0, bun_test_1.expect)(app.getIo()).not.toBeNull();
2886
+ return [4 /*yield*/, app.close()];
2887
+ case 2:
2888
+ _a.sent();
2889
+ server.close();
2890
+ return [2 /*return*/];
2891
+ }
2892
+ });
2893
+ }); });
2894
+ (0, bun_test_1.it)("setupAdapter warns when redis adapter has no URL", function () { return __awaiter(void 0, void 0, void 0, function () {
2895
+ var http, origValkey, origRedis, app, expressApp, server;
2896
+ return __generator(this, function (_a) {
2897
+ switch (_a.label) {
2898
+ case 0: return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require("node:http")); })];
2899
+ case 1:
2900
+ http = _a.sent();
2901
+ origValkey = process.env.VALKEY_URL;
2902
+ origRedis = process.env.REDIS_URL;
2903
+ delete process.env.VALKEY_URL;
2904
+ delete process.env.REDIS_URL;
2905
+ app = new realtimeApp_1.RealtimeApp({
2906
+ adapter: "redis",
2907
+ debug: true,
2908
+ tokenSecret: "test-secret",
2909
+ });
2910
+ expressApp = (0, express_1.default)();
2911
+ app.register(expressApp);
2912
+ server = http.createServer(expressApp);
2913
+ app.onServerCreated(server);
2914
+ (0, bun_test_1.expect)(app.getIo()).not.toBeNull();
2915
+ return [4 /*yield*/, app.close()];
2916
+ case 2:
2917
+ _a.sent();
2918
+ server.close();
2919
+ process.env.VALKEY_URL = origValkey;
2920
+ process.env.REDIS_URL = origRedis;
2921
+ return [2 /*return*/];
2922
+ }
2923
+ });
2924
+ }); });
2925
+ (0, bun_test_1.it)("setupAdapter with none adapter does nothing extra", function () { return __awaiter(void 0, void 0, void 0, function () {
2926
+ var http, app, expressApp, server;
2927
+ return __generator(this, function (_a) {
2928
+ switch (_a.label) {
2929
+ case 0: return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require("node:http")); })];
2930
+ case 1:
2931
+ http = _a.sent();
2932
+ app = new realtimeApp_1.RealtimeApp({
2933
+ adapter: "none",
2934
+ debug: true,
2935
+ tokenSecret: "test-secret",
2936
+ });
2937
+ expressApp = (0, express_1.default)();
2938
+ app.register(expressApp);
2939
+ server = http.createServer(expressApp);
2940
+ app.onServerCreated(server);
2941
+ (0, bun_test_1.expect)(app.getIo()).not.toBeNull();
2942
+ return [4 /*yield*/, app.close()];
2943
+ case 2:
2944
+ _a.sent();
2945
+ server.close();
2946
+ return [2 /*return*/];
2947
+ }
2948
+ });
2949
+ }); });
2950
+ (0, bun_test_1.it)("close is safe after onServerCreated", function () { return __awaiter(void 0, void 0, void 0, function () {
2951
+ var http, app, expressApp, server;
2952
+ return __generator(this, function (_a) {
2953
+ switch (_a.label) {
2954
+ case 0: return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require("node:http")); })];
2955
+ case 1:
2956
+ http = _a.sent();
2957
+ app = new realtimeApp_1.RealtimeApp({ tokenSecret: "test-secret" });
2958
+ expressApp = (0, express_1.default)();
2959
+ app.register(expressApp);
2960
+ server = http.createServer(expressApp);
2961
+ app.onServerCreated(server);
2962
+ return [4 /*yield*/, app.close()];
2963
+ case 2:
2964
+ _a.sent();
2965
+ (0, bun_test_1.expect)(app.getIo()).toBeNull();
2966
+ server.close();
2967
+ return [2 /*return*/];
2968
+ }
2969
+ });
2970
+ }); });
2971
+ (0, bun_test_1.it)("health endpoint reports running after onServerCreated", function () { return __awaiter(void 0, void 0, void 0, function () {
2972
+ var http, app, expressApp, server, supertest, st, res;
2973
+ return __generator(this, function (_a) {
2974
+ switch (_a.label) {
2975
+ case 0: return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require("node:http")); })];
2976
+ case 1:
2977
+ http = _a.sent();
2978
+ app = new realtimeApp_1.RealtimeApp({ tokenSecret: "test-secret" });
2979
+ expressApp = (0, express_1.default)();
2980
+ app.register(expressApp);
2981
+ server = http.createServer(expressApp);
2982
+ app.onServerCreated(server);
2983
+ return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require("supertest")); })];
2984
+ case 2:
2985
+ supertest = _a.sent();
2986
+ st = supertest.default(expressApp);
2987
+ return [4 /*yield*/, st.get("/realtime/health").expect(200)];
2988
+ case 3:
2989
+ res = _a.sent();
2990
+ (0, bun_test_1.expect)(res.body.status).toBe("running");
2991
+ return [4 /*yield*/, app.close()];
2992
+ case 4:
2993
+ _a.sent();
2994
+ server.close();
2995
+ return [2 /*return*/];
2996
+ }
2997
+ });
2998
+ }); });
2999
+ (0, bun_test_1.it)("onServerCreated with custom cors option", function () { return __awaiter(void 0, void 0, void 0, function () {
3000
+ var http, app, expressApp, server;
3001
+ return __generator(this, function (_a) {
3002
+ switch (_a.label) {
3003
+ case 0: return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require("node:http")); })];
3004
+ case 1:
3005
+ http = _a.sent();
3006
+ app = new realtimeApp_1.RealtimeApp({
3007
+ cors: { methods: ["GET"], origin: "https://example.com" },
3008
+ tokenSecret: "test-secret",
3009
+ });
3010
+ expressApp = (0, express_1.default)();
3011
+ app.register(expressApp);
3012
+ server = http.createServer(expressApp);
3013
+ app.onServerCreated(server);
3014
+ (0, bun_test_1.expect)(app.getIo()).not.toBeNull();
3015
+ return [4 /*yield*/, app.close()];
3016
+ case 2:
3017
+ _a.sent();
3018
+ server.close();
3019
+ return [2 /*return*/];
3020
+ }
3021
+ });
3022
+ }); });
3023
+ (0, bun_test_1.it)("setupAdapter uses VALKEY_URL when redisUrl not provided", function () { return __awaiter(void 0, void 0, void 0, function () {
3024
+ var http, app, expressApp, server;
3025
+ return __generator(this, function (_a) {
3026
+ switch (_a.label) {
3027
+ case 0: return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require("node:http")); })];
3028
+ case 1:
3029
+ http = _a.sent();
3030
+ process.env.VALKEY_URL = "redis://localhost:6379";
3031
+ app = new realtimeApp_1.RealtimeApp({
3032
+ adapter: "redis",
3033
+ debug: true,
3034
+ tokenSecret: "test-secret",
3035
+ });
3036
+ expressApp = (0, express_1.default)();
3037
+ app.register(expressApp);
3038
+ server = http.createServer(expressApp);
3039
+ app.onServerCreated(server);
3040
+ (0, bun_test_1.expect)(app.getIo()).not.toBeNull();
3041
+ return [4 /*yield*/, app.close()];
3042
+ case 2:
3043
+ _a.sent();
3044
+ server.close();
3045
+ delete process.env.VALKEY_URL;
3046
+ return [2 /*return*/];
3047
+ }
3048
+ });
3049
+ }); });
3050
+ });
2143
3051
  (0, bun_test_1.describe)("redactCredentials", function () {
2144
3052
  (0, bun_test_1.it)("redacts user:password@ in a redis URL", function () {
2145
3053
  (0, bun_test_1.expect)((0, realtimeApp_1.redactCredentials)("redis://user:secret@host:6379/0")).toBe("redis://***@host:6379/0");