@terreno/api 0.13.2 → 0.14.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/__tests__/versionCheckPlugin.test.js +53 -3
- package/dist/api.arrayOperations.test.js +1 -0
- package/dist/api.asyncHandler.test.d.ts +1 -0
- package/dist/api.asyncHandler.test.js +236 -0
- package/dist/api.d.ts +15 -4
- package/dist/api.errors.test.js +1 -0
- package/dist/api.hooks.test.js +1 -0
- package/dist/api.js +153 -104
- package/dist/api.query.test.js +1 -0
- package/dist/api.test.js +174 -0
- package/dist/auth.d.ts +10 -5
- package/dist/auth.js +163 -90
- package/dist/auth.test.js +159 -0
- package/dist/betterAuthApp.test.js +1 -0
- package/dist/betterAuthSetup.d.ts +5 -6
- package/dist/betterAuthSetup.js +17 -14
- package/dist/betterAuthSetup.test.js +1 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.js +248 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +328 -0
- package/dist/configuration.test.js +1 -0
- package/dist/configurationApp.d.ts +1 -1
- package/dist/configurationApp.js +17 -13
- package/dist/configurationPlugin.test.js +1 -0
- package/dist/consentApp.test.js +1 -0
- package/dist/envConfigurationPlugin.d.ts +2 -0
- package/dist/envConfigurationPlugin.js +173 -0
- package/dist/envConfigurationPlugin.test.d.ts +1 -0
- package/dist/envConfigurationPlugin.test.js +322 -0
- package/dist/errors.d.ts +18 -7
- package/dist/errors.js +106 -10
- package/dist/errors.test.js +16 -1
- package/dist/example.js +16 -7
- package/dist/expressServer.d.ts +10 -9
- package/dist/expressServer.js +62 -53
- package/dist/expressServer.test.js +53 -2
- package/dist/githubAuth.d.ts +2 -1
- package/dist/githubAuth.js +41 -26
- package/dist/githubAuth.test.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +42 -20
- package/dist/models/versionConfig.d.ts +2 -0
- package/dist/models/versionConfig.js +8 -0
- package/dist/notifiers/googleChatNotifier.js +14 -16
- package/dist/notifiers/googleChatNotifier.test.js +1 -0
- package/dist/notifiers/slackNotifier.js +16 -14
- package/dist/notifiers/slackNotifier.test.js +41 -3
- package/dist/notifiers/zoomNotifier.js +7 -10
- package/dist/notifiers/zoomNotifier.test.js +1 -0
- package/dist/openApi.d.ts +1 -1
- package/dist/openApi.test.js +1 -0
- package/dist/openApiBuilder.d.ts +39 -6
- package/dist/openApiBuilder.js +1 -31
- package/dist/openApiBuilder.test.js +1 -0
- package/dist/openApiValidator.js +1 -0
- package/dist/openApiValidator.test.js +65 -0
- package/dist/permissions.d.ts +4 -4
- package/dist/permissions.js +67 -65
- package/dist/permissions.middleware.test.js +1 -0
- package/dist/permissions.test.js +1 -0
- package/dist/plugins.d.ts +5 -5
- package/dist/plugins.js +18 -9
- package/dist/plugins.test.js +1 -1
- package/dist/populate.d.ts +15 -8
- package/dist/populate.js +23 -24
- package/dist/populate.test.js +1 -0
- package/dist/realtime/changeStreamWatcher.d.ts +73 -0
- package/dist/realtime/changeStreamWatcher.js +720 -0
- package/dist/realtime/index.d.ts +6 -0
- package/dist/realtime/index.js +27 -0
- package/dist/realtime/queryMatcher.d.ts +14 -0
- package/dist/realtime/queryMatcher.js +250 -0
- package/dist/realtime/queryStore.d.ts +37 -0
- package/dist/realtime/queryStore.js +195 -0
- package/dist/realtime/realtime.test.d.ts +10 -0
- package/dist/realtime/realtime.test.js +2158 -0
- package/dist/realtime/realtimeApp.d.ts +93 -0
- package/dist/realtime/realtimeApp.js +560 -0
- package/dist/realtime/registry.d.ts +40 -0
- package/dist/realtime/registry.js +38 -0
- package/dist/realtime/socketUser.d.ts +10 -0
- package/dist/realtime/socketUser.js +17 -0
- package/dist/realtime/types.d.ts +100 -0
- package/dist/realtime/types.js +2 -0
- package/dist/requestContext.d.ts +37 -0
- package/dist/requestContext.js +344 -0
- package/dist/requestContext.test.d.ts +1 -0
- package/dist/requestContext.test.js +241 -0
- package/dist/terrenoApp.d.ts +8 -0
- package/dist/terrenoApp.js +50 -13
- package/dist/terrenoApp.test.js +194 -21
- package/dist/terrenoPlugin.d.ts +11 -0
- package/dist/tests/bunSetup.js +1 -0
- package/dist/tests.js +1 -1
- package/dist/transformers.d.ts +2 -2
- package/dist/transformers.js +5 -3
- package/dist/transformers.test.js +90 -0
- package/dist/types/consentResponse.d.ts +6 -3
- package/dist/versionCheckPlugin.d.ts +2 -0
- package/dist/versionCheckPlugin.js +18 -12
- package/package.json +4 -2
- package/src/__tests__/versionCheckPlugin.test.ts +37 -3
- package/src/api.arrayOperations.test.ts +1 -0
- package/src/api.asyncHandler.test.ts +177 -0
- package/src/api.errors.test.ts +1 -0
- package/src/api.hooks.test.ts +1 -0
- package/src/api.query.test.ts +1 -0
- package/src/api.test.ts +132 -0
- package/src/api.ts +199 -84
- package/src/auth.test.ts +160 -0
- package/src/auth.ts +120 -50
- package/src/betterAuthApp.test.ts +1 -0
- package/src/betterAuthSetup.test.ts +1 -0
- package/src/betterAuthSetup.ts +46 -19
- package/src/config.test.ts +255 -0
- package/src/config.ts +206 -0
- package/src/configuration.test.ts +1 -0
- package/src/configurationApp.ts +59 -24
- package/src/configurationPlugin.test.ts +1 -0
- package/src/consentApp.test.ts +1 -0
- package/src/envConfigurationPlugin.test.ts +143 -0
- package/src/envConfigurationPlugin.ts +100 -0
- package/src/errors.test.ts +19 -1
- package/src/errors.ts +94 -20
- package/src/example.ts +46 -21
- package/src/express.d.ts +18 -1
- package/src/expressServer.test.ts +50 -2
- package/src/expressServer.ts +80 -50
- package/src/githubAuth.test.ts +1 -0
- package/src/githubAuth.ts +59 -38
- package/src/index.ts +4 -0
- package/src/logger.ts +47 -17
- package/src/models/versionConfig.ts +13 -2
- package/src/notifiers/googleChatNotifier.test.ts +1 -0
- package/src/notifiers/googleChatNotifier.ts +7 -9
- package/src/notifiers/slackNotifier.test.ts +29 -3
- package/src/notifiers/slackNotifier.ts +9 -7
- package/src/notifiers/zoomNotifier.test.ts +1 -0
- package/src/notifiers/zoomNotifier.ts +8 -11
- package/src/openApi.test.ts +1 -0
- package/src/openApi.ts +4 -4
- package/src/openApiBuilder.test.ts +1 -0
- package/src/openApiBuilder.ts +14 -11
- package/src/openApiValidator.test.ts +59 -0
- package/src/openApiValidator.ts +3 -2
- package/src/permissions.middleware.test.ts +1 -0
- package/src/permissions.test.ts +1 -0
- package/src/permissions.ts +30 -25
- package/src/plugins.test.ts +1 -1
- package/src/plugins.ts +21 -14
- package/src/populate.test.ts +1 -0
- package/src/populate.ts +44 -36
- package/src/realtime/changeStreamWatcher.ts +568 -0
- package/src/realtime/index.ts +34 -0
- package/src/realtime/queryMatcher.ts +179 -0
- package/src/realtime/queryStore.ts +132 -0
- package/src/realtime/realtime.test.ts +1755 -0
- package/src/realtime/realtimeApp.ts +478 -0
- package/src/realtime/registry.ts +64 -0
- package/src/realtime/socketUser.ts +25 -0
- package/src/realtime/types.ts +112 -0
- package/src/requestContext.test.ts +196 -0
- package/src/requestContext.ts +368 -0
- package/src/terrenoApp.test.ts +137 -11
- package/src/terrenoApp.ts +64 -17
- package/src/terrenoPlugin.ts +12 -0
- package/src/tests/bunSetup.ts +1 -0
- package/src/tests.ts +7 -2
- package/src/transformers.test.ts +70 -2
- package/src/transformers.ts +15 -7
- package/src/types/consentResponse.ts +8 -10
- package/src/versionCheckPlugin.ts +15 -7
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
47
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
48
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
49
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
50
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
51
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
52
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
56
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
57
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
58
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
59
|
+
function step(op) {
|
|
60
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
61
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
62
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
63
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
64
|
+
switch (op[0]) {
|
|
65
|
+
case 0: case 1: t = op; break;
|
|
66
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
67
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
68
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
69
|
+
default:
|
|
70
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
71
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
72
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
73
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
74
|
+
if (t[2]) _.ops.pop();
|
|
75
|
+
_.trys.pop(); continue;
|
|
76
|
+
}
|
|
77
|
+
op = body.call(thisArg, _);
|
|
78
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
79
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var __values = (this && this.__values) || function(o) {
|
|
83
|
+
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
84
|
+
if (m) return m.call(o);
|
|
85
|
+
if (o && typeof o.length === "number") return {
|
|
86
|
+
next: function () {
|
|
87
|
+
if (o && i >= o.length) o = void 0;
|
|
88
|
+
return { value: o && o[i++], done: !o };
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
92
|
+
};
|
|
93
|
+
var __read = (this && this.__read) || function (o, n) {
|
|
94
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
95
|
+
if (!m) return o;
|
|
96
|
+
var i = m.call(o), r, ar = [], e;
|
|
97
|
+
try {
|
|
98
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
99
|
+
}
|
|
100
|
+
catch (error) { e = { error: error }; }
|
|
101
|
+
finally {
|
|
102
|
+
try {
|
|
103
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
104
|
+
}
|
|
105
|
+
finally { if (e) throw e.error; }
|
|
106
|
+
}
|
|
107
|
+
return ar;
|
|
108
|
+
};
|
|
109
|
+
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
|
|
110
|
+
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
|
|
111
|
+
if (ar || !(i in from)) {
|
|
112
|
+
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
|
|
113
|
+
ar[i] = from[i];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return to.concat(ar || Array.prototype.slice.call(from));
|
|
117
|
+
};
|
|
118
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
119
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
120
|
+
};
|
|
121
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
122
|
+
exports.stopChangeStreamWatcher = exports.startChangeStreamWatcher = exports.emitToDocumentAndQueryRooms = exports.emitToAuthorizedRoom = exports.serializeDoc = exports.ensureApiId = exports.resolveRooms = exports.mapOperationType = void 0;
|
|
123
|
+
// biome-ignore-all lint/suspicious/noExplicitAny: change stream and socket handlers use dynamic document shapes
|
|
124
|
+
var Sentry = __importStar(require("@sentry/bun"));
|
|
125
|
+
var luxon_1 = require("luxon");
|
|
126
|
+
var mongoose_1 = __importDefault(require("mongoose"));
|
|
127
|
+
var logger_1 = require("../logger");
|
|
128
|
+
var permissions_1 = require("../permissions");
|
|
129
|
+
var queryMatcher_1 = require("./queryMatcher");
|
|
130
|
+
var queryStore_1 = require("./queryStore");
|
|
131
|
+
var registry_1 = require("./registry");
|
|
132
|
+
var socketUser_1 = require("./socketUser");
|
|
133
|
+
var changeWatcher = null;
|
|
134
|
+
var DEFAULT_IGNORED_COLLECTIONS = ["socketio", "sessions"];
|
|
135
|
+
/**
|
|
136
|
+
* Map MongoDB change stream operation types to our method names.
|
|
137
|
+
*
|
|
138
|
+
* Soft deletes (an `update` that sets `deleted: true`) are reclassified as
|
|
139
|
+
* `"delete"` only when the model has `"delete"` enabled in its realtime
|
|
140
|
+
* methods. Otherwise they fall back to `"update"` so models that subscribe
|
|
141
|
+
* to updates (but not deletes) still see the change — without this fallback,
|
|
142
|
+
* a model configured with `methods: ["create", "update"]` would silently
|
|
143
|
+
* drop soft-delete events.
|
|
144
|
+
*
|
|
145
|
+
* Exported for testing.
|
|
146
|
+
*/
|
|
147
|
+
var mapOperationType = function (operationType, change, enabledMethods) {
|
|
148
|
+
var _a, _b;
|
|
149
|
+
if (enabledMethods === void 0) { enabledMethods = ["create", "update", "delete"]; }
|
|
150
|
+
if (operationType === "insert") {
|
|
151
|
+
return "create";
|
|
152
|
+
}
|
|
153
|
+
if (operationType === "update" || operationType === "replace") {
|
|
154
|
+
// Soft delete on an update event: the document was patched with deleted=true.
|
|
155
|
+
// `change` is typed as the full union (without operationType narrowing) because
|
|
156
|
+
// callers/tests pass change objects without setting `operationType` on the change.
|
|
157
|
+
var updateChange = change;
|
|
158
|
+
var isSoftDelete = operationType === "update" && ((_b = (_a = updateChange.updateDescription) === null || _a === void 0 ? void 0 : _a.updatedFields) === null || _b === void 0 ? void 0 : _b.deleted) === true;
|
|
159
|
+
if (isSoftDelete && enabledMethods.includes("delete")) {
|
|
160
|
+
return "delete";
|
|
161
|
+
}
|
|
162
|
+
return "update";
|
|
163
|
+
}
|
|
164
|
+
if (operationType === "delete") {
|
|
165
|
+
return "delete";
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
};
|
|
169
|
+
exports.mapOperationType = mapOperationType;
|
|
170
|
+
/**
|
|
171
|
+
* Get the collection tag from a route path (strips leading "/").
|
|
172
|
+
* E.g. "/todos" -> "todos"
|
|
173
|
+
*/
|
|
174
|
+
var getCollectionTag = function (routePath) { return routePath.replace(/^\//, ""); };
|
|
175
|
+
var getSocketsInRoom = function (io, room) {
|
|
176
|
+
var e_1, _a;
|
|
177
|
+
var socketIds = io.sockets.adapter.rooms.get(room);
|
|
178
|
+
if (!socketIds) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
var sockets = [];
|
|
182
|
+
try {
|
|
183
|
+
for (var socketIds_1 = __values(socketIds), socketIds_1_1 = socketIds_1.next(); !socketIds_1_1.done; socketIds_1_1 = socketIds_1.next()) {
|
|
184
|
+
var socketId = socketIds_1_1.value;
|
|
185
|
+
var socket = io.sockets.sockets.get(socketId);
|
|
186
|
+
if (socket) {
|
|
187
|
+
sockets.push(socket);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
192
|
+
finally {
|
|
193
|
+
try {
|
|
194
|
+
if (socketIds_1_1 && !socketIds_1_1.done && (_a = socketIds_1.return)) _a.call(socketIds_1);
|
|
195
|
+
}
|
|
196
|
+
finally { if (e_1) throw e_1.error; }
|
|
197
|
+
}
|
|
198
|
+
return sockets;
|
|
199
|
+
};
|
|
200
|
+
var canReadDocument = function (entry, user, doc) { return __awaiter(void 0, void 0, void 0, function () {
|
|
201
|
+
return __generator(this, function (_a) {
|
|
202
|
+
return [2 /*return*/, (0, permissions_1.checkPermissions)("read", entry.options.permissions.read, user, doc)];
|
|
203
|
+
});
|
|
204
|
+
}); };
|
|
205
|
+
/**
|
|
206
|
+
* Determine which Socket.io rooms to emit to based on the room strategy.
|
|
207
|
+
* Exported for testing.
|
|
208
|
+
*/
|
|
209
|
+
var resolveRooms = function (entry, doc, method) {
|
|
210
|
+
var _a, _b, _c;
|
|
211
|
+
var roomStrategy = entry.config.roomStrategy;
|
|
212
|
+
// Use the collection tag (e.g. "todos") for model rooms, matching what the frontend subscribes to
|
|
213
|
+
var collectionTag = getCollectionTag(entry.routePath);
|
|
214
|
+
if (typeof roomStrategy === "function") {
|
|
215
|
+
// Custom room resolver — pass a minimal pseudo-request. The strategy only inspects
|
|
216
|
+
// doc/method; we never read fields off req here, so an empty cast is safe.
|
|
217
|
+
return roomStrategy(doc, method, {});
|
|
218
|
+
}
|
|
219
|
+
switch (roomStrategy) {
|
|
220
|
+
case "owner": {
|
|
221
|
+
var ownerId = (_c = (_b = (_a = doc === null || doc === void 0 ? void 0 : doc.ownerId) === null || _a === void 0 ? void 0 : _a.toString) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : doc === null || doc === void 0 ? void 0 : doc.ownerId;
|
|
222
|
+
if (ownerId) {
|
|
223
|
+
return ["user:".concat(ownerId)];
|
|
224
|
+
}
|
|
225
|
+
// If no ownerId, fall back to model room
|
|
226
|
+
return ["model:".concat(collectionTag)];
|
|
227
|
+
}
|
|
228
|
+
case "model":
|
|
229
|
+
return ["model:".concat(collectionTag)];
|
|
230
|
+
case "broadcast":
|
|
231
|
+
return ["authenticated"];
|
|
232
|
+
default:
|
|
233
|
+
return ["model:".concat(collectionTag)];
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
exports.resolveRooms = resolveRooms;
|
|
237
|
+
/**
|
|
238
|
+
* Ensure serialized documents include `id` to match REST API responses.
|
|
239
|
+
* Change stream fullDocument payloads are raw BSON objects with `_id` only.
|
|
240
|
+
*/
|
|
241
|
+
var ensureApiId = function (data) {
|
|
242
|
+
if (data == null || typeof data !== "object" || Array.isArray(data)) {
|
|
243
|
+
return data;
|
|
244
|
+
}
|
|
245
|
+
var serialized = data;
|
|
246
|
+
if (serialized._id != null && serialized.id == null) {
|
|
247
|
+
return __assign(__assign({}, serialized), { id: serialized._id });
|
|
248
|
+
}
|
|
249
|
+
return data;
|
|
250
|
+
};
|
|
251
|
+
exports.ensureApiId = ensureApiId;
|
|
252
|
+
/**
|
|
253
|
+
* Serialize a document for emission.
|
|
254
|
+
*
|
|
255
|
+
* Precedence:
|
|
256
|
+
* 1. `realtimeResponseHandler` if provided (full control over what's emitted).
|
|
257
|
+
* 2. modelRouter `responseHandler` if provided — invoked with a synthetic request
|
|
258
|
+
* so the same stripping logic used for REST responses (e.g. removing `hash`/`salt`)
|
|
259
|
+
* applies to realtime events. This prevents accidental leaks when an app only
|
|
260
|
+
* configures sanitization in the REST `responseHandler`.
|
|
261
|
+
* 3. `toJSON()` fallback.
|
|
262
|
+
*
|
|
263
|
+
* If a user-supplied handler throws, we re-throw so the caller's outer try/catch
|
|
264
|
+
* records the failure and the event is dropped. Falling back to `toJSON()` here
|
|
265
|
+
* would risk leaking unsanitized fields (e.g. `hash`/`salt`) that the handler
|
|
266
|
+
* was supposed to strip.
|
|
267
|
+
*/
|
|
268
|
+
var serializeDoc = function (entry, doc, method, user) { return __awaiter(void 0, void 0, void 0, function () {
|
|
269
|
+
var _a, error_1, responseHandler, restMethod, syntheticReq, _b, error_2;
|
|
270
|
+
var _c;
|
|
271
|
+
return __generator(this, function (_d) {
|
|
272
|
+
switch (_d.label) {
|
|
273
|
+
case 0:
|
|
274
|
+
if (!entry.config.realtimeResponseHandler) return [3 /*break*/, 4];
|
|
275
|
+
_d.label = 1;
|
|
276
|
+
case 1:
|
|
277
|
+
_d.trys.push([1, 3, , 4]);
|
|
278
|
+
_a = exports.ensureApiId;
|
|
279
|
+
return [4 /*yield*/, entry.config.realtimeResponseHandler(doc, method)];
|
|
280
|
+
case 2: return [2 /*return*/, _a.apply(void 0, [_d.sent()])];
|
|
281
|
+
case 3:
|
|
282
|
+
error_1 = _d.sent();
|
|
283
|
+
logger_1.logger.error("[realtime] realtimeResponseHandler threw for ".concat(entry.modelName, "/").concat(method, ": ").concat(error_1, ". ") +
|
|
284
|
+
"Dropping event to avoid leaking unsanitized data.");
|
|
285
|
+
throw error_1;
|
|
286
|
+
case 4:
|
|
287
|
+
responseHandler = (_c = entry.options) === null || _c === void 0 ? void 0 : _c.responseHandler;
|
|
288
|
+
if (!responseHandler) return [3 /*break*/, 8];
|
|
289
|
+
_d.label = 5;
|
|
290
|
+
case 5:
|
|
291
|
+
_d.trys.push([5, 7, , 8]);
|
|
292
|
+
restMethod = method === "delete" ? "read" : method;
|
|
293
|
+
syntheticReq = { params: {}, query: {}, user: user };
|
|
294
|
+
_b = exports.ensureApiId;
|
|
295
|
+
return [4 /*yield*/, responseHandler(doc, restMethod, syntheticReq, entry.options)];
|
|
296
|
+
case 6: return [2 /*return*/, _b.apply(void 0, [_d.sent()])];
|
|
297
|
+
case 7:
|
|
298
|
+
error_2 = _d.sent();
|
|
299
|
+
logger_1.logger.error("[realtime] modelRouter responseHandler threw during realtime serialization for " +
|
|
300
|
+
"".concat(entry.modelName, "/").concat(method, ": ").concat(error_2, ". Dropping event to avoid leaking unsanitized data."));
|
|
301
|
+
throw error_2;
|
|
302
|
+
case 8: return [2 /*return*/, (0, exports.ensureApiId)(typeof doc.toJSON === "function" ? doc.toJSON() : doc)];
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}); };
|
|
306
|
+
exports.serializeDoc = serializeDoc;
|
|
307
|
+
var emitToAuthorizedRoom = function (io, room, event, entry, fullDocument, logDebug) { return __awaiter(void 0, void 0, void 0, function () {
|
|
308
|
+
var sockets, sockets_1, sockets_1_1, socket, user, permissionDocument, canRead, data, error_3, e_2_1;
|
|
309
|
+
var e_2, _a;
|
|
310
|
+
return __generator(this, function (_b) {
|
|
311
|
+
switch (_b.label) {
|
|
312
|
+
case 0:
|
|
313
|
+
sockets = getSocketsInRoom(io, room);
|
|
314
|
+
_b.label = 1;
|
|
315
|
+
case 1:
|
|
316
|
+
_b.trys.push([1, 9, 10, 11]);
|
|
317
|
+
sockets_1 = __values(sockets), sockets_1_1 = sockets_1.next();
|
|
318
|
+
_b.label = 2;
|
|
319
|
+
case 2:
|
|
320
|
+
if (!!sockets_1_1.done) return [3 /*break*/, 8];
|
|
321
|
+
socket = sockets_1_1.value;
|
|
322
|
+
user = (0, socketUser_1.getSocketUser)(socket);
|
|
323
|
+
_b.label = 3;
|
|
324
|
+
case 3:
|
|
325
|
+
_b.trys.push([3, 6, , 7]);
|
|
326
|
+
permissionDocument = fullDocument !== null && fullDocument !== void 0 ? fullDocument : {};
|
|
327
|
+
return [4 /*yield*/, canReadDocument(entry, user, permissionDocument)];
|
|
328
|
+
case 4:
|
|
329
|
+
canRead = _b.sent();
|
|
330
|
+
if (!canRead) {
|
|
331
|
+
logDebug("[realtime] Skipped ".concat(room, " for ").concat(socket.id, ": read permission denied"));
|
|
332
|
+
return [3 /*break*/, 7];
|
|
333
|
+
}
|
|
334
|
+
if (!fullDocument) {
|
|
335
|
+
socket.emit("sync", event);
|
|
336
|
+
return [3 /*break*/, 7];
|
|
337
|
+
}
|
|
338
|
+
return [4 /*yield*/, (0, exports.serializeDoc)(entry, fullDocument, event.method, user)];
|
|
339
|
+
case 5:
|
|
340
|
+
data = _b.sent();
|
|
341
|
+
socket.emit("sync", __assign(__assign({}, event), { data: data }));
|
|
342
|
+
return [3 /*break*/, 7];
|
|
343
|
+
case 6:
|
|
344
|
+
error_3 = _b.sent();
|
|
345
|
+
logger_1.logger.error("[realtime] Failed to emit ".concat(entry.modelName, "/").concat(event.method, " to socket ").concat(socket.id, ": ").concat(error_3));
|
|
346
|
+
Sentry.captureException(error_3);
|
|
347
|
+
return [3 /*break*/, 7];
|
|
348
|
+
case 7:
|
|
349
|
+
sockets_1_1 = sockets_1.next();
|
|
350
|
+
return [3 /*break*/, 2];
|
|
351
|
+
case 8: return [3 /*break*/, 11];
|
|
352
|
+
case 9:
|
|
353
|
+
e_2_1 = _b.sent();
|
|
354
|
+
e_2 = { error: e_2_1 };
|
|
355
|
+
return [3 /*break*/, 11];
|
|
356
|
+
case 10:
|
|
357
|
+
try {
|
|
358
|
+
if (sockets_1_1 && !sockets_1_1.done && (_a = sockets_1.return)) _a.call(sockets_1);
|
|
359
|
+
}
|
|
360
|
+
finally { if (e_2) throw e_2.error; }
|
|
361
|
+
return [7 /*endfinally*/];
|
|
362
|
+
case 11: return [2 /*return*/];
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}); };
|
|
366
|
+
exports.emitToAuthorizedRoom = emitToAuthorizedRoom;
|
|
367
|
+
/**
|
|
368
|
+
* Emit a sync event to document-specific and query rooms.
|
|
369
|
+
*
|
|
370
|
+
* Document rooms: `document:{collection}:{docId}` — clients subscribed to a single document.
|
|
371
|
+
* Query rooms: `query:{queryId}` — clients subscribed to a query filter. The change stream
|
|
372
|
+
* watcher evaluates whether the document matches each active query for the collection.
|
|
373
|
+
*
|
|
374
|
+
* For deletes, we are careful not to leak cross-user activity:
|
|
375
|
+
* - Soft deletes (fullDocument present) are matched against each query like updates so
|
|
376
|
+
* query subscribers only see deletes for docs that matched their filter.
|
|
377
|
+
* - Hard deletes (fullDocument absent) on owner-strategy collections are NOT forwarded
|
|
378
|
+
* to query rooms — subscribers will reconcile on their next fetch. Other strategies
|
|
379
|
+
* forward the delete because the model/broadcast rooms are not user-scoped.
|
|
380
|
+
*
|
|
381
|
+
* Exported for testing.
|
|
382
|
+
*/
|
|
383
|
+
var emitToDocumentAndQueryRooms = function (io, collection, event, fullDocument, logDebug, entry) { return __awaiter(void 0, void 0, void 0, function () {
|
|
384
|
+
var docRoom, isOwnerStrategy, querySubscriptions, querySubscriptions_1, querySubscriptions_1_1, _a, queryId, query, queryRoom, docMatches, removeEvent, e_3_1;
|
|
385
|
+
var e_3, _b;
|
|
386
|
+
return __generator(this, function (_c) {
|
|
387
|
+
switch (_c.label) {
|
|
388
|
+
case 0:
|
|
389
|
+
docRoom = "document:".concat(collection, ":").concat(event.id);
|
|
390
|
+
if (!entry) return [3 /*break*/, 2];
|
|
391
|
+
return [4 /*yield*/, (0, exports.emitToAuthorizedRoom)(io, docRoom, event, entry, fullDocument, logDebug)];
|
|
392
|
+
case 1:
|
|
393
|
+
_c.sent();
|
|
394
|
+
return [3 /*break*/, 3];
|
|
395
|
+
case 2:
|
|
396
|
+
io.to(docRoom).emit("sync", event);
|
|
397
|
+
_c.label = 3;
|
|
398
|
+
case 3:
|
|
399
|
+
logDebug("[realtime] Emitted ".concat(event.method, " to ").concat(docRoom));
|
|
400
|
+
isOwnerStrategy = (entry === null || entry === void 0 ? void 0 : entry.config.roomStrategy) === "owner";
|
|
401
|
+
querySubscriptions = (0, queryStore_1.getQuerySubscriptionsForCollection)(collection);
|
|
402
|
+
_c.label = 4;
|
|
403
|
+
case 4:
|
|
404
|
+
_c.trys.push([4, 28, 29, 30]);
|
|
405
|
+
querySubscriptions_1 = __values(querySubscriptions), querySubscriptions_1_1 = querySubscriptions_1.next();
|
|
406
|
+
_c.label = 5;
|
|
407
|
+
case 5:
|
|
408
|
+
if (!!querySubscriptions_1_1.done) return [3 /*break*/, 27];
|
|
409
|
+
_a = querySubscriptions_1_1.value, queryId = _a.queryId, query = _a.query;
|
|
410
|
+
queryRoom = "query:".concat(queryId);
|
|
411
|
+
if (!(event.method === "delete")) return [3 /*break*/, 14];
|
|
412
|
+
if (!!fullDocument) return [3 /*break*/, 9];
|
|
413
|
+
// Hard delete with no document context. For owner-strategy collections we can't
|
|
414
|
+
// tell which query rooms belong to the owner without leaking activity to others,
|
|
415
|
+
// so skip query fanout entirely — subscribers will reconcile on next fetch.
|
|
416
|
+
if (isOwnerStrategy) {
|
|
417
|
+
logDebug("[realtime] Skipping hard delete fanout to ".concat(queryRoom, " (owner strategy, no fullDocument)"));
|
|
418
|
+
return [3 /*break*/, 26];
|
|
419
|
+
}
|
|
420
|
+
if (!entry) return [3 /*break*/, 7];
|
|
421
|
+
return [4 /*yield*/, (0, exports.emitToAuthorizedRoom)(io, queryRoom, event, entry, fullDocument, logDebug)];
|
|
422
|
+
case 6:
|
|
423
|
+
_c.sent();
|
|
424
|
+
return [3 /*break*/, 8];
|
|
425
|
+
case 7:
|
|
426
|
+
io.to(queryRoom).emit("sync", event);
|
|
427
|
+
_c.label = 8;
|
|
428
|
+
case 8:
|
|
429
|
+
logDebug("[realtime] Emitted hard delete to ".concat(queryRoom));
|
|
430
|
+
return [3 /*break*/, 26];
|
|
431
|
+
case 9:
|
|
432
|
+
if (!(0, queryMatcher_1.matchesQuery)(fullDocument, query)) return [3 /*break*/, 13];
|
|
433
|
+
if (!entry) return [3 /*break*/, 11];
|
|
434
|
+
return [4 /*yield*/, (0, exports.emitToAuthorizedRoom)(io, queryRoom, event, entry, fullDocument, logDebug)];
|
|
435
|
+
case 10:
|
|
436
|
+
_c.sent();
|
|
437
|
+
return [3 /*break*/, 12];
|
|
438
|
+
case 11:
|
|
439
|
+
io.to(queryRoom).emit("sync", event);
|
|
440
|
+
_c.label = 12;
|
|
441
|
+
case 12:
|
|
442
|
+
logDebug("[realtime] Emitted soft delete to ".concat(queryRoom, " (query matched)"));
|
|
443
|
+
_c.label = 13;
|
|
444
|
+
case 13: return [3 /*break*/, 26];
|
|
445
|
+
case 14:
|
|
446
|
+
if (!fullDocument) {
|
|
447
|
+
return [3 /*break*/, 26];
|
|
448
|
+
}
|
|
449
|
+
docMatches = (0, queryMatcher_1.matchesQuery)(fullDocument, query);
|
|
450
|
+
if (!(event.method === "create" && docMatches)) return [3 /*break*/, 18];
|
|
451
|
+
if (!entry) return [3 /*break*/, 16];
|
|
452
|
+
return [4 /*yield*/, (0, exports.emitToAuthorizedRoom)(io, queryRoom, event, entry, fullDocument, logDebug)];
|
|
453
|
+
case 15:
|
|
454
|
+
_c.sent();
|
|
455
|
+
return [3 /*break*/, 17];
|
|
456
|
+
case 16:
|
|
457
|
+
io.to(queryRoom).emit("sync", event);
|
|
458
|
+
_c.label = 17;
|
|
459
|
+
case 17:
|
|
460
|
+
logDebug("[realtime] Emitted create to ".concat(queryRoom, " (query matched)"));
|
|
461
|
+
return [3 /*break*/, 26];
|
|
462
|
+
case 18:
|
|
463
|
+
if (!(event.method === "update")) return [3 /*break*/, 26];
|
|
464
|
+
if (!docMatches) return [3 /*break*/, 22];
|
|
465
|
+
if (!entry) return [3 /*break*/, 20];
|
|
466
|
+
return [4 /*yield*/, (0, exports.emitToAuthorizedRoom)(io, queryRoom, event, entry, fullDocument, logDebug)];
|
|
467
|
+
case 19:
|
|
468
|
+
_c.sent();
|
|
469
|
+
return [3 /*break*/, 21];
|
|
470
|
+
case 20:
|
|
471
|
+
io.to(queryRoom).emit("sync", event);
|
|
472
|
+
_c.label = 21;
|
|
473
|
+
case 21:
|
|
474
|
+
logDebug("[realtime] Emitted update to ".concat(queryRoom, " (query matched)"));
|
|
475
|
+
return [3 /*break*/, 26];
|
|
476
|
+
case 22:
|
|
477
|
+
removeEvent = __assign(__assign({}, event), { method: "delete" });
|
|
478
|
+
if (!entry) return [3 /*break*/, 24];
|
|
479
|
+
return [4 /*yield*/, (0, exports.emitToAuthorizedRoom)(io, queryRoom, removeEvent, entry, fullDocument, logDebug)];
|
|
480
|
+
case 23:
|
|
481
|
+
_c.sent();
|
|
482
|
+
return [3 /*break*/, 25];
|
|
483
|
+
case 24:
|
|
484
|
+
io.to(queryRoom).emit("sync", removeEvent);
|
|
485
|
+
_c.label = 25;
|
|
486
|
+
case 25:
|
|
487
|
+
logDebug("[realtime] Emitted delete to ".concat(queryRoom, " (query no longer matched)"));
|
|
488
|
+
_c.label = 26;
|
|
489
|
+
case 26:
|
|
490
|
+
querySubscriptions_1_1 = querySubscriptions_1.next();
|
|
491
|
+
return [3 /*break*/, 5];
|
|
492
|
+
case 27: return [3 /*break*/, 30];
|
|
493
|
+
case 28:
|
|
494
|
+
e_3_1 = _c.sent();
|
|
495
|
+
e_3 = { error: e_3_1 };
|
|
496
|
+
return [3 /*break*/, 30];
|
|
497
|
+
case 29:
|
|
498
|
+
try {
|
|
499
|
+
if (querySubscriptions_1_1 && !querySubscriptions_1_1.done && (_b = querySubscriptions_1.return)) _b.call(querySubscriptions_1);
|
|
500
|
+
}
|
|
501
|
+
finally { if (e_3) throw e_3.error; }
|
|
502
|
+
return [7 /*endfinally*/];
|
|
503
|
+
case 30: return [2 /*return*/];
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
}); };
|
|
507
|
+
exports.emitToDocumentAndQueryRooms = emitToDocumentAndQueryRooms;
|
|
508
|
+
/**
|
|
509
|
+
* Start watching MongoDB change streams and emitting real-time events.
|
|
510
|
+
*/
|
|
511
|
+
var startChangeStreamWatcher = function (io, config, debug) {
|
|
512
|
+
var _a, _b, _c, _d;
|
|
513
|
+
if (config === void 0) { config = {}; }
|
|
514
|
+
if (debug === void 0) { debug = false; }
|
|
515
|
+
var logInfo = function (message) {
|
|
516
|
+
if (debug) {
|
|
517
|
+
logger_1.logger.info(message);
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
var logDebug = function (message) {
|
|
521
|
+
if (debug) {
|
|
522
|
+
logger_1.logger.debug(message);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
try {
|
|
526
|
+
logInfo("[realtime] Initializing change stream watcher...");
|
|
527
|
+
var ignored = new Set(__spreadArray(__spreadArray([], __read(DEFAULT_IGNORED_COLLECTIONS), false), __read(((_a = config.ignoredCollections) !== null && _a !== void 0 ? _a : [])), false));
|
|
528
|
+
var ignoredOps_1 = new Set((_b = config.ignoredOperations) !== null && _b !== void 0 ? _b : []);
|
|
529
|
+
// Build the change stream pipeline
|
|
530
|
+
var pipeline = [
|
|
531
|
+
{
|
|
532
|
+
$match: {
|
|
533
|
+
"ns.coll": { $nin: Array.from(ignored) },
|
|
534
|
+
operationType: { $in: ["insert", "update", "replace", "delete"] },
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
$project: {
|
|
539
|
+
documentKey: 1,
|
|
540
|
+
fullDocument: 1,
|
|
541
|
+
ns: 1,
|
|
542
|
+
operationType: 1,
|
|
543
|
+
updateDescription: 1,
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
];
|
|
547
|
+
var nativeDb = mongoose_1.default.connection.db;
|
|
548
|
+
if (!nativeDb) {
|
|
549
|
+
throw new Error("MongoDB connection not available for change stream");
|
|
550
|
+
}
|
|
551
|
+
var options = {
|
|
552
|
+
batchSize: (_c = config.batchSize) !== null && _c !== void 0 ? _c : 50,
|
|
553
|
+
fullDocument: (_d = config.fullDocument) !== null && _d !== void 0 ? _d : "updateLookup",
|
|
554
|
+
fullDocumentBeforeChange: "off",
|
|
555
|
+
// How long the cursor waits for new events before yielding control.
|
|
556
|
+
// Lower values give more responsive updates at the cost of more frequent driver round-trips.
|
|
557
|
+
maxAwaitTimeMS: 1000,
|
|
558
|
+
};
|
|
559
|
+
changeWatcher = nativeDb.watch(pipeline, options);
|
|
560
|
+
if (!changeWatcher) {
|
|
561
|
+
throw new Error("Failed to create change stream watcher");
|
|
562
|
+
}
|
|
563
|
+
changeWatcher.on("change", function (rawChange) { return __awaiter(void 0, void 0, void 0, function () {
|
|
564
|
+
var change, collectionName, docId, entry, method, fullDocument, isHardDelete, rooms, collectionTag, collection, event_1, rooms_1, rooms_1_1, room, e_4_1, metadata, error_4;
|
|
565
|
+
var e_4, _a;
|
|
566
|
+
var _b, _c, _d, _e;
|
|
567
|
+
return __generator(this, function (_f) {
|
|
568
|
+
switch (_f.label) {
|
|
569
|
+
case 0:
|
|
570
|
+
_f.trys.push([0, 10, , 11]);
|
|
571
|
+
// The pipeline restricts operationType to a subset that always has ns/documentKey;
|
|
572
|
+
// narrow once here so downstream code doesn't need repeated casts.
|
|
573
|
+
if (rawChange.operationType !== "insert" &&
|
|
574
|
+
rawChange.operationType !== "update" &&
|
|
575
|
+
rawChange.operationType !== "replace" &&
|
|
576
|
+
rawChange.operationType !== "delete") {
|
|
577
|
+
return [2 /*return*/];
|
|
578
|
+
}
|
|
579
|
+
change = rawChange;
|
|
580
|
+
collectionName = (_b = change.ns) === null || _b === void 0 ? void 0 : _b.coll;
|
|
581
|
+
docId = (_d = (_c = change.documentKey) === null || _c === void 0 ? void 0 : _c._id) === null || _d === void 0 ? void 0 : _d.toString();
|
|
582
|
+
if (!collectionName || !docId) {
|
|
583
|
+
return [2 /*return*/];
|
|
584
|
+
}
|
|
585
|
+
// Check if this operation type is ignored
|
|
586
|
+
if (ignoredOps_1.has(change.operationType)) {
|
|
587
|
+
return [2 /*return*/];
|
|
588
|
+
}
|
|
589
|
+
entry = (0, registry_1.findRegistryEntryByCollection)(collectionName);
|
|
590
|
+
if (!entry) {
|
|
591
|
+
// Not a registered realtime model — skip
|
|
592
|
+
logDebug("[realtime] No registry entry for collection: ".concat(collectionName));
|
|
593
|
+
return [2 /*return*/];
|
|
594
|
+
}
|
|
595
|
+
method = (0, exports.mapOperationType)(change.operationType, change, entry.config.methods);
|
|
596
|
+
if (!method) {
|
|
597
|
+
return [2 /*return*/];
|
|
598
|
+
}
|
|
599
|
+
// Check if this method is enabled for this model
|
|
600
|
+
if (!entry.config.methods.includes(method)) {
|
|
601
|
+
logDebug("[realtime] Method ".concat(method, " not enabled for ").concat(entry.modelName));
|
|
602
|
+
return [2 /*return*/];
|
|
603
|
+
}
|
|
604
|
+
fullDocument = change.operationType === "delete" ? undefined : change.fullDocument;
|
|
605
|
+
isHardDelete = method === "delete" && !fullDocument;
|
|
606
|
+
rooms = void 0;
|
|
607
|
+
if (isHardDelete) {
|
|
608
|
+
// Hard delete: no fullDocument, so we can't resolve owner/custom rooms.
|
|
609
|
+
// For owner strategy we cannot safely fan out to a model room without
|
|
610
|
+
// leaking deletes across users — admins still receive the event via the
|
|
611
|
+
// admin room and any document-specific subscribers via document rooms.
|
|
612
|
+
if (entry.config.roomStrategy === "owner") {
|
|
613
|
+
rooms = ["admin"];
|
|
614
|
+
}
|
|
615
|
+
else if (entry.config.roomStrategy === "broadcast") {
|
|
616
|
+
rooms = ["authenticated"];
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
collectionTag = getCollectionTag(entry.routePath);
|
|
620
|
+
rooms = ["model:".concat(collectionTag)];
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
rooms = (0, exports.resolveRooms)(entry, fullDocument, method);
|
|
625
|
+
}
|
|
626
|
+
collection = getCollectionTag(entry.routePath);
|
|
627
|
+
event_1 = __assign({ collection: collection, id: docId, method: method, model: entry.modelName, timestamp: luxon_1.DateTime.now().toMillis() }, (change.operationType === "update" && ((_e = change.updateDescription) === null || _e === void 0 ? void 0 : _e.updatedFields)
|
|
628
|
+
? { updatedFields: Object.keys(change.updateDescription.updatedFields) }
|
|
629
|
+
: {}));
|
|
630
|
+
_f.label = 1;
|
|
631
|
+
case 1:
|
|
632
|
+
_f.trys.push([1, 6, 7, 8]);
|
|
633
|
+
rooms_1 = __values(rooms), rooms_1_1 = rooms_1.next();
|
|
634
|
+
_f.label = 2;
|
|
635
|
+
case 2:
|
|
636
|
+
if (!!rooms_1_1.done) return [3 /*break*/, 5];
|
|
637
|
+
room = rooms_1_1.value;
|
|
638
|
+
return [4 /*yield*/, (0, exports.emitToAuthorizedRoom)(io, room, event_1, entry, fullDocument, logDebug)];
|
|
639
|
+
case 3:
|
|
640
|
+
_f.sent();
|
|
641
|
+
_f.label = 4;
|
|
642
|
+
case 4:
|
|
643
|
+
rooms_1_1 = rooms_1.next();
|
|
644
|
+
return [3 /*break*/, 2];
|
|
645
|
+
case 5: return [3 /*break*/, 8];
|
|
646
|
+
case 6:
|
|
647
|
+
e_4_1 = _f.sent();
|
|
648
|
+
e_4 = { error: e_4_1 };
|
|
649
|
+
return [3 /*break*/, 8];
|
|
650
|
+
case 7:
|
|
651
|
+
try {
|
|
652
|
+
if (rooms_1_1 && !rooms_1_1.done && (_a = rooms_1.return)) _a.call(rooms_1);
|
|
653
|
+
}
|
|
654
|
+
finally { if (e_4) throw e_4.error; }
|
|
655
|
+
return [7 /*endfinally*/];
|
|
656
|
+
case 8:
|
|
657
|
+
// Emit to document-specific and query rooms
|
|
658
|
+
return [4 /*yield*/, (0, exports.emitToDocumentAndQueryRooms)(io, collection, event_1, fullDocument, logDebug, entry)];
|
|
659
|
+
case 9:
|
|
660
|
+
// Emit to document-specific and query rooms
|
|
661
|
+
_f.sent();
|
|
662
|
+
logDebug("[realtime] Emitted ".concat(method, " for ").concat(entry.modelName, "/").concat(docId, " to rooms: ").concat(rooms.join(", ")));
|
|
663
|
+
metadata = {
|
|
664
|
+
collection: event_1.collection,
|
|
665
|
+
id: event_1.id,
|
|
666
|
+
method: event_1.method,
|
|
667
|
+
model: event_1.model,
|
|
668
|
+
timestamp: event_1.timestamp,
|
|
669
|
+
};
|
|
670
|
+
if (event_1.updatedFields) {
|
|
671
|
+
metadata.updatedFields = event_1.updatedFields;
|
|
672
|
+
}
|
|
673
|
+
logInfo("[realtime] sync event: ".concat(JSON.stringify(metadata)));
|
|
674
|
+
return [3 /*break*/, 11];
|
|
675
|
+
case 10:
|
|
676
|
+
error_4 = _f.sent();
|
|
677
|
+
logger_1.logger.error("[realtime] Error processing change event: ".concat(error_4));
|
|
678
|
+
Sentry.captureException(error_4);
|
|
679
|
+
return [3 /*break*/, 11];
|
|
680
|
+
case 11: return [2 /*return*/];
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
}); });
|
|
684
|
+
changeWatcher.on("error", function (err) {
|
|
685
|
+
Sentry.captureException(err);
|
|
686
|
+
logger_1.logger.error("[realtime] Change stream error: ".concat((err === null || err === void 0 ? void 0 : err.message) || err));
|
|
687
|
+
});
|
|
688
|
+
changeWatcher.on("close", function () {
|
|
689
|
+
logger_1.logger.warn("[realtime] Change stream closed");
|
|
690
|
+
});
|
|
691
|
+
changeWatcher.on("end", function () {
|
|
692
|
+
logger_1.logger.warn("[realtime] Change stream ended");
|
|
693
|
+
});
|
|
694
|
+
logInfo("[realtime] Change stream watcher initialized successfully");
|
|
695
|
+
}
|
|
696
|
+
catch (error) {
|
|
697
|
+
logger_1.logger.error("[realtime] Failed to initialize change stream watcher: ".concat(error));
|
|
698
|
+
Sentry.captureException(error);
|
|
699
|
+
throw error;
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
exports.startChangeStreamWatcher = startChangeStreamWatcher;
|
|
703
|
+
/**
|
|
704
|
+
* Stop the change stream watcher.
|
|
705
|
+
*/
|
|
706
|
+
var stopChangeStreamWatcher = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
707
|
+
return __generator(this, function (_a) {
|
|
708
|
+
switch (_a.label) {
|
|
709
|
+
case 0:
|
|
710
|
+
if (!changeWatcher) return [3 /*break*/, 2];
|
|
711
|
+
return [4 /*yield*/, changeWatcher.close()];
|
|
712
|
+
case 1:
|
|
713
|
+
_a.sent();
|
|
714
|
+
changeWatcher = null;
|
|
715
|
+
_a.label = 2;
|
|
716
|
+
case 2: return [2 /*return*/];
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
}); };
|
|
720
|
+
exports.stopChangeStreamWatcher = stopChangeStreamWatcher;
|