@terreno/api 0.20.2 → 0.22.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.
Files changed (107) hide show
  1. package/.ai/guidelines/core.md +71 -0
  2. package/.ai/skills/mongoose-schema-safety/SKILL.md +143 -0
  3. package/README.md +54 -1
  4. package/bunfig.toml +1 -1
  5. package/dist/__tests__/versionCheckPlugin.test.js +29 -7
  6. package/dist/actions.openApi.test.js +13 -11
  7. package/dist/api.js +98 -11
  8. package/dist/api.query.test.js +31 -1
  9. package/dist/api.test.js +211 -0
  10. package/dist/auth.test.js +418 -43
  11. package/dist/betterAuth.d.ts +1 -1
  12. package/dist/consentApp.test.js +1 -0
  13. package/dist/example.js +4 -4
  14. package/dist/expressServer.d.ts +0 -22
  15. package/dist/expressServer.js +1 -125
  16. package/dist/expressServer.test.js +90 -91
  17. package/dist/githubAuth.test.js +22 -22
  18. package/dist/logger.d.ts +154 -0
  19. package/dist/logger.js +445 -26
  20. package/dist/logger.test.js +435 -0
  21. package/dist/middleware.d.ts +7 -0
  22. package/dist/middleware.js +58 -1
  23. package/dist/middleware.test.js +159 -0
  24. package/dist/models/consentForm.js +2 -1
  25. package/dist/models/consentResponse.js +2 -1
  26. package/dist/models/versionConfig.js +2 -1
  27. package/dist/openApi.test.js +10 -17
  28. package/dist/openApiBuilder.d.ts +18 -0
  29. package/dist/openApiBuilder.js +21 -0
  30. package/dist/openApiBuilder.test.js +34 -10
  31. package/dist/permissions.test.js +10 -43
  32. package/dist/populate.test.js +10 -42
  33. package/dist/realtime/changeStreamWatcher.d.ts +4 -4
  34. package/dist/realtime/changeStreamWatcher.js +2 -4
  35. package/dist/realtime/queryMatcher.d.ts +1 -1
  36. package/dist/realtime/queryMatcher.js +39 -14
  37. package/dist/realtime/types.d.ts +3 -3
  38. package/dist/requestContext.d.ts +61 -0
  39. package/dist/requestContext.js +74 -0
  40. package/dist/secretProviders.test.js +335 -0
  41. package/dist/syncConsents.test.js +2 -2
  42. package/dist/terrenoApp.d.ts +27 -15
  43. package/dist/terrenoApp.js +24 -14
  44. package/dist/terrenoApp.test.js +52 -0
  45. package/dist/tests/bunSetup.js +66 -262
  46. package/dist/tests/createTestData.d.ts +9 -0
  47. package/dist/tests/createTestData.js +272 -0
  48. package/dist/tests/models.d.ts +71 -0
  49. package/dist/tests/models.js +134 -0
  50. package/dist/tests/mongoTestSetup.d.ts +7 -0
  51. package/dist/tests/mongoTestSetup.js +150 -0
  52. package/dist/tests/testEnv.d.ts +0 -0
  53. package/dist/tests/testEnv.js +6 -0
  54. package/dist/tests/testHelper.d.ts +22 -0
  55. package/dist/tests/testHelper.js +115 -0
  56. package/dist/tests/types.d.ts +29 -0
  57. package/dist/tests/types.js +2 -0
  58. package/dist/tests.d.ts +10 -78
  59. package/dist/tests.js +24 -241
  60. package/dist/transformers.test.js +14 -50
  61. package/package.json +18 -4
  62. package/src/__snapshots__/openApiBuilder.test.ts.snap +1 -0
  63. package/src/__tests__/versionCheckPlugin.test.ts +43 -15
  64. package/src/actions.openApi.test.ts +12 -10
  65. package/src/api.query.test.ts +24 -1
  66. package/src/api.test.ts +169 -0
  67. package/src/api.ts +71 -0
  68. package/src/auth.test.ts +287 -39
  69. package/src/betterAuth.ts +1 -1
  70. package/src/consentApp.test.ts +1 -0
  71. package/src/example.ts +4 -4
  72. package/src/expressServer.test.ts +82 -85
  73. package/src/expressServer.ts +1 -213
  74. package/src/githubAuth.test.ts +22 -22
  75. package/src/logger.test.ts +466 -1
  76. package/src/logger.ts +477 -14
  77. package/src/middleware.test.ts +74 -2
  78. package/src/middleware.ts +57 -0
  79. package/src/models/consentForm.ts +3 -4
  80. package/src/models/consentResponse.ts +6 -4
  81. package/src/models/versionConfig.ts +3 -4
  82. package/src/openApi.test.ts +10 -17
  83. package/src/openApiBuilder.test.ts +27 -10
  84. package/src/openApiBuilder.ts +24 -0
  85. package/src/permissions.test.ts +8 -23
  86. package/src/populate.test.ts +7 -22
  87. package/src/realtime/changeStreamWatcher.ts +15 -10
  88. package/src/realtime/queryMatcher.ts +54 -27
  89. package/src/realtime/types.ts +4 -4
  90. package/src/requestContext.ts +86 -0
  91. package/src/secretProviders.test.ts +219 -1
  92. package/src/syncConsents.test.ts +1 -1
  93. package/src/terrenoApp.test.ts +38 -0
  94. package/src/terrenoApp.ts +37 -15
  95. package/src/tests/bunSetup.ts +22 -236
  96. package/src/tests/createTestData.ts +176 -0
  97. package/src/tests/models.ts +164 -0
  98. package/src/tests/mongoTestSetup.ts +69 -0
  99. package/src/tests/testEnv.ts +4 -0
  100. package/src/tests/testHelper.ts +57 -0
  101. package/src/tests/types.ts +35 -0
  102. package/src/tests.ts +40 -231
  103. package/src/transformers.test.ts +11 -30
  104. package/tsconfig.typedoc.json +4 -0
  105. package/dist/tests/index.d.ts +0 -1
  106. package/dist/tests/index.js +0 -17
  107. package/src/tests/index.ts +0 -1
@@ -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 = (0, expressServer_1.setupServer)({
194
- addRoutes: addRoutes,
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 = (0, expressServer_1.setupServer)({
342
- addRoutes: addRoutes,
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 = (0, expressServer_1.setupServer)({
669
- addRoutes: addRoutes,
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 = (0, expressServer_1.setupServer)({
771
- addMiddleware: function (a) {
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
- // setupServer does not install express-session, so prime a fake session from a request
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
- addRoutes: addRoutes,
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 setupServer registered it.
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 = (0, expressServer_1.setupServer)({
914
- addRoutes: addRoutes,
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 = (0, expressServer_1.setupServer)({
987
- addRoutes: addRoutes,
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;