@terreno/api 0.18.0 → 0.20.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 (41) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/api.test.js +18 -8
  3. package/dist/auth.d.ts +5 -5
  4. package/dist/auth.js +123 -131
  5. package/dist/configuration.test.js +289 -10
  6. package/dist/configurationApp.d.ts +72 -5
  7. package/dist/configurationApp.js +168 -48
  8. package/dist/configurationPlugin.d.ts +64 -7
  9. package/dist/configurationPlugin.js +161 -39
  10. package/dist/configurationPlugin.test.js +238 -1
  11. package/dist/expressServer.test.js +0 -1
  12. package/dist/openApi.d.ts +6 -6
  13. package/dist/openApi.js +21 -21
  14. package/dist/populate.test.js +23 -0
  15. package/dist/realtime/queryMatcher.js +0 -6
  16. package/dist/realtime/queryStore.js +3 -11
  17. package/dist/realtime/realtime.test.js +41 -34
  18. package/dist/secretProviders.d.ts +79 -2
  19. package/dist/secretProviders.js +177 -9
  20. package/dist/secretProviders.test.d.ts +1 -0
  21. package/dist/secretProviders.test.js +391 -0
  22. package/package.json +1 -1
  23. package/src/actions.openApi.test.ts +1 -1
  24. package/src/actions.ts +0 -1
  25. package/src/api.test.ts +10 -2
  26. package/src/auth.ts +19 -19
  27. package/src/configuration.test.ts +171 -7
  28. package/src/configurationApp.ts +213 -30
  29. package/src/configurationPlugin.test.ts +174 -2
  30. package/src/configurationPlugin.ts +157 -28
  31. package/src/expressServer.test.ts +0 -1
  32. package/src/openApi.ts +21 -21
  33. package/src/populate.test.ts +25 -0
  34. package/src/realtime/queryMatcher.ts +0 -6
  35. package/src/realtime/queryStore.ts +1 -10
  36. package/src/realtime/realtime.test.ts +24 -24
  37. package/src/realtime/realtimeApp.ts +0 -1
  38. package/src/realtime/registry.ts +0 -1
  39. package/src/realtime/types.ts +0 -4
  40. package/src/secretProviders.test.ts +186 -0
  41. package/src/secretProviders.ts +145 -5
@@ -46,25 +46,108 @@ var __values = (this && this.__values) || function(o) {
46
46
  };
47
47
  throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
48
48
  };
49
+ var __read = (this && this.__read) || function (o, n) {
50
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
51
+ if (!m) return o;
52
+ var i = m.call(o), r, ar = [], e;
53
+ try {
54
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
55
+ }
56
+ catch (error) { e = { error: error }; }
57
+ finally {
58
+ try {
59
+ if (r && !r.done && (m = i["return"])) m.call(i);
60
+ }
61
+ finally { if (e) throw e.error; }
62
+ }
63
+ return ar;
64
+ };
65
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
66
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
67
+ if (ar || !(i in from)) {
68
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
69
+ ar[i] = from[i];
70
+ }
71
+ }
72
+ return to.concat(ar || Array.prototype.slice.call(from));
73
+ };
49
74
  Object.defineProperty(exports, "__esModule", { value: true });
50
- exports.configurationPlugin = void 0;
75
+ exports.configurationPlugin = exports.flattenToDotPaths = void 0;
51
76
  var errors_1 = require("./errors");
52
77
  var logger_1 = require("./logger");
53
78
  var plugins_1 = require("./plugins");
54
79
  // ---------------------------------------------------------------------------
80
+ // Helpers
81
+ // ---------------------------------------------------------------------------
82
+ /**
83
+ * Flattens a nested patch into MongoDB-style dotted paths, recursing into plain
84
+ * objects only; arrays and other values are treated as leaves.
85
+ *
86
+ * @example
87
+ * flattenToDotPaths({a: {b: 1}}) // => [["a.b", 1]]
88
+ */
89
+ var flattenToDotPaths = function (obj, prefix) {
90
+ var e_1, _a;
91
+ if (prefix === void 0) { prefix = ""; }
92
+ var out = [];
93
+ try {
94
+ for (var _b = __values(Object.entries(obj)), _c = _b.next(); !_c.done; _c = _b.next()) {
95
+ var _d = __read(_c.value, 2), key = _d[0], value = _d[1];
96
+ var path = prefix ? "".concat(prefix, ".").concat(key) : key;
97
+ var isPlainObject = value !== null &&
98
+ typeof value === "object" &&
99
+ !Array.isArray(value) &&
100
+ Object.getPrototypeOf(value) === Object.prototype;
101
+ if (isPlainObject) {
102
+ out.push.apply(out, __spreadArray([], __read((0, exports.flattenToDotPaths)(value, path)), false));
103
+ }
104
+ else {
105
+ out.push([path, value]);
106
+ }
107
+ }
108
+ }
109
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
110
+ finally {
111
+ try {
112
+ if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
113
+ }
114
+ finally { if (e_1) throw e_1.error; }
115
+ }
116
+ return out;
117
+ };
118
+ exports.flattenToDotPaths = flattenToDotPaths;
119
+ /**
120
+ * Builds the filter used to locate the singleton document. When the schema is
121
+ * soft-delete aware (has a `deleted` path, e.g. via `isDeletedPlugin`), the
122
+ * singleton is "the one non-deleted document"; otherwise any document matches.
123
+ */
124
+ var buildSingletonFilter = function (schema) {
125
+ return schema.path("deleted") ? { deleted: false } : {};
126
+ };
127
+ // ---------------------------------------------------------------------------
55
128
  // Plugin
56
129
  // ---------------------------------------------------------------------------
57
130
  /**
58
131
  * Mongoose schema plugin that adds singleton configuration behavior.
59
132
  *
60
133
  * Adds:
61
- * - Pre-save hook enforcing exactly one document
134
+ * - Pre-save hook enforcing exactly one non-deleted document (soft-delete aware
135
+ * when the schema has a `deleted` path, e.g. via `isDeletedPlugin`)
62
136
  * - `getConfig()` static: fetches or creates the singleton (full doc or keyed value)
63
- * - `updateConfig(updates)` static: patches the singleton
137
+ * - `updateConfig(updates)` static: patches the singleton via `findOneAndUpdate({$set})`
138
+ * with dotted paths (preserves sibling subdoc fields; tolerates legacy fields)
64
139
  * - `getSecretFields()` static: returns metadata for fields with `secret: true`
65
- * - `resolveSecrets(provider?)` static: fetches secret values, using the plugin provider by default
140
+ * - `resolveSecrets(provider?)` static: resolves secret values into an in-memory map,
141
+ * using the plugin provider by default (never persists values)
142
+ * - Hard-delete blockers (`deleteOne`/`deleteMany`/`findOneAndDelete`); soft deletes
143
+ * (setting `deleted: true`) are allowed
66
144
  *
67
- * Mark fields as secrets using schema path options:
145
+ * Soft deletes are allowed and a soft-deleted document does not block creating a
146
+ * new singleton. The `_singleton` unique index is opt-in via
147
+ * `enforceSingletonIndex` (default off).
148
+ *
149
+ * Mark fields as secrets using schema path options. Pin a version with the
150
+ * optional `secretVersion` option:
68
151
  * ```typescript
69
152
  * const configSchema = new Schema({
70
153
  * apiKey: {
@@ -72,6 +155,7 @@ var plugins_1 = require("./plugins");
72
155
  * description: "Third-party API key",
73
156
  * secret: true,
74
157
  * secretName: "my-api-key",
158
+ * secretVersion: "3", // optional — resolves "latest" when omitted
75
159
  * },
76
160
  * });
77
161
  * configSchema.plugin(configurationPlugin, {secretProvider: new EnvSecretProvider()});
@@ -81,27 +165,34 @@ var configurationPlugin = function (schema, options) {
81
165
  var pluginOptions = options !== null && options !== void 0 ? options : {};
82
166
  // Apply findOneOrNone so the singleton lookup avoids bare Model.findOne (idempotent).
83
167
  (0, plugins_1.findOneOrNone)(schema);
84
- // Add a sentinel field with a unique index to enforce singleton at the DB level.
85
- // All config documents get _singleton: "config", and the unique index prevents duplicates.
86
- schema.add({
87
- _singleton: {
88
- default: "config",
89
- description: "Sentinel field enforcing singleton constraint",
90
- immutable: true,
91
- select: false,
92
- type: String,
93
- },
94
- });
95
- schema.index({ _singleton: 1 }, { unique: true });
96
- // Enforce singleton: only one document allowed (application-level guard)
168
+ // Optionally add a sentinel field with a unique index to enforce the singleton
169
+ // at the database level. This is opt-in (default off) so it does not conflict
170
+ // with consumers that already enforce a single non-deleted document via the
171
+ // pre-save guard below or via their own soft-delete plugin/indexes.
172
+ if (pluginOptions.enforceSingletonIndex) {
173
+ schema.add({
174
+ _singleton: {
175
+ default: "config",
176
+ description: "Sentinel field enforcing singleton constraint",
177
+ immutable: true,
178
+ select: false,
179
+ type: String,
180
+ },
181
+ });
182
+ schema.index({ _singleton: 1 }, { unique: true });
183
+ }
184
+ // Enforce singleton: only one non-deleted document allowed (application-level
185
+ // guard). Soft-delete-aware: a soft-deleted document does not block creating a
186
+ // new singleton.
97
187
  schema.pre("save", function () {
98
188
  return __awaiter(this, void 0, void 0, function () {
99
- var existing;
189
+ var filter, existing;
100
190
  return __generator(this, function (_a) {
101
191
  switch (_a.label) {
102
192
  case 0:
103
193
  if (!this.isNew) return [3 /*break*/, 2];
104
- return [4 /*yield*/, this.constructor.exists({})];
194
+ filter = buildSingletonFilter(schema);
195
+ return [4 /*yield*/, this.constructor.exists(filter)];
105
196
  case 1:
106
197
  existing = _a.sent();
107
198
  if (existing) {
@@ -135,14 +226,15 @@ var configurationPlugin = function (schema, options) {
135
226
  // Static: get the singleton configuration document or a value at a path (race-safe via upsert)
136
227
  schema.statics.getConfig = function (key) {
137
228
  return __awaiter(this, void 0, void 0, function () {
138
- var findSingleton, config, created, err_1, parts, value, parts_1, parts_1_1, part;
139
- var e_1, _a;
229
+ var singletonFilter, findSingleton, config, created, err_1, parts, value, parts_1, parts_1_1, part;
230
+ var e_2, _a;
140
231
  var _this = this;
141
232
  return __generator(this, function (_b) {
142
233
  switch (_b.label) {
143
234
  case 0:
235
+ singletonFilter = buildSingletonFilter(this.schema);
144
236
  findSingleton = function () {
145
- return _this.findOneOrNone({});
237
+ return _this.findOneOrNone(singletonFilter);
146
238
  };
147
239
  return [4 /*yield*/, findSingleton()];
148
240
  case 1:
@@ -181,32 +273,61 @@ var configurationPlugin = function (schema, options) {
181
273
  value = value[part];
182
274
  }
183
275
  }
184
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
276
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
185
277
  finally {
186
278
  try {
187
279
  if (parts_1_1 && !parts_1_1.done && (_a = parts_1.return)) _a.call(parts_1);
188
280
  }
189
- finally { if (e_1) throw e_1.error; }
281
+ finally { if (e_2) throw e_2.error; }
190
282
  }
191
283
  return [2 /*return*/, value];
192
284
  }
193
285
  });
194
286
  });
195
287
  };
196
- // Static: update the singleton configuration document (race-safe)
288
+ // Static: update the singleton configuration document via $set dotted paths.
289
+ // Flattening to dotted paths preserves sibling subdoc fields and tolerates
290
+ // legacy/out-of-schema fields already persisted on the document.
197
291
  schema.statics.updateConfig = function (updates) {
198
292
  return __awaiter(this, void 0, void 0, function () {
199
- var config;
200
- return __generator(this, function (_a) {
201
- switch (_a.label) {
202
- case 0: return [4 /*yield*/, this.getConfig()];
293
+ var singletonFilter, setFields, _a, _b, _c, path, value, updated;
294
+ var e_3, _d;
295
+ return __generator(this, function (_e) {
296
+ switch (_e.label) {
297
+ case 0:
298
+ singletonFilter = buildSingletonFilter(this.schema);
299
+ setFields = {};
300
+ try {
301
+ for (_a = __values((0, exports.flattenToDotPaths)(updates)), _b = _a.next(); !_b.done; _b = _a.next()) {
302
+ _c = __read(_b.value, 2), path = _c[0], value = _c[1];
303
+ setFields[path] = value;
304
+ }
305
+ }
306
+ catch (e_3_1) { e_3 = { error: e_3_1 }; }
307
+ finally {
308
+ try {
309
+ if (_b && !_b.done && (_d = _a.return)) _d.call(_a);
310
+ }
311
+ finally { if (e_3) throw e_3.error; }
312
+ }
313
+ // Nothing to set — return the current singleton (creating it if missing).
314
+ if (Object.keys(setFields).length === 0) {
315
+ return [2 /*return*/, this.getConfig()];
316
+ }
317
+ return [4 /*yield*/, this.findOneAndUpdate(singletonFilter, { $set: setFields }, { new: true, runValidators: true })];
203
318
  case 1:
204
- config = _a.sent();
205
- Object.assign(config, updates);
206
- return [4 /*yield*/, config.save()];
319
+ updated = _e.sent();
320
+ if (updated) {
321
+ return [2 /*return*/, updated];
322
+ }
323
+ // No singleton yet — create one (with subdocument defaults applied), then
324
+ // apply the patch.
325
+ return [4 /*yield*/, this.getConfig()];
207
326
  case 2:
208
- _a.sent();
209
- return [2 /*return*/, config];
327
+ // No singleton yet — create one (with subdocument defaults applied), then
328
+ // apply the patch.
329
+ _e.sent();
330
+ return [2 /*return*/, this.findOneAndUpdate(singletonFilter, { $set: setFields }, { new: true, runValidators: true }).orFail()];
210
331
  }
211
332
  });
212
333
  });
@@ -223,6 +344,7 @@ var configurationPlugin = function (schema, options) {
223
344
  path: prefix ? "".concat(prefix, ".").concat(pathName) : pathName,
224
345
  secretName: (_a = opts.secretName) !== null && _a !== void 0 ? _a : pathName,
225
346
  secretProvider: opts.secretProvider,
347
+ version: opts.secretVersion,
226
348
  });
227
349
  }
228
350
  // Recurse into subschemas
@@ -238,7 +360,7 @@ var configurationPlugin = function (schema, options) {
238
360
  schema.statics.resolveSecrets = function (provider) {
239
361
  return __awaiter(this, void 0, void 0, function () {
240
362
  var resolvedProvider, secrets, resolved, results, failCount, results_1, results_1_1, result;
241
- var e_2, _a;
363
+ var e_4, _a;
242
364
  var _this = this;
243
365
  return __generator(this, function (_b) {
244
366
  switch (_b.label) {
@@ -254,7 +376,7 @@ var configurationPlugin = function (schema, options) {
254
376
  var value;
255
377
  return __generator(this, function (_a) {
256
378
  switch (_a.label) {
257
- case 0: return [4 /*yield*/, resolvedProvider.getSecret(meta.secretName)];
379
+ case 0: return [4 /*yield*/, resolvedProvider.getSecret(meta.secretName, meta.version)];
258
380
  case 1:
259
381
  value = _a.sent();
260
382
  if (value !== null) {
@@ -276,12 +398,12 @@ var configurationPlugin = function (schema, options) {
276
398
  }
277
399
  }
278
400
  }
279
- catch (e_2_1) { e_2 = { error: e_2_1 }; }
401
+ catch (e_4_1) { e_4 = { error: e_4_1 }; }
280
402
  finally {
281
403
  try {
282
404
  if (results_1_1 && !results_1_1.done && (_a = results_1.return)) _a.call(results_1);
283
405
  }
284
- finally { if (e_2) throw e_2.error; }
406
+ finally { if (e_4) throw e_4.error; }
285
407
  }
286
408
  if (failCount > 0) {
287
409
  logger_1.logger.warn("".concat(failCount, "/").concat(secrets.length, " secrets failed to resolve"));
@@ -89,6 +89,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
89
89
  var bun_test_1 = require("bun:test");
90
90
  var mongoose_1 = __importStar(require("mongoose"));
91
91
  var configurationPlugin_1 = require("./configurationPlugin");
92
+ var plugins_1 = require("./plugins");
92
93
  var testConfigSchema = new mongoose_1.Schema({
93
94
  apiKey: {
94
95
  default: "",
@@ -133,14 +134,47 @@ var simpleSchema = new mongoose_1.Schema({
133
134
  });
134
135
  simpleSchema.plugin(configurationPlugin_1.configurationPlugin);
135
136
  var SimpleConfigModel = (0, mongoose_1.model)("SimpleConfiguration", simpleSchema);
137
+ // --- Schema opting into the _singleton unique index ---
138
+ var indexedSchema = new mongoose_1.Schema({
139
+ value: { default: "default", description: "A value", type: String },
140
+ });
141
+ indexedSchema.plugin(configurationPlugin_1.configurationPlugin, { enforceSingletonIndex: true });
142
+ var IndexedConfigModel = (0, mongoose_1.model)("IndexedConfiguration", indexedSchema);
143
+ // --- Soft-delete-aware schema ---
144
+ var softDeleteSchema = new mongoose_1.Schema({
145
+ value: { default: "default", description: "A value", type: String },
146
+ });
147
+ softDeleteSchema.plugin(configurationPlugin_1.configurationPlugin);
148
+ softDeleteSchema.plugin(plugins_1.isDeletedPlugin);
149
+ var SoftDeleteConfigModel = (0, mongoose_1.model)("SoftDeleteConfiguration", softDeleteSchema);
150
+ // --- Schema with a validated field (enum) for runValidators coverage ---
151
+ var validatedSchema = new mongoose_1.Schema({
152
+ level: {
153
+ default: "low",
154
+ description: "Severity level",
155
+ enum: ["low", "medium", "high"],
156
+ type: String,
157
+ },
158
+ });
159
+ validatedSchema.plugin(configurationPlugin_1.configurationPlugin);
160
+ var ValidatedConfigModel = (0, mongoose_1.model)("ValidatedConfiguration", validatedSchema);
136
161
  (0, bun_test_1.describe)("configurationPlugin", function () {
137
162
  (0, bun_test_1.describe)("schema setup", function () {
138
- (0, bun_test_1.it)("adds a _singleton field with unique index", function () {
163
+ (0, bun_test_1.it)("does not add a _singleton index by default", function () {
139
164
  var indexes = SimpleConfigModel.schema.indexes();
140
165
  var singletonIndex = indexes.find(function (_a) {
141
166
  var _b = __read(_a, 1), fields = _b[0];
142
167
  return fields._singleton !== undefined;
143
168
  });
169
+ (0, bun_test_1.expect)(singletonIndex).toBeUndefined();
170
+ (0, bun_test_1.expect)(SimpleConfigModel.schema.path("_singleton")).toBeUndefined();
171
+ });
172
+ (0, bun_test_1.it)("adds a _singleton field with unique index when enforceSingletonIndex is true", function () {
173
+ var indexes = IndexedConfigModel.schema.indexes();
174
+ var singletonIndex = indexes.find(function (_a) {
175
+ var _b = __read(_a, 1), fields = _b[0];
176
+ return fields._singleton !== undefined;
177
+ });
144
178
  (0, bun_test_1.expect)(singletonIndex).toBeDefined();
145
179
  (0, bun_test_1.expect)(singletonIndex[1].unique).toBe(true);
146
180
  });
@@ -256,6 +290,55 @@ var SimpleConfigModel = (0, mongoose_1.model)("SimpleConfiguration", simpleSchem
256
290
  }
257
291
  });
258
292
  }); });
293
+ (0, bun_test_1.it)("passes the discovered secret version to the provider", function () { return __awaiter(void 0, void 0, void 0, function () {
294
+ var versionedSchema, VersionedModel, received, provider;
295
+ return __generator(this, function (_a) {
296
+ switch (_a.label) {
297
+ case 0:
298
+ versionedSchema = new mongoose_1.Schema({
299
+ token: {
300
+ default: "",
301
+ description: "Pinned secret",
302
+ secret: true,
303
+ secretName: "pinned-token",
304
+ secretVersion: "5",
305
+ type: String,
306
+ },
307
+ });
308
+ versionedSchema.plugin(configurationPlugin_1.configurationPlugin);
309
+ VersionedModel = (0, mongoose_1.model)("VersionedConfiguration", versionedSchema);
310
+ received = [];
311
+ provider = {
312
+ getSecret: function (name, version) { return __awaiter(void 0, void 0, void 0, function () {
313
+ return __generator(this, function (_a) {
314
+ received.push({ name: name, version: version });
315
+ return [2 /*return*/, "value"];
316
+ });
317
+ }); },
318
+ name: "versioned-provider",
319
+ };
320
+ return [4 /*yield*/, VersionedModel.resolveSecrets(provider)];
321
+ case 1:
322
+ _a.sent();
323
+ (0, bun_test_1.expect)(received).toEqual([{ name: "pinned-token", version: "5" }]);
324
+ return [2 /*return*/];
325
+ }
326
+ });
327
+ }); });
328
+ });
329
+ (0, bun_test_1.describe)("flattenToDotPaths", function () {
330
+ (0, bun_test_1.it)("flattens nested plain objects into dotted paths", function () {
331
+ (0, bun_test_1.expect)((0, configurationPlugin_1.flattenToDotPaths)({ a: { b: 1 } })).toEqual([["a.b", 1]]);
332
+ });
333
+ (0, bun_test_1.it)("treats arrays as leaves", function () {
334
+ (0, bun_test_1.expect)((0, configurationPlugin_1.flattenToDotPaths)({ a: [1, 2] })).toEqual([["a", [1, 2]]]);
335
+ });
336
+ (0, bun_test_1.it)("treats null as a leaf and keeps top-level keys", function () {
337
+ (0, bun_test_1.expect)((0, configurationPlugin_1.flattenToDotPaths)({ a: null, b: "x" })).toEqual([
338
+ ["a", null],
339
+ ["b", "x"],
340
+ ]);
341
+ });
259
342
  });
260
343
  (0, bun_test_1.describe)("singleton behavior (requires MongoDB)", function () {
261
344
  var dbConnected = false;
@@ -425,6 +508,38 @@ var SimpleConfigModel = (0, mongoose_1.model)("SimpleConfiguration", simpleSchem
425
508
  }
426
509
  });
427
510
  }); });
511
+ (0, bun_test_1.it)("runs schema validators on updateConfig (rejects invalid enum)", function () { return __awaiter(void 0, void 0, void 0, function () {
512
+ var _a, ok;
513
+ return __generator(this, function (_b) {
514
+ switch (_b.label) {
515
+ case 0:
516
+ if (!dbConnected) {
517
+ return [2 /*return*/];
518
+ }
519
+ _b.label = 1;
520
+ case 1:
521
+ _b.trys.push([1, 3, , 4]);
522
+ return [4 /*yield*/, ValidatedConfigModel.collection.drop()];
523
+ case 2:
524
+ _b.sent();
525
+ return [3 /*break*/, 4];
526
+ case 3:
527
+ _a = _b.sent();
528
+ return [3 /*break*/, 4];
529
+ case 4: return [4 /*yield*/, ValidatedConfigModel.getConfig()];
530
+ case 5:
531
+ _b.sent();
532
+ return [4 /*yield*/, (0, bun_test_1.expect)(ValidatedConfigModel.updateConfig({ level: "bogus" })).rejects.toThrow()];
533
+ case 6:
534
+ _b.sent();
535
+ return [4 /*yield*/, ValidatedConfigModel.updateConfig({ level: "high" })];
536
+ case 7:
537
+ ok = _b.sent();
538
+ (0, bun_test_1.expect)(ok.level).toBe("high");
539
+ return [2 /*return*/];
540
+ }
541
+ });
542
+ }); });
428
543
  (0, bun_test_1.it)("prevents deleteOne", function () { return __awaiter(void 0, void 0, void 0, function () {
429
544
  var err_1;
430
545
  return __generator(this, function (_a) {
@@ -507,4 +622,126 @@ var SimpleConfigModel = (0, mongoose_1.model)("SimpleConfiguration", simpleSchem
507
622
  });
508
623
  }); });
509
624
  });
625
+ (0, bun_test_1.describe)("soft-delete-aware singleton (requires MongoDB)", function () {
626
+ var dbConnected = false;
627
+ (0, bun_test_1.beforeAll)(function () { return __awaiter(void 0, void 0, void 0, function () {
628
+ var _a;
629
+ return __generator(this, function (_b) {
630
+ switch (_b.label) {
631
+ case 0:
632
+ _b.trys.push([0, 4, , 5]);
633
+ if (!(mongoose_1.default.connection.readyState === 1)) return [3 /*break*/, 1];
634
+ dbConnected = true;
635
+ return [3 /*break*/, 3];
636
+ case 1: return [4 /*yield*/, mongoose_1.default.connect("mongodb://127.0.0.1/terreno-config-test", {
637
+ connectTimeoutMS: 3000,
638
+ serverSelectionTimeoutMS: 3000,
639
+ })];
640
+ case 2:
641
+ _b.sent();
642
+ dbConnected = true;
643
+ _b.label = 3;
644
+ case 3: return [3 /*break*/, 5];
645
+ case 4:
646
+ _a = _b.sent();
647
+ dbConnected = false;
648
+ return [3 /*break*/, 5];
649
+ case 5: return [2 /*return*/];
650
+ }
651
+ });
652
+ }); });
653
+ (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
654
+ var _a;
655
+ return __generator(this, function (_b) {
656
+ switch (_b.label) {
657
+ case 0:
658
+ if (!dbConnected) {
659
+ return [2 /*return*/];
660
+ }
661
+ _b.label = 1;
662
+ case 1:
663
+ _b.trys.push([1, 3, , 4]);
664
+ return [4 /*yield*/, SoftDeleteConfigModel.collection.drop()];
665
+ case 2:
666
+ _b.sent();
667
+ return [3 /*break*/, 4];
668
+ case 3:
669
+ _a = _b.sent();
670
+ return [3 /*break*/, 4];
671
+ case 4: return [2 /*return*/];
672
+ }
673
+ });
674
+ }); });
675
+ (0, bun_test_1.it)("operates on the non-deleted singleton", function () { return __awaiter(void 0, void 0, void 0, function () {
676
+ var config, updated;
677
+ return __generator(this, function (_a) {
678
+ switch (_a.label) {
679
+ case 0:
680
+ if (!dbConnected) {
681
+ return [2 /*return*/];
682
+ }
683
+ return [4 /*yield*/, SoftDeleteConfigModel.getConfig()];
684
+ case 1:
685
+ config = _a.sent();
686
+ (0, bun_test_1.expect)(config.value).toBe("default");
687
+ return [4 /*yield*/, SoftDeleteConfigModel.updateConfig({ value: "live" })];
688
+ case 2:
689
+ updated = _a.sent();
690
+ (0, bun_test_1.expect)(updated.value).toBe("live");
691
+ (0, bun_test_1.expect)(updated.deleted).toBe(false);
692
+ return [2 /*return*/];
693
+ }
694
+ });
695
+ }); });
696
+ (0, bun_test_1.it)("allows a new singleton after the existing one is soft-deleted", function () { return __awaiter(void 0, void 0, void 0, function () {
697
+ var first, second;
698
+ return __generator(this, function (_a) {
699
+ switch (_a.label) {
700
+ case 0:
701
+ if (!dbConnected) {
702
+ return [2 /*return*/];
703
+ }
704
+ return [4 /*yield*/, SoftDeleteConfigModel.getConfig()];
705
+ case 1:
706
+ first = _a.sent();
707
+ // Soft delete by setting deleted: true (allowed)
708
+ first.deleted = true;
709
+ return [4 /*yield*/, first.save()];
710
+ case 2:
711
+ _a.sent();
712
+ return [4 /*yield*/, SoftDeleteConfigModel.getConfig()];
713
+ case 3:
714
+ second = _a.sent();
715
+ (0, bun_test_1.expect)(second.deleted).toBe(false);
716
+ (0, bun_test_1.expect)(second._id.toString()).not.toBe(first._id.toString());
717
+ return [2 /*return*/];
718
+ }
719
+ });
720
+ }); });
721
+ (0, bun_test_1.it)("does not let updateConfig touch a soft-deleted document", function () { return __awaiter(void 0, void 0, void 0, function () {
722
+ var first, updated;
723
+ return __generator(this, function (_a) {
724
+ switch (_a.label) {
725
+ case 0:
726
+ if (!dbConnected) {
727
+ return [2 /*return*/];
728
+ }
729
+ return [4 /*yield*/, SoftDeleteConfigModel.getConfig()];
730
+ case 1:
731
+ first = _a.sent();
732
+ first.deleted = true;
733
+ return [4 /*yield*/, first.save()];
734
+ case 2:
735
+ _a.sent();
736
+ return [4 /*yield*/, SoftDeleteConfigModel.updateConfig({ value: "fresh" })];
737
+ case 3:
738
+ updated = _a.sent();
739
+ (0, bun_test_1.expect)(updated.deleted).toBe(false);
740
+ (0, bun_test_1.expect)(updated.value).toBe("fresh");
741
+ (0, bun_test_1.expect)(updated._id.toString()).not.toBe(first._id.toString());
742
+ return [2 /*return*/];
743
+ }
744
+ });
745
+ }); });
746
+ });
510
747
  });
@@ -1039,7 +1039,6 @@ var tests_1 = require("./tests");
1039
1039
  case 1:
1040
1040
  express = _a.sent();
1041
1041
  originalListen = express.default.application.listen;
1042
- // biome-ignore lint/suspicious/noExplicitAny: mocking Express internals requires type escape
1043
1042
  express.default.application.listen = (0, bun_test_1.mock)(function () {
1044
1043
  var args = [];
1045
1044
  for (var _i = 0; _i < arguments.length; _i++) {
package/dist/openApi.d.ts CHANGED
@@ -53,9 +53,9 @@ export declare const defaultOpenApiErrorResponses: {
53
53
  description: string;
54
54
  };
55
55
  };
56
- export declare function getOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>): express.RequestHandler;
57
- export declare function listOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>): express.RequestHandler;
58
- export declare function createOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>): express.RequestHandler;
59
- export declare function patchOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>): express.RequestHandler;
60
- export declare function deleteOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>): express.RequestHandler;
61
- export declare function readOpenApiMiddleware<T>(options: Partial<ModelRouterOptions<T>>, properties: Record<string, unknown>, required: string[], queryParameters: Array<Record<string, unknown>>): express.RequestHandler;
56
+ export declare const getOpenApiMiddleware: <T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>) => express.RequestHandler;
57
+ export declare const listOpenApiMiddleware: <T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>) => express.RequestHandler;
58
+ export declare const createOpenApiMiddleware: <T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>) => express.RequestHandler;
59
+ export declare const patchOpenApiMiddleware: <T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>) => express.RequestHandler;
60
+ export declare const deleteOpenApiMiddleware: <T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>) => express.RequestHandler;
61
+ export declare const readOpenApiMiddleware: <T>(options: Partial<ModelRouterOptions<T>>, properties: Record<string, unknown>, required: string[], queryParameters: Array<Record<string, unknown>>) => express.RequestHandler;