@terreno/api 0.20.1 → 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/githubAuth.test.js
CHANGED
|
@@ -79,10 +79,10 @@ var passport_1 = __importDefault(require("passport"));
|
|
|
79
79
|
var passport_local_mongoose_1 = __importDefault(require("passport-local-mongoose"));
|
|
80
80
|
var supertest_1 = __importDefault(require("supertest"));
|
|
81
81
|
var auth_1 = require("./auth");
|
|
82
|
-
var expressServer_1 = require("./expressServer");
|
|
83
82
|
var githubAuth_1 = require("./githubAuth");
|
|
84
83
|
var logger_1 = require("./logger");
|
|
85
84
|
var plugins_1 = require("./plugins");
|
|
85
|
+
var terrenoApp_1 = require("./terrenoApp");
|
|
86
86
|
var fakeGithubOutcome = { type: "redirect", url: "http://github.com/mock" };
|
|
87
87
|
var installFakeGithubStrategy = function () {
|
|
88
88
|
var strategy = {
|
|
@@ -190,8 +190,8 @@ var connectDb = function () { return __awaiter(void 0, void 0, void 0, function
|
|
|
190
190
|
return [4 /*yield*/, testUser.save()];
|
|
191
191
|
case 5:
|
|
192
192
|
_a.sent();
|
|
193
|
-
app =
|
|
194
|
-
|
|
193
|
+
app = new terrenoApp_1.TerrenoApp({
|
|
194
|
+
configureApp: addRoutes,
|
|
195
195
|
githubAuth: {
|
|
196
196
|
allowAccountLinking: true,
|
|
197
197
|
callbackURL: "http://localhost:9000/auth/github/callback",
|
|
@@ -200,7 +200,7 @@ var connectDb = function () { return __awaiter(void 0, void 0, void 0, function
|
|
|
200
200
|
},
|
|
201
201
|
skipListen: true,
|
|
202
202
|
userModel: GitHubTestUserModel,
|
|
203
|
-
});
|
|
203
|
+
}).build();
|
|
204
204
|
agent = supertest_1.default.agent(app);
|
|
205
205
|
return [2 /*return*/];
|
|
206
206
|
}
|
|
@@ -338,11 +338,11 @@ var connectDb = function () { return __awaiter(void 0, void 0, void 0, function
|
|
|
338
338
|
case 2:
|
|
339
339
|
_a.sent();
|
|
340
340
|
// Setup server WITHOUT GitHub auth
|
|
341
|
-
app =
|
|
342
|
-
|
|
341
|
+
app = new terrenoApp_1.TerrenoApp({
|
|
342
|
+
configureApp: addRoutes,
|
|
343
343
|
skipListen: true,
|
|
344
344
|
userModel: GitHubTestUserModel,
|
|
345
|
-
});
|
|
345
|
+
}).build();
|
|
346
346
|
agent = supertest_1.default.agent(app);
|
|
347
347
|
return [2 /*return*/];
|
|
348
348
|
}
|
|
@@ -665,8 +665,8 @@ var invokeGitHubVerify = function (req, accessToken, refreshToken, profile) {
|
|
|
665
665
|
return [4 /*yield*/, GitHubTestUserModel.deleteMany({})];
|
|
666
666
|
case 2:
|
|
667
667
|
_a.sent();
|
|
668
|
-
app =
|
|
669
|
-
|
|
668
|
+
app = new terrenoApp_1.TerrenoApp({
|
|
669
|
+
configureApp: addRoutes,
|
|
670
670
|
githubAuth: {
|
|
671
671
|
allowAccountLinking: true,
|
|
672
672
|
callbackURL: "http://localhost:9000/auth/github/callback",
|
|
@@ -675,7 +675,7 @@ var invokeGitHubVerify = function (req, accessToken, refreshToken, profile) {
|
|
|
675
675
|
},
|
|
676
676
|
skipListen: true,
|
|
677
677
|
userModel: GitHubTestUserModel,
|
|
678
|
-
});
|
|
678
|
+
}).build();
|
|
679
679
|
agent = supertest_1.default.agent(app);
|
|
680
680
|
return [2 /*return*/];
|
|
681
681
|
}
|
|
@@ -767,10 +767,10 @@ var invokeGitHubVerify = function (req, accessToken, refreshToken, profile) {
|
|
|
767
767
|
return [4 /*yield*/, GitHubTestUserModel.deleteMany({})];
|
|
768
768
|
case 2:
|
|
769
769
|
_a.sent();
|
|
770
|
-
app =
|
|
771
|
-
|
|
770
|
+
app = new terrenoApp_1.TerrenoApp({
|
|
771
|
+
beforeJsonSetup: function (a) {
|
|
772
772
|
// The handler reads (req as unknown as {session?: {returnTo?: string}}).session?.returnTo.
|
|
773
|
-
//
|
|
773
|
+
// TerrenoApp does not install express-session, so prime a fake session from a request
|
|
774
774
|
// header for tests.
|
|
775
775
|
a.use(function (req, _res, next) {
|
|
776
776
|
var headerReturnTo = req.headers["x-mock-return-to"];
|
|
@@ -780,7 +780,7 @@ var invokeGitHubVerify = function (req, accessToken, refreshToken, profile) {
|
|
|
780
780
|
next();
|
|
781
781
|
});
|
|
782
782
|
},
|
|
783
|
-
|
|
783
|
+
configureApp: addRoutes,
|
|
784
784
|
githubAuth: {
|
|
785
785
|
allowAccountLinking: true,
|
|
786
786
|
callbackURL: "http://localhost:9000/auth/github/callback",
|
|
@@ -789,8 +789,8 @@ var invokeGitHubVerify = function (req, accessToken, refreshToken, profile) {
|
|
|
789
789
|
},
|
|
790
790
|
skipListen: true,
|
|
791
791
|
userModel: GitHubTestUserModel,
|
|
792
|
-
});
|
|
793
|
-
// Swap the github strategy with our fake after
|
|
792
|
+
}).build();
|
|
793
|
+
// Swap the github strategy with our fake after TerrenoApp registered it.
|
|
794
794
|
installFakeGithubStrategy();
|
|
795
795
|
agent = supertest_1.default.agent(app);
|
|
796
796
|
return [2 /*return*/];
|
|
@@ -910,8 +910,8 @@ var invokeGitHubVerify = function (req, accessToken, refreshToken, profile) {
|
|
|
910
910
|
return [4 /*yield*/, GitHubTestUserModel.deleteMany({})];
|
|
911
911
|
case 2:
|
|
912
912
|
_a.sent();
|
|
913
|
-
app =
|
|
914
|
-
|
|
913
|
+
app = new terrenoApp_1.TerrenoApp({
|
|
914
|
+
configureApp: addRoutes,
|
|
915
915
|
githubAuth: {
|
|
916
916
|
allowAccountLinking: true,
|
|
917
917
|
callbackURL: "http://localhost:9000/auth/github/callback",
|
|
@@ -920,7 +920,7 @@ var invokeGitHubVerify = function (req, accessToken, refreshToken, profile) {
|
|
|
920
920
|
},
|
|
921
921
|
skipListen: true,
|
|
922
922
|
userModel: GitHubTestUserModel,
|
|
923
|
-
});
|
|
923
|
+
}).build();
|
|
924
924
|
installFakeGithubStrategy();
|
|
925
925
|
agent = supertest_1.default.agent(app);
|
|
926
926
|
return [2 /*return*/];
|
|
@@ -983,8 +983,8 @@ var invokeGitHubVerify = function (req, accessToken, refreshToken, profile) {
|
|
|
983
983
|
return [4 /*yield*/, GitHubTestUserModel.deleteMany({})];
|
|
984
984
|
case 2:
|
|
985
985
|
_a.sent();
|
|
986
|
-
app =
|
|
987
|
-
|
|
986
|
+
app = new terrenoApp_1.TerrenoApp({
|
|
987
|
+
configureApp: addRoutes,
|
|
988
988
|
githubAuth: {
|
|
989
989
|
allowAccountLinking: true,
|
|
990
990
|
callbackURL: "http://localhost:9000/auth/github/callback",
|
|
@@ -993,7 +993,7 @@ var invokeGitHubVerify = function (req, accessToken, refreshToken, profile) {
|
|
|
993
993
|
},
|
|
994
994
|
skipListen: true,
|
|
995
995
|
userModel: GitHubTestUserModel,
|
|
996
|
-
});
|
|
996
|
+
}).build();
|
|
997
997
|
installFakeGithubStrategy();
|
|
998
998
|
agent = supertest_1.default.agent(app);
|
|
999
999
|
return [2 /*return*/];
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
import winston from "winston";
|
|
2
|
+
/** Always attached to Winston metadata while a request/job ALS scope is active. */
|
|
3
|
+
export interface TerrenoRequestLogEntry {
|
|
4
|
+
requestId: string;
|
|
5
|
+
userId: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface LogContextFields {
|
|
8
|
+
jobId?: string;
|
|
9
|
+
requestId?: string;
|
|
10
|
+
sessionId?: string;
|
|
11
|
+
terrenoLabels?: Record<string, string>;
|
|
12
|
+
terrenoLogPrefix?: string;
|
|
13
|
+
traceId?: string;
|
|
14
|
+
userId?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Builds the ` key=value ...` suffix appended to console/file log lines after the message.
|
|
18
|
+
* Request-scoped fields come from AsyncLocalStorage via Winston metadata; `terrenoLabels` and
|
|
19
|
+
* `terrenoLogPrefix` come from {@link createScopedLogger}. Nested `terrenoRequestLog`
|
|
20
|
+
* (`requestId` + `userId` including `null` when anonymous) is attached on the Winston info
|
|
21
|
+
* object for structured transports only, not repeated in this suffix.
|
|
22
|
+
*/
|
|
23
|
+
export declare const formatLogContextSuffix: (fields: LogContextFields) => string;
|
|
2
24
|
export declare const winstonLogger: winston.Logger;
|
|
25
|
+
/**
|
|
26
|
+
* Global application logger. Each method writes through Winston (console/file transports) and, when
|
|
27
|
+
* `USE_SENTRY_LOGGING=true`, mirrors the line to Sentry with the active request context attached.
|
|
28
|
+
*
|
|
29
|
+
* Prefer {@link createScopedLogger} when a workflow spans multiple log lines that should share a
|
|
30
|
+
* prefix or labels.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* import {logger} from "@terreno/api";
|
|
35
|
+
*
|
|
36
|
+
* logger.info("Server started", {port: 4000});
|
|
37
|
+
* logger.warn("Slow query", {ms: 500});
|
|
38
|
+
* logger.error("Failed to process", {error});
|
|
39
|
+
* logger.debug("Request details", {body: req.body});
|
|
40
|
+
*
|
|
41
|
+
* // Convenient `.catch` handler for promises – logs and captures the exception.
|
|
42
|
+
* await chargeCard(id).catch(logger.catch);
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
3
45
|
export declare const logger: {
|
|
4
46
|
catch: (e: unknown) => void;
|
|
5
47
|
debug: (msg: string, ...args: unknown[]) => void;
|
|
@@ -7,6 +49,116 @@ export declare const logger: {
|
|
|
7
49
|
info: (msg: string, ...args: unknown[]) => void;
|
|
8
50
|
warn: (msg: string, ...args: unknown[]) => void;
|
|
9
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* Logger-shaped object returned by {@link createScopedLogger} and {@link createFeatureFlaggedLogger}.
|
|
54
|
+
* Method signatures match the global {@link logger} so the three are interchangeable at call sites.
|
|
55
|
+
*/
|
|
56
|
+
export interface ScopedLogger {
|
|
57
|
+
/** Log a caught exception. Suitable as a promise handler: `promise.catch(log.catch)`. */
|
|
58
|
+
catch: (e: unknown) => void;
|
|
59
|
+
debug: (msg: string, ...args: unknown[]) => void;
|
|
60
|
+
error: (msg: string, ...args: unknown[]) => void;
|
|
61
|
+
info: (msg: string, ...args: unknown[]) => void;
|
|
62
|
+
warn: (msg: string, ...args: unknown[]) => void;
|
|
63
|
+
}
|
|
64
|
+
export interface CreateScopedLoggerOptions {
|
|
65
|
+
/** Short, stable token prepended to every message (for grep and log Explorer text search). */
|
|
66
|
+
prefix?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Workflow-specific dimensions merged into Winston metadata as `terrenoLabels` (plain-text
|
|
69
|
+
* suffix and structured jsonPayload on cloud transports). Avoid keys that collide with
|
|
70
|
+
* request context or scoped metadata: requestId, jobId, sessionId, userId, traceId, spanId,
|
|
71
|
+
* terrenoLogPrefix, terrenoRequestLog, terrenoLabels.
|
|
72
|
+
*/
|
|
73
|
+
labels?: Record<string, string | number | boolean | undefined>;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Creates a {@link ScopedLogger} that prefixes every message and/or attaches stable `labels` to
|
|
77
|
+
* every line, so multi-step workflows are easy to group and search.
|
|
78
|
+
*
|
|
79
|
+
* - `prefix` is prepended to the human-readable message (easy grep / Log Explorer text search) and
|
|
80
|
+
* also stored as the Winston metadata field `terrenoLogPrefix`.
|
|
81
|
+
* - `labels` are normalized to strings and stored as the Winston metadata field `terrenoLabels`.
|
|
82
|
+
* They appear in the plain-text ` key=value` suffix (see {@link formatLogContextSuffix}) and as
|
|
83
|
+
* discrete fields on structured transports such as `@google-cloud/logging-winston`.
|
|
84
|
+
*
|
|
85
|
+
* Both ride on a Winston **child logger**, so they merge with — and never overwrite — the
|
|
86
|
+
* request/job correlation fields that AsyncLocalStorage injects (`requestId`, `userId`,
|
|
87
|
+
* `terrenoRequestLog`, etc.). Avoid label keys that collide with those framework fields:
|
|
88
|
+
* `requestId`, `jobId`, `sessionId`, `userId`, `traceId`, `spanId`, `terrenoLogPrefix`,
|
|
89
|
+
* `terrenoRequestLog`, `terrenoLabels`.
|
|
90
|
+
*
|
|
91
|
+
* If both `prefix` and `labels` are empty, the global {@link logger} is returned unchanged.
|
|
92
|
+
*
|
|
93
|
+
* @param options - Optional `prefix` token and/or `labels` dimensions for this scope.
|
|
94
|
+
* @returns A scoped logger sharing the same methods as the global {@link logger}.
|
|
95
|
+
* @see {@link createFeatureFlaggedLogger} to gate a scoped logger behind a feature flag.
|
|
96
|
+
*
|
|
97
|
+
* @example Reuse one instance for a whole workflow so every line shares identifiers
|
|
98
|
+
* ```typescript
|
|
99
|
+
* import {createScopedLogger} from "@terreno/api";
|
|
100
|
+
*
|
|
101
|
+
* const log = createScopedLogger({
|
|
102
|
+
* prefix: "[InvoicePay]",
|
|
103
|
+
* labels: {invoiceId: invoice._id.toString(), attempt: String(attemptNumber)},
|
|
104
|
+
* });
|
|
105
|
+
*
|
|
106
|
+
* log.info("Starting capture"); // -> "[InvoicePay] Starting capture invoiceId=... attempt=1 requestId=..."
|
|
107
|
+
* log.warn("Stripe rate limited, backing off");
|
|
108
|
+
* await capture(invoice).catch(log.catch);
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export declare const createScopedLogger: (options?: CreateScopedLoggerOptions) => ScopedLogger;
|
|
112
|
+
export interface CreateFeatureFlaggedLoggerOptions {
|
|
113
|
+
/**
|
|
114
|
+
* When this returns true, log calls are forwarded to `target`. Invoked on every call so flags
|
|
115
|
+
* can flip without process restart (env, database-backed flags, `@terreno/feature-flags`, etc.).
|
|
116
|
+
*/
|
|
117
|
+
isEnabled: () => boolean;
|
|
118
|
+
/** Defaults to global `logger`; pass `createScopedLogger({...})` for gated diagnostic blocks. */
|
|
119
|
+
target?: ScopedLogger;
|
|
120
|
+
/**
|
|
121
|
+
* When false (default), `catch` always forwards to `target` so `promise.catch(log.catch)` still
|
|
122
|
+
* records errors when the flag is off. Set true to gate `catch` the same as other levels.
|
|
123
|
+
*/
|
|
124
|
+
gateCatch?: boolean;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Wraps a {@link ScopedLogger} so all `debug` / `info` / `warn` / `error` traffic is dropped while
|
|
128
|
+
* `isEnabled()` returns false. Use it to keep verbose diagnostics in the code but silent until a
|
|
129
|
+
* flag turns them on — no redeploy required.
|
|
130
|
+
*
|
|
131
|
+
* `isEnabled` is evaluated on **every** call, so it can read any feature-flag source: an
|
|
132
|
+
* environment variable, a cached/remote flag map, or a call into `@terreno/feature-flags` from your
|
|
133
|
+
* app. (`@terreno/api` deliberately does not import `@terreno/feature-flags` to avoid a package
|
|
134
|
+
* cycle — you supply the predicate.)
|
|
135
|
+
*
|
|
136
|
+
* @param options - The `isEnabled` predicate plus an optional `target` logger and `gateCatch`.
|
|
137
|
+
* @returns A scoped logger that forwards to `target` only while the flag is enabled.
|
|
138
|
+
* @see {@link createScopedLogger} for the usual `target`.
|
|
139
|
+
*
|
|
140
|
+
* @example Gate a scoped logger behind an env var (flips live, no restart)
|
|
141
|
+
* ```typescript
|
|
142
|
+
* import {createFeatureFlaggedLogger, createScopedLogger} from "@terreno/api";
|
|
143
|
+
*
|
|
144
|
+
* const jobLog = createFeatureFlaggedLogger({
|
|
145
|
+
* isEnabled: () => process.env.JOB_TRACE_LOGS === "true",
|
|
146
|
+
* target: createScopedLogger({prefix: "[Job]", labels: {jobName: "nightly-sync"}}),
|
|
147
|
+
* });
|
|
148
|
+
*
|
|
149
|
+
* jobLog.info("step 1"); // silent unless JOB_TRACE_LOGS=true
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* @example Drive it from `@terreno/feature-flags` in app code
|
|
153
|
+
* ```typescript
|
|
154
|
+
* const debugLog = createFeatureFlaggedLogger({
|
|
155
|
+
* isEnabled: () => flags.isEnabled("debug.billing"),
|
|
156
|
+
* target: createScopedLogger({prefix: "[Billing]"}),
|
|
157
|
+
* gateCatch: true, // also silence `catch` while the flag is off (default: false)
|
|
158
|
+
* });
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export declare const createFeatureFlaggedLogger: (options: CreateFeatureFlaggedLoggerOptions) => ScopedLogger;
|
|
10
162
|
export interface LoggingOptions {
|
|
11
163
|
level?: "debug" | "info" | "warn" | "error";
|
|
12
164
|
transports?: winston.transport[];
|
|
@@ -19,5 +171,7 @@ export interface LoggingOptions {
|
|
|
19
171
|
logSlowRequests?: boolean;
|
|
20
172
|
logSlowRequestsReadMs?: number;
|
|
21
173
|
logSlowRequestsWriteMs?: number;
|
|
174
|
+
/** When true, skips the dev JSONL file under `.terreno/logs/app.log`. */
|
|
175
|
+
disableTerrenoDevJsonlLog?: boolean;
|
|
22
176
|
}
|
|
23
177
|
export declare const setupLogging: (options?: LoggingOptions) => void;
|