@terreno/api 0.20.2 → 0.21.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/.ai/guidelines/core.md +71 -0
- package/.ai/skills/mongoose-schema-safety/SKILL.md +143 -0
- package/README.md +54 -1
- package/dist/__tests__/versionCheckPlugin.test.js +29 -7
- package/dist/actions.openApi.test.js +13 -11
- package/dist/api.js +98 -11
- package/dist/api.query.test.js +31 -1
- package/dist/api.test.js +211 -0
- package/dist/auth.test.js +10 -10
- package/dist/betterAuth.d.ts +1 -1
- package/dist/consentApp.test.js +1 -0
- package/dist/example.js +4 -4
- package/dist/expressServer.d.ts +0 -22
- package/dist/expressServer.js +1 -125
- package/dist/expressServer.test.js +90 -91
- package/dist/githubAuth.test.js +22 -22
- package/dist/logger.d.ts +154 -0
- package/dist/logger.js +445 -26
- package/dist/logger.test.js +435 -0
- package/dist/middleware.d.ts +7 -0
- package/dist/middleware.js +58 -1
- package/dist/middleware.test.js +159 -0
- package/dist/openApi.test.js +10 -17
- package/dist/openApiBuilder.test.js +18 -10
- package/dist/realtime/changeStreamWatcher.d.ts +4 -4
- package/dist/realtime/changeStreamWatcher.js +2 -4
- package/dist/realtime/queryMatcher.d.ts +1 -1
- package/dist/realtime/queryMatcher.js +39 -14
- package/dist/realtime/types.d.ts +3 -3
- package/dist/requestContext.d.ts +61 -0
- package/dist/requestContext.js +74 -0
- package/dist/secretProviders.test.js +335 -0
- package/dist/terrenoApp.d.ts +27 -15
- package/dist/terrenoApp.js +24 -14
- package/dist/terrenoApp.test.js +52 -0
- package/dist/tests/bunSetup.js +61 -7
- package/dist/tests.js +27 -4
- package/package.json +1 -1
- package/src/__tests__/versionCheckPlugin.test.ts +43 -15
- package/src/actions.openApi.test.ts +12 -10
- package/src/api.query.test.ts +24 -1
- package/src/api.test.ts +169 -0
- package/src/api.ts +71 -0
- package/src/auth.test.ts +10 -10
- package/src/betterAuth.ts +1 -1
- package/src/consentApp.test.ts +1 -0
- package/src/example.ts +4 -4
- package/src/expressServer.test.ts +82 -85
- package/src/expressServer.ts +1 -213
- package/src/githubAuth.test.ts +22 -22
- package/src/logger.test.ts +466 -1
- package/src/logger.ts +477 -14
- package/src/middleware.test.ts +74 -2
- package/src/middleware.ts +57 -0
- package/src/openApi.test.ts +10 -17
- package/src/openApiBuilder.test.ts +18 -10
- package/src/realtime/changeStreamWatcher.ts +15 -10
- package/src/realtime/queryMatcher.ts +54 -27
- package/src/realtime/types.ts +4 -4
- package/src/requestContext.ts +86 -0
- package/src/secretProviders.test.ts +219 -1
- package/src/terrenoApp.test.ts +38 -0
- package/src/terrenoApp.ts +37 -15
- package/src/tests/bunSetup.ts +16 -3
- package/src/tests.ts +17 -4
package/dist/middleware.test.js
CHANGED
|
@@ -32,10 +32,52 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
45
|
+
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);
|
|
46
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
47
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
48
|
+
function step(op) {
|
|
49
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
50
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
51
|
+
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;
|
|
52
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
53
|
+
switch (op[0]) {
|
|
54
|
+
case 0: case 1: t = op; break;
|
|
55
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
56
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
57
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
58
|
+
default:
|
|
59
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
60
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
61
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
62
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
63
|
+
if (t[2]) _.ops.pop();
|
|
64
|
+
_.trys.pop(); continue;
|
|
65
|
+
}
|
|
66
|
+
op = body.call(thisArg, _);
|
|
67
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
68
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
72
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
73
|
+
};
|
|
35
74
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
75
|
var bun_test_1 = require("bun:test");
|
|
37
76
|
var Sentry = __importStar(require("@sentry/bun"));
|
|
77
|
+
var express_1 = __importDefault(require("express"));
|
|
78
|
+
var supertest_1 = __importDefault(require("supertest"));
|
|
38
79
|
var middleware_1 = require("./middleware");
|
|
80
|
+
var requestContext_1 = require("./requestContext");
|
|
39
81
|
var buildReq = function (headers) {
|
|
40
82
|
return {
|
|
41
83
|
get: function (name) { return headers[name]; },
|
|
@@ -80,3 +122,120 @@ var buildNext = function () { return (0, bun_test_1.mock)(function () { }); };
|
|
|
80
122
|
(0, bun_test_1.expect)(next.mock.calls[0]).toHaveLength(0);
|
|
81
123
|
});
|
|
82
124
|
});
|
|
125
|
+
(0, bun_test_1.describe)("jsonResponseRequestIdMiddleware", function () {
|
|
126
|
+
var buildStackedApp = function () {
|
|
127
|
+
var app = (0, express_1.default)();
|
|
128
|
+
app.use(requestContext_1.requestContextMiddleware);
|
|
129
|
+
app.use(middleware_1.jsonResponseRequestIdMiddleware);
|
|
130
|
+
app.get("/object", function (_req, res) {
|
|
131
|
+
return res.json({ hello: "world" });
|
|
132
|
+
});
|
|
133
|
+
app.get("/array", function (_req, res) {
|
|
134
|
+
return res.json([1, 2]);
|
|
135
|
+
});
|
|
136
|
+
app.get("/openapi.json", function (_req, res) {
|
|
137
|
+
return res.json({ openapi: "3.0.0", paths: {} });
|
|
138
|
+
});
|
|
139
|
+
app.get("/openapi/components/schemas/Food.json", function (_req, res) {
|
|
140
|
+
return res.json({ description: "A food", type: "object" });
|
|
141
|
+
});
|
|
142
|
+
app.get("/openapi/validate", function (_req, res) {
|
|
143
|
+
return res.json({ document: { openapi: "3.0.0" }, valid: true });
|
|
144
|
+
});
|
|
145
|
+
return app;
|
|
146
|
+
};
|
|
147
|
+
(0, bun_test_1.it)("adds requestId to object JSON bodies and matches X-Request-ID header", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
148
|
+
var app, res;
|
|
149
|
+
return __generator(this, function (_a) {
|
|
150
|
+
switch (_a.label) {
|
|
151
|
+
case 0:
|
|
152
|
+
app = buildStackedApp();
|
|
153
|
+
return [4 /*yield*/, (0, supertest_1.default)(app).get("/object").expect(200)];
|
|
154
|
+
case 1:
|
|
155
|
+
res = _a.sent();
|
|
156
|
+
(0, bun_test_1.expect)(res.body.hello).toBe("world");
|
|
157
|
+
(0, bun_test_1.expect)(res.body.requestId).toBeDefined();
|
|
158
|
+
(0, bun_test_1.expect)(res.body.requestId).toBe(res.headers["x-request-id"]);
|
|
159
|
+
return [2 /*return*/];
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}); });
|
|
163
|
+
(0, bun_test_1.it)("does not wrap JSON array bodies", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
164
|
+
var app, res;
|
|
165
|
+
return __generator(this, function (_a) {
|
|
166
|
+
switch (_a.label) {
|
|
167
|
+
case 0:
|
|
168
|
+
app = buildStackedApp();
|
|
169
|
+
return [4 /*yield*/, (0, supertest_1.default)(app).get("/array").expect(200)];
|
|
170
|
+
case 1:
|
|
171
|
+
res = _a.sent();
|
|
172
|
+
(0, bun_test_1.expect)(res.body).toEqual([1, 2]);
|
|
173
|
+
(0, bun_test_1.expect)(res.headers["x-request-id"]).toBeDefined();
|
|
174
|
+
return [2 /*return*/];
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}); });
|
|
178
|
+
(0, bun_test_1.it)("does not inject requestId into GET /openapi.json bodies", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
179
|
+
var app, res;
|
|
180
|
+
return __generator(this, function (_a) {
|
|
181
|
+
switch (_a.label) {
|
|
182
|
+
case 0:
|
|
183
|
+
app = buildStackedApp();
|
|
184
|
+
return [4 /*yield*/, (0, supertest_1.default)(app).get("/openapi.json").expect(200)];
|
|
185
|
+
case 1:
|
|
186
|
+
res = _a.sent();
|
|
187
|
+
(0, bun_test_1.expect)(res.body).toEqual({ openapi: "3.0.0", paths: {} });
|
|
188
|
+
(0, bun_test_1.expect)(res.body.requestId).toBeUndefined();
|
|
189
|
+
return [2 /*return*/];
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}); });
|
|
193
|
+
(0, bun_test_1.it)("does not inject requestId into GET /openapi/components/...json bodies", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
194
|
+
var app, res;
|
|
195
|
+
return __generator(this, function (_a) {
|
|
196
|
+
switch (_a.label) {
|
|
197
|
+
case 0:
|
|
198
|
+
app = buildStackedApp();
|
|
199
|
+
return [4 /*yield*/, (0, supertest_1.default)(app).get("/openapi/components/schemas/Food.json").expect(200)];
|
|
200
|
+
case 1:
|
|
201
|
+
res = _a.sent();
|
|
202
|
+
(0, bun_test_1.expect)(res.body).toEqual({ description: "A food", type: "object" });
|
|
203
|
+
(0, bun_test_1.expect)(res.body.requestId).toBeUndefined();
|
|
204
|
+
return [2 /*return*/];
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}); });
|
|
208
|
+
(0, bun_test_1.it)("does not inject requestId into GET /openapi/validate bodies", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
209
|
+
var app, res;
|
|
210
|
+
return __generator(this, function (_a) {
|
|
211
|
+
switch (_a.label) {
|
|
212
|
+
case 0:
|
|
213
|
+
app = buildStackedApp();
|
|
214
|
+
return [4 /*yield*/, (0, supertest_1.default)(app).get("/openapi/validate").expect(200)];
|
|
215
|
+
case 1:
|
|
216
|
+
res = _a.sent();
|
|
217
|
+
(0, bun_test_1.expect)(res.body).toEqual({ document: { openapi: "3.0.0" }, valid: true });
|
|
218
|
+
(0, bun_test_1.expect)(res.body.requestId).toBeUndefined();
|
|
219
|
+
return [2 /*return*/];
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}); });
|
|
223
|
+
(0, bun_test_1.it)("uses incoming X-Request-ID on wrapped object responses", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
224
|
+
var app, res;
|
|
225
|
+
return __generator(this, function (_a) {
|
|
226
|
+
switch (_a.label) {
|
|
227
|
+
case 0:
|
|
228
|
+
app = buildStackedApp();
|
|
229
|
+
return [4 /*yield*/, (0, supertest_1.default)(app)
|
|
230
|
+
.get("/object")
|
|
231
|
+
.set("X-Request-ID", "client-rid-99")
|
|
232
|
+
.expect(200)];
|
|
233
|
+
case 1:
|
|
234
|
+
res = _a.sent();
|
|
235
|
+
(0, bun_test_1.expect)(res.body.requestId).toBe("client-rid-99");
|
|
236
|
+
(0, bun_test_1.expect)(res.headers["x-request-id"]).toBe("client-rid-99");
|
|
237
|
+
return [2 /*return*/];
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}); });
|
|
241
|
+
});
|
package/dist/openApi.test.js
CHANGED
|
@@ -70,10 +70,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
70
70
|
var bun_test_1 = require("bun:test");
|
|
71
71
|
var supertest_1 = __importDefault(require("supertest"));
|
|
72
72
|
var api_1 = require("./api");
|
|
73
|
-
var auth_1 = require("./auth");
|
|
74
|
-
var expressServer_1 = require("./expressServer");
|
|
75
73
|
var openApi_1 = require("./openApi");
|
|
76
74
|
var permissions_1 = require("./permissions");
|
|
75
|
+
var terrenoApp_1 = require("./terrenoApp");
|
|
77
76
|
var tests_1 = require("./tests");
|
|
78
77
|
function getMessageSummaryOpenApiMiddleware(options) {
|
|
79
78
|
if (!options.openApi) {
|
|
@@ -136,13 +135,11 @@ function addRoutes(router, options) {
|
|
|
136
135
|
return __generator(this, function (_a) {
|
|
137
136
|
process.env.REFRESH_TOKEN_SECRET = "testsecret1234";
|
|
138
137
|
process.env.ENABLE_SWAGGER = "true";
|
|
139
|
-
app =
|
|
140
|
-
|
|
138
|
+
app = new terrenoApp_1.TerrenoApp({
|
|
139
|
+
configureApp: addRoutes,
|
|
141
140
|
skipListen: true,
|
|
142
141
|
userModel: tests_1.UserModel,
|
|
143
|
-
});
|
|
144
|
-
(0, auth_1.setupAuth)(app, tests_1.UserModel);
|
|
145
|
-
(0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
|
|
142
|
+
}).build();
|
|
146
143
|
return [2 /*return*/];
|
|
147
144
|
});
|
|
148
145
|
}); });
|
|
@@ -342,13 +339,11 @@ function addRoutesPopulate(router, options) {
|
|
|
342
339
|
return __generator(this, function (_a) {
|
|
343
340
|
process.env.REFRESH_TOKEN_SECRET = "testsecret1234";
|
|
344
341
|
process.env.ENABLE_SWAGGER = "false";
|
|
345
|
-
app =
|
|
346
|
-
|
|
342
|
+
app = new terrenoApp_1.TerrenoApp({
|
|
343
|
+
configureApp: addRoutes,
|
|
347
344
|
skipListen: true,
|
|
348
345
|
userModel: tests_1.UserModel,
|
|
349
|
-
});
|
|
350
|
-
(0, auth_1.setupAuth)(app, tests_1.UserModel);
|
|
351
|
-
(0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
|
|
346
|
+
}).build();
|
|
352
347
|
return [2 /*return*/];
|
|
353
348
|
});
|
|
354
349
|
}); });
|
|
@@ -371,13 +366,11 @@ function addRoutesPopulate(router, options) {
|
|
|
371
366
|
(0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
372
367
|
return __generator(this, function (_a) {
|
|
373
368
|
process.env.REFRESH_TOKEN_SECRET = "testsecret1234";
|
|
374
|
-
app =
|
|
375
|
-
|
|
369
|
+
app = new terrenoApp_1.TerrenoApp({
|
|
370
|
+
configureApp: addRoutesPopulate,
|
|
376
371
|
skipListen: true,
|
|
377
372
|
userModel: tests_1.UserModel,
|
|
378
|
-
});
|
|
379
|
-
(0, auth_1.setupAuth)(app, tests_1.UserModel);
|
|
380
|
-
(0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
|
|
373
|
+
}).build();
|
|
381
374
|
return [2 /*return*/];
|
|
382
375
|
});
|
|
383
376
|
}); });
|
|
@@ -54,10 +54,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
54
54
|
var bun_test_1 = require("bun:test");
|
|
55
55
|
var supertest_1 = __importDefault(require("supertest"));
|
|
56
56
|
var api_1 = require("./api");
|
|
57
|
-
var auth_1 = require("./auth");
|
|
58
|
-
var expressServer_1 = require("./expressServer");
|
|
59
57
|
var openApiBuilder_1 = require("./openApiBuilder");
|
|
60
58
|
var permissions_1 = require("./permissions");
|
|
59
|
+
var terrenoApp_1 = require("./terrenoApp");
|
|
61
60
|
var tests_1 = require("./tests");
|
|
62
61
|
function addRoutesWithBuilder(router, options) {
|
|
63
62
|
var _this = this;
|
|
@@ -168,13 +167,11 @@ function addRoutesWithBuilder(router, options) {
|
|
|
168
167
|
return __generator(this, function (_a) {
|
|
169
168
|
process.env.REFRESH_TOKEN_SECRET = "testsecret1234";
|
|
170
169
|
process.env.ENABLE_SWAGGER = "true";
|
|
171
|
-
app =
|
|
172
|
-
|
|
170
|
+
app = new terrenoApp_1.TerrenoApp({
|
|
171
|
+
configureApp: addRoutesWithBuilder,
|
|
173
172
|
skipListen: true,
|
|
174
173
|
userModel: tests_1.UserModel,
|
|
175
|
-
});
|
|
176
|
-
(0, auth_1.setupAuth)(app, tests_1.UserModel);
|
|
177
|
-
(0, auth_1.addAuthRoutes)(app, tests_1.UserModel);
|
|
174
|
+
}).build();
|
|
178
175
|
return [2 /*return*/];
|
|
179
176
|
});
|
|
180
177
|
}); });
|
|
@@ -367,7 +364,11 @@ function addRoutesWithBuilder(router, options) {
|
|
|
367
364
|
return [4 /*yield*/, server.get("/food/stats").expect(200)];
|
|
368
365
|
case 1:
|
|
369
366
|
res = _a.sent();
|
|
370
|
-
(0, bun_test_1.expect)(res.body).toEqual({
|
|
367
|
+
(0, bun_test_1.expect)(res.body).toEqual({
|
|
368
|
+
avgCalories: 250,
|
|
369
|
+
count: 10,
|
|
370
|
+
requestId: res.headers["x-request-id"],
|
|
371
|
+
});
|
|
371
372
|
return [2 /*return*/];
|
|
372
373
|
}
|
|
373
374
|
});
|
|
@@ -384,7 +385,10 @@ function addRoutesWithBuilder(router, options) {
|
|
|
384
385
|
.expect(201)];
|
|
385
386
|
case 1:
|
|
386
387
|
res = _a.sent();
|
|
387
|
-
(0, bun_test_1.expect)(res.body).toEqual({
|
|
388
|
+
(0, bun_test_1.expect)(res.body).toEqual({
|
|
389
|
+
reportId: "report-123",
|
|
390
|
+
requestId: res.headers["x-request-id"],
|
|
391
|
+
});
|
|
388
392
|
return [2 /*return*/];
|
|
389
393
|
}
|
|
390
394
|
});
|
|
@@ -414,7 +418,11 @@ function addRoutesWithBuilder(router, options) {
|
|
|
414
418
|
return [4 /*yield*/, server.get("/food/categories/cat-123").expect(200)];
|
|
415
419
|
case 1:
|
|
416
420
|
res = _a.sent();
|
|
417
|
-
(0, bun_test_1.expect)(res.body).toEqual({
|
|
421
|
+
(0, bun_test_1.expect)(res.body).toEqual({
|
|
422
|
+
id: "cat-123",
|
|
423
|
+
name: "Fruits",
|
|
424
|
+
requestId: res.headers["x-request-id"],
|
|
425
|
+
});
|
|
418
426
|
return [2 /*return*/];
|
|
419
427
|
}
|
|
420
428
|
});
|
|
@@ -21,7 +21,7 @@ export declare const mapOperationType: (operationType: string, change: ChangeStr
|
|
|
21
21
|
* Determine which Socket.io rooms to emit to based on the room strategy.
|
|
22
22
|
* Exported for testing.
|
|
23
23
|
*/
|
|
24
|
-
export declare const resolveRooms: (entry: RealtimeRegistryEntry, doc:
|
|
24
|
+
export declare const resolveRooms: (entry: RealtimeRegistryEntry, doc: Record<string, unknown>, method: string) => string[];
|
|
25
25
|
/**
|
|
26
26
|
* Ensure serialized documents include `id` to match REST API responses.
|
|
27
27
|
* Change stream fullDocument payloads are raw BSON objects with `_id` only.
|
|
@@ -43,8 +43,8 @@ export declare const ensureApiId: (data: unknown) => unknown;
|
|
|
43
43
|
* would risk leaking unsanitized fields (e.g. `hash`/`salt`) that the handler
|
|
44
44
|
* was supposed to strip.
|
|
45
45
|
*/
|
|
46
|
-
export declare const serializeDoc: (entry: RealtimeRegistryEntry, doc:
|
|
47
|
-
export declare const emitToAuthorizedRoom: (io: Server, room: string, event: RealtimeEvent, entry: RealtimeRegistryEntry, fullDocument:
|
|
46
|
+
export declare const serializeDoc: (entry: RealtimeRegistryEntry, doc: Record<string, unknown>, method: "create" | "update" | "delete", user?: User) => Promise<unknown>;
|
|
47
|
+
export declare const emitToAuthorizedRoom: (io: Server, room: string, event: RealtimeEvent, entry: RealtimeRegistryEntry, fullDocument: Record<string, unknown> | undefined, logDebug: (msg: string) => void) => Promise<void>;
|
|
48
48
|
/**
|
|
49
49
|
* Emit a sync event to document-specific and query rooms.
|
|
50
50
|
*
|
|
@@ -61,7 +61,7 @@ export declare const emitToAuthorizedRoom: (io: Server, room: string, event: Rea
|
|
|
61
61
|
*
|
|
62
62
|
* Exported for testing.
|
|
63
63
|
*/
|
|
64
|
-
export declare const emitToDocumentAndQueryRooms: (io: Server, collection: string, event: RealtimeEvent, fullDocument:
|
|
64
|
+
export declare const emitToDocumentAndQueryRooms: (io: Server, collection: string, event: RealtimeEvent, fullDocument: Record<string, unknown> | undefined, logDebug: (msg: string) => void, entry?: RealtimeRegistryEntry) => Promise<void>;
|
|
65
65
|
/**
|
|
66
66
|
* Start watching MongoDB change streams and emitting real-time events.
|
|
67
67
|
*/
|
|
@@ -120,7 +120,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
120
120
|
};
|
|
121
121
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
122
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
123
|
var Sentry = __importStar(require("@sentry/bun"));
|
|
125
124
|
var luxon_1 = require("luxon");
|
|
126
125
|
var mongoose_1 = __importDefault(require("mongoose"));
|
|
@@ -208,7 +207,6 @@ var canReadDocument = function (entry, user, doc) { return __awaiter(void 0, voi
|
|
|
208
207
|
* Exported for testing.
|
|
209
208
|
*/
|
|
210
209
|
var resolveRooms = function (entry, doc, method) {
|
|
211
|
-
var _a, _b, _c;
|
|
212
210
|
var roomStrategy = entry.config.roomStrategy;
|
|
213
211
|
// Use the collection tag (e.g. "todos") for model rooms, matching what the frontend subscribes to
|
|
214
212
|
var collectionTag = getCollectionTag(entry.routePath);
|
|
@@ -219,7 +217,7 @@ var resolveRooms = function (entry, doc, method) {
|
|
|
219
217
|
}
|
|
220
218
|
switch (roomStrategy) {
|
|
221
219
|
case "owner": {
|
|
222
|
-
var ownerId = (
|
|
220
|
+
var ownerId = (doc === null || doc === void 0 ? void 0 : doc.ownerId) != null ? String(doc.ownerId) : undefined;
|
|
223
221
|
if (ownerId) {
|
|
224
222
|
return ["user:".concat(ownerId)];
|
|
225
223
|
}
|
|
@@ -625,7 +623,7 @@ var startChangeStreamWatcher = function (io, config, debug) {
|
|
|
625
623
|
}
|
|
626
624
|
}
|
|
627
625
|
else {
|
|
628
|
-
rooms = (0, exports.resolveRooms)(entry, fullDocument, method);
|
|
626
|
+
rooms = (0, exports.resolveRooms)(entry, fullDocument !== null && fullDocument !== void 0 ? fullDocument : {}, method);
|
|
629
627
|
}
|
|
630
628
|
collection = getCollectionTag(entry.routePath);
|
|
631
629
|
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)
|
|
@@ -11,4 +11,4 @@
|
|
|
11
11
|
* @param query - MongoDB-style query object
|
|
12
12
|
* @returns true if the document matches all query conditions
|
|
13
13
|
*/
|
|
14
|
-
export declare const matchesQuery: (doc:
|
|
14
|
+
export declare const matchesQuery: (doc: Record<string, unknown>, query: Record<string, unknown>) => boolean;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// biome-ignore-all lint/suspicious/noExplicitAny: MongoDB query matcher evaluates dynamic filter shapes
|
|
3
2
|
/**
|
|
4
3
|
* Simple in-memory MongoDB query matcher.
|
|
5
4
|
* Evaluates a MongoDB-style query object against a document without hitting the database.
|
|
@@ -63,14 +62,32 @@ var normalize = function (value) {
|
|
|
63
62
|
return value;
|
|
64
63
|
}
|
|
65
64
|
// Handle ObjectId-like objects with toString
|
|
66
|
-
if (typeof value === "object" &&
|
|
67
|
-
|
|
68
|
-
(
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
66
|
+
var obj = value;
|
|
67
|
+
var ctorName = (_a = obj.constructor) === null || _a === void 0 ? void 0 : _a.name;
|
|
68
|
+
if (typeof obj.toString === "function" && ctorName !== "Object") {
|
|
69
|
+
return String(value);
|
|
70
|
+
}
|
|
71
71
|
}
|
|
72
72
|
return value;
|
|
73
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* JS abstract relational comparison on unknown values.
|
|
76
|
+
* Numeric operands compare numerically; everything else compares as strings.
|
|
77
|
+
* This mirrors the coercion behaviour of `>` / `<` on the `any`-typed values
|
|
78
|
+
* that MongoDB in-memory matching historically received.
|
|
79
|
+
*/
|
|
80
|
+
var compareValues = function (a, b) {
|
|
81
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
82
|
+
return a - b;
|
|
83
|
+
}
|
|
84
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
85
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
86
|
+
}
|
|
87
|
+
var numA = Number(a);
|
|
88
|
+
var numB = Number(b);
|
|
89
|
+
return numA - numB;
|
|
90
|
+
};
|
|
74
91
|
var matchesCondition = function (rawValue, condition) {
|
|
75
92
|
var e_2, _a;
|
|
76
93
|
var value = normalize(rawValue);
|
|
@@ -99,26 +116,34 @@ var matchesCondition = function (rawValue, condition) {
|
|
|
99
116
|
return false;
|
|
100
117
|
}
|
|
101
118
|
break;
|
|
102
|
-
case "$gt":
|
|
103
|
-
|
|
119
|
+
case "$gt": {
|
|
120
|
+
var cmp = compareValues(value, normOp);
|
|
121
|
+
if (Number.isNaN(cmp) || cmp <= 0) {
|
|
104
122
|
return false;
|
|
105
123
|
}
|
|
106
124
|
break;
|
|
107
|
-
|
|
108
|
-
|
|
125
|
+
}
|
|
126
|
+
case "$gte": {
|
|
127
|
+
var cmp = compareValues(value, normOp);
|
|
128
|
+
if (Number.isNaN(cmp) || cmp < 0) {
|
|
109
129
|
return false;
|
|
110
130
|
}
|
|
111
131
|
break;
|
|
112
|
-
|
|
113
|
-
|
|
132
|
+
}
|
|
133
|
+
case "$lt": {
|
|
134
|
+
var cmp = compareValues(value, normOp);
|
|
135
|
+
if (Number.isNaN(cmp) || cmp >= 0) {
|
|
114
136
|
return false;
|
|
115
137
|
}
|
|
116
138
|
break;
|
|
117
|
-
|
|
118
|
-
|
|
139
|
+
}
|
|
140
|
+
case "$lte": {
|
|
141
|
+
var cmp = compareValues(value, normOp);
|
|
142
|
+
if (Number.isNaN(cmp) || cmp > 0) {
|
|
119
143
|
return false;
|
|
120
144
|
}
|
|
121
145
|
break;
|
|
146
|
+
}
|
|
122
147
|
case "$in": {
|
|
123
148
|
if (!Array.isArray(operand)) {
|
|
124
149
|
return false;
|
package/dist/realtime/types.d.ts
CHANGED
|
@@ -13,9 +13,9 @@ export interface RealtimeConfig {
|
|
|
13
13
|
* - 'broadcast': emit to all authenticated sockets
|
|
14
14
|
* - function: custom room resolver returning room name(s)
|
|
15
15
|
*/
|
|
16
|
-
roomStrategy: "owner" | "model" | "broadcast" | ((doc:
|
|
16
|
+
roomStrategy: "owner" | "model" | "broadcast" | ((doc: Record<string, unknown>, method: string, req: express.Request) => string[]);
|
|
17
17
|
/** Custom serializer for real-time events. Falls back to the modelRouter responseHandler. */
|
|
18
|
-
realtimeResponseHandler?: (doc:
|
|
18
|
+
realtimeResponseHandler?: (doc: Record<string, unknown>, method: string) => unknown;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* A real-time sync event emitted to clients via WebSocket.
|
|
@@ -94,7 +94,7 @@ export interface QuerySubscription {
|
|
|
94
94
|
/** Collection tag (e.g. "todos") */
|
|
95
95
|
collection: string;
|
|
96
96
|
/** MongoDB-style query filter (e.g. {completed: false}) */
|
|
97
|
-
query: Record<string,
|
|
97
|
+
query: Record<string, unknown>;
|
|
98
98
|
/** Client-provided queryId (ignored — server computes a canonical ID) */
|
|
99
99
|
queryId?: string;
|
|
100
100
|
}
|
package/dist/requestContext.d.ts
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
import type express from "express";
|
|
2
2
|
import type { JwtPayload } from "jsonwebtoken";
|
|
3
|
+
/**
|
|
4
|
+
* Correlation fields stored in AsyncLocalStorage for the lifetime of a request or job. Every log
|
|
5
|
+
* line emitted inside the scope is enriched with these. `requestId` is the only required field; the
|
|
6
|
+
* rest are populated when headers, trace context, or auth supply them.
|
|
7
|
+
*/
|
|
3
8
|
export interface RequestContext {
|
|
9
|
+
/** Background job identifier (from `x-job-id` or set via {@link runWithRequestContext}). */
|
|
4
10
|
jobId?: string;
|
|
11
|
+
/** Stable id shared by all log lines for one request/job; echoed to clients as `X-Request-ID`. */
|
|
5
12
|
requestId: string;
|
|
13
|
+
/** Auth session id, resolved from the JWT/Better Auth session or `x-session-id`. */
|
|
6
14
|
sessionId?: string;
|
|
15
|
+
/** Distributed-tracing span id, parsed from Cloud Trace or W3C `traceparent`. */
|
|
7
16
|
spanId?: string;
|
|
17
|
+
/** Distributed-tracing trace id, parsed from Cloud Trace or W3C `traceparent`. */
|
|
8
18
|
traceId?: string;
|
|
19
|
+
/** Whether the trace is sampled, per the incoming trace headers. */
|
|
9
20
|
traceSampled?: boolean;
|
|
21
|
+
/** Authenticated user id, populated after auth middleware runs. */
|
|
10
22
|
userId?: string;
|
|
11
23
|
}
|
|
12
24
|
export type RequestContextAttributes = Record<string, string>;
|
|
25
|
+
/**
|
|
26
|
+
* Canonical HTTP header names for each correlation field. Use these to propagate context to
|
|
27
|
+
* downstream services (pair with {@link getCurrentRequestContextAttributes}) or to read it from an
|
|
28
|
+
* incoming request (pair with {@link getRequestContextFromAttributes}).
|
|
29
|
+
*/
|
|
13
30
|
export declare const REQUEST_CONTEXT_ATTRIBUTE_NAMES: {
|
|
14
31
|
readonly jobId: "x-job-id";
|
|
15
32
|
readonly requestId: "x-request-id";
|
|
@@ -26,12 +43,56 @@ export interface JwtSessionPayload extends JwtPayload {
|
|
|
26
43
|
}
|
|
27
44
|
export declare const getSessionIdFromJwtPayload: (payload?: JwtSessionPayload | null) => string | undefined;
|
|
28
45
|
export declare const getRequestContextFromAttributes: (attributes?: Record<string, string | undefined>) => RequestContext;
|
|
46
|
+
/**
|
|
47
|
+
* Returns the full {@link RequestContext} for the active AsyncLocalStorage scope, or `undefined`
|
|
48
|
+
* when called outside any request/job scope. The logger uses this to enrich each line.
|
|
49
|
+
*/
|
|
29
50
|
export declare const getCurrentRequestContext: () => RequestContext | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Returns the active correlation fields as a plain object (empty when outside a scope). This is the
|
|
53
|
+
* shape attached to Sentry log attributes and is handy when you need to log or forward the current
|
|
54
|
+
* context yourself.
|
|
55
|
+
*/
|
|
30
56
|
export declare const getCurrentLogContext: () => Partial<RequestContext>;
|
|
31
57
|
export declare const applyRequestContextToSentry: (context?: Partial<RequestContext>) => void;
|
|
32
58
|
export declare const setRequestContext: (updates: Partial<RequestContext>) => void;
|
|
59
|
+
/**
|
|
60
|
+
* Serializes the active correlation context into HTTP header attributes (keyed by
|
|
61
|
+
* {@link REQUEST_CONTEXT_ATTRIBUTE_NAMES}) so it can be propagated on outbound requests to other
|
|
62
|
+
* services, keeping the same `requestId`/`traceId` across service boundaries.
|
|
63
|
+
*/
|
|
33
64
|
export declare const getCurrentRequestContextAttributes: (overrides?: Partial<RequestContext>) => RequestContextAttributes;
|
|
65
|
+
/**
|
|
66
|
+
* Runs `callback` inside a fresh correlation scope so every log line it emits shares the same
|
|
67
|
+
* identifiers — the manual equivalent of {@link requestContextMiddleware} for background jobs,
|
|
68
|
+
* cron tasks, scripts, queue consumers, etc. A `requestId` is generated when not supplied, and the
|
|
69
|
+
* context is mirrored to Sentry.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* import {createScopedLogger, runWithRequestContext} from "@terreno/api";
|
|
74
|
+
*
|
|
75
|
+
* await runWithRequestContext({jobId: "nightly-sync"}, async () => {
|
|
76
|
+
* const log = createScopedLogger({prefix: "[NightlySync]"});
|
|
77
|
+
* log.info("started"); // includes jobId + a generated requestId on every line
|
|
78
|
+
* await sync();
|
|
79
|
+
* });
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
34
82
|
export declare const runWithRequestContext: <T>(context: Partial<RequestContext>, callback: () => T) => T;
|
|
83
|
+
/**
|
|
84
|
+
* Like {@link runWithRequestContext}, but seeds the scope from raw header attributes (for example
|
|
85
|
+
* those received on an incoming message or forwarded by another service). Parses Cloud Trace / W3C
|
|
86
|
+
* `traceparent` into `traceId`/`spanId` via {@link getRequestContextFromAttributes}.
|
|
87
|
+
*/
|
|
35
88
|
export declare const runWithRequestContextAttributes: <T>(attributes: Record<string, string | undefined> | undefined, callback: () => T) => T;
|
|
36
89
|
export declare const updateRequestContextFromRequest: (req: express.Request, res?: express.Response) => void;
|
|
90
|
+
/**
|
|
91
|
+
* Express middleware that opens a correlation scope for the request. Mounted early by `TerrenoApp` /
|
|
92
|
+
* `setupServer`, it resolves a `requestId` (from request-id/correlation headers, Cloud Trace, or
|
|
93
|
+
* W3C `traceparent`, else a new UUID), captures any `jobId`/`sessionId`/trace fields, echoes
|
|
94
|
+
* `X-Request-ID` back to the client, and runs the remaining middleware inside the scope so all
|
|
95
|
+
* downstream logs are correlated. A later auth-aware pass ({@link updateRequestContextFromRequest})
|
|
96
|
+
* fills in `userId`/`sessionId`.
|
|
97
|
+
*/
|
|
37
98
|
export declare const requestContextMiddleware: (req: express.Request, res: express.Response, next: express.NextFunction) => void;
|