@terreno/api 0.0.18 → 0.1.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 (48) hide show
  1. package/.claude/CLAUDE.local.md +204 -0
  2. package/.cursor/rules/00-root.mdc +338 -0
  3. package/.github/copilot-instructions.md +333 -0
  4. package/AGENTS.md +23 -3
  5. package/README.md +73 -3
  6. package/dist/api.d.ts +68 -1
  7. package/dist/api.js +139 -4
  8. package/dist/api.test.js +906 -2
  9. package/dist/auth.js +3 -1
  10. package/dist/errors.js +14 -11
  11. package/dist/example.js +7 -7
  12. package/dist/githubAuth.test.js +3 -3
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.js +2 -0
  15. package/dist/openApi.test.js +8 -5
  16. package/dist/openApiBuilder.d.ts +69 -1
  17. package/dist/openApiBuilder.js +109 -5
  18. package/dist/openApiValidator.d.ts +296 -0
  19. package/dist/openApiValidator.js +698 -0
  20. package/dist/openApiValidator.test.d.ts +1 -0
  21. package/dist/openApiValidator.test.js +346 -0
  22. package/dist/plugins.test.js +3 -3
  23. package/dist/terrenoPlugin.d.ts +4 -0
  24. package/dist/terrenoPlugin.js +2 -0
  25. package/dist/tests.js +34 -24
  26. package/package.json +4 -1
  27. package/src/__snapshots__/openApi.test.ts.snap +399 -0
  28. package/src/__snapshots__/openApiBuilder.test.ts.snap +108 -0
  29. package/src/api.test.ts +743 -2
  30. package/src/api.ts +209 -3
  31. package/src/auth.ts +3 -1
  32. package/src/errors.ts +14 -11
  33. package/src/example.ts +7 -7
  34. package/src/githubAuth.test.ts +3 -3
  35. package/src/index.ts +2 -0
  36. package/src/openApi.test.ts +8 -5
  37. package/src/openApiBuilder.ts +188 -15
  38. package/src/openApiValidator.test.ts +241 -0
  39. package/src/openApiValidator.ts +860 -0
  40. package/src/plugins.test.ts +3 -3
  41. package/src/terrenoPlugin.ts +5 -0
  42. package/src/tests.ts +34 -24
  43. package/.cursorrules +0 -107
  44. package/.windsurfrules +0 -107
  45. package/dist/response.d.ts +0 -0
  46. package/dist/response.js +0 -1
  47. package/index.ts +0 -1
  48. package/src/response.ts +0 -0
package/dist/auth.js CHANGED
@@ -377,7 +377,9 @@ function addAuthRoutes(app, userModel, authOptions) {
377
377
  logger_1.logger.warn("Invalid login: ".concat(info));
378
378
  return [2 /*return*/, res.status(401).json({ message: info === null || info === void 0 ? void 0 : info.message })];
379
379
  }
380
- logger_1.logger.info("User logged in: ".concat(user._id, ", type: ").concat(user.type || "N/A"));
380
+ if (process.env.NODE_ENV !== "test") {
381
+ logger_1.logger.info("User logged in: ".concat(user._id, ", type: ").concat(user.type || "N/A"));
382
+ }
381
383
  return [4 /*yield*/, (0, exports.generateTokens)(user, authOptions)];
382
384
  case 1:
383
385
  tokens = _a.sent();
package/dist/errors.js CHANGED
@@ -125,21 +125,24 @@ exports.APIError = APIError;
125
125
  // model.
126
126
  function errorsPlugin(schema) {
127
127
  var errorSchema = new mongoose_1.Schema({
128
- code: String,
129
- detail: String,
130
- id: String,
128
+ code: { description: "Application-specific error code", type: String },
129
+ detail: { description: "Human-readable explanation of the error", type: String },
130
+ id: { description: "Unique identifier for this error occurrence", type: String },
131
131
  links: {
132
- about: String,
133
- type: String,
132
+ about: { description: "Link to documentation about this error", type: String },
133
+ type: { description: "Link describing the error type", type: String },
134
134
  },
135
- meta: mongoose_1.Schema.Types.Mixed,
135
+ meta: { description: "Non-standard meta information about the error", type: mongoose_1.Schema.Types.Mixed },
136
136
  source: {
137
- header: String,
138
- parameter: String,
139
- pointer: String,
137
+ header: { description: "HTTP header that caused the error", type: String },
138
+ parameter: { description: "Query parameter that caused the error", type: String },
139
+ pointer: {
140
+ description: "JSON pointer to the request field that caused the error",
141
+ type: String,
142
+ },
140
143
  },
141
- status: Number,
142
- title: { required: true, type: String },
144
+ status: { description: "HTTP status code for this error", type: Number },
145
+ title: { description: "Short summary of the error", required: true, type: String },
143
146
  });
144
147
  schema.add({ apiErrors: errorSchema });
145
148
  }
package/dist/example.js CHANGED
@@ -65,19 +65,19 @@ mongoose_1.default
65
65
  logger_1.logger.error("Error connecting to mongo ".concat(err));
66
66
  });
67
67
  var userSchema = new mongoose_1.Schema({
68
- admin: { default: false, type: Boolean },
69
- username: String,
68
+ admin: { default: false, description: "Whether the user has admin privileges", type: Boolean },
69
+ username: { description: "The user's username", type: String },
70
70
  });
71
71
  userSchema.plugin(passport_local_mongoose_1.default, { usernameField: "email" });
72
72
  userSchema.plugin(plugins_1.createdUpdatedPlugin);
73
73
  userSchema.plugin(plugins_1.baseUserPlugin);
74
74
  var UserModel = (0, mongoose_1.model)("User", userSchema);
75
75
  var schema = new mongoose_1.Schema({
76
- calories: Number,
77
- created: Date,
78
- hidden: { default: false, type: Boolean },
79
- name: String,
80
- ownerId: { ref: "User", type: "ObjectId" },
76
+ calories: { description: "Number of calories in the food", type: Number },
77
+ created: { description: "When this food was created", type: Date },
78
+ hidden: { default: false, description: "Whether this food is hidden from listings", type: Boolean },
79
+ name: { description: "The name of the food", type: String },
80
+ ownerId: { description: "The user who owns this food entry", ref: "User", type: "ObjectId" },
81
81
  });
82
82
  var FoodModel = (0, mongoose_1.model)("Food", schema);
83
83
  function getBaseServer() {
@@ -82,9 +82,9 @@ var logger_1 = require("./logger");
82
82
  var plugins_1 = require("./plugins");
83
83
  // Create schema for GitHub-enabled user
84
84
  var testUserSchema = new mongoose_1.Schema({
85
- admin: { default: false, type: Boolean },
86
- name: String,
87
- username: String,
85
+ admin: { default: false, description: "Whether the user has admin privileges", type: Boolean },
86
+ name: { description: "The user's display name", type: String },
87
+ username: { description: "The user's username", type: String },
88
88
  });
89
89
  testUserSchema.plugin(passport_local_mongoose_1.default, {
90
90
  attemptsField: "attempts",
package/dist/index.d.ts CHANGED
@@ -8,8 +8,10 @@ export * from "./middleware";
8
8
  export * from "./notifiers";
9
9
  export * from "./openApiBuilder";
10
10
  export * from "./openApiEtag";
11
+ export * from "./openApiValidator";
11
12
  export * from "./permissions";
12
13
  export * from "./plugins";
13
14
  export * from "./populate";
15
+ export * from "./terrenoPlugin";
14
16
  export * from "./transformers";
15
17
  export * from "./utils";
package/dist/index.js CHANGED
@@ -24,8 +24,10 @@ __exportStar(require("./middleware"), exports);
24
24
  __exportStar(require("./notifiers"), exports);
25
25
  __exportStar(require("./openApiBuilder"), exports);
26
26
  __exportStar(require("./openApiEtag"), exports);
27
+ __exportStar(require("./openApiValidator"), exports);
27
28
  __exportStar(require("./permissions"), exports);
28
29
  __exportStar(require("./plugins"), exports);
29
30
  __exportStar(require("./populate"), exports);
31
+ __exportStar(require("./terrenoPlugin"), exports);
30
32
  __exportStar(require("./transformers"), exports);
31
33
  __exportStar(require("./utils"), exports);
@@ -273,13 +273,13 @@ function addRoutes(router, options) {
273
273
  // Ensure that a Number query field supports gt/gte/lt/lte and just a Number
274
274
  (0, bun_test_1.expect)(foodQuery.schema).toEqual({
275
275
  oneOf: [
276
- { type: "number" },
276
+ { description: "Number of calories in the food", type: "number" },
277
277
  {
278
278
  properties: {
279
- $gt: { type: "number" },
280
- $gte: { type: "number" },
281
- $lt: { type: "number" },
282
- $lte: { type: "number" },
279
+ $gt: { description: "Number of calories in the food", type: "number" },
280
+ $gte: { description: "Number of calories in the food", type: "number" },
281
+ $lt: { description: "Number of calories in the food", type: "number" },
282
+ $lte: { description: "Number of calories in the food", type: "number" },
283
283
  },
284
284
  type: "object",
285
285
  },
@@ -393,6 +393,7 @@ function addRoutesPopulate(router, options) {
393
393
  type: "string",
394
394
  },
395
395
  name: {
396
+ description: "The user's display name",
396
397
  type: "string",
397
398
  },
398
399
  },
@@ -406,12 +407,14 @@ function addRoutesPopulate(router, options) {
406
407
  type: "array",
407
408
  });
408
409
  (0, bun_test_1.expect)(properties.likesIds).toEqual({
410
+ description: "User likes for this food",
409
411
  items: {
410
412
  properties: {
411
413
  _id: {
412
414
  type: "string",
413
415
  },
414
416
  likes: {
417
+ description: "Whether the user liked the item",
415
418
  type: "boolean",
416
419
  },
417
420
  userId: {
@@ -153,6 +153,20 @@ export type OpenApiResponse = {
153
153
  };
154
154
  };
155
155
  };
156
+ /**
157
+ * Result from building OpenAPI middleware with schemas exposed.
158
+ * Useful when you want to use the schemas with asyncHandler's validation.
159
+ */
160
+ export interface OpenApiBuildResult {
161
+ /** The OpenAPI documentation middleware */
162
+ middleware: any;
163
+ /** Request body schema if defined */
164
+ bodySchema?: Record<string, OpenApiSchemaProperty>;
165
+ /** Query parameter schemas if defined */
166
+ querySchema?: Record<string, OpenApiSchemaProperty>;
167
+ /** Whether validation was enabled on this builder */
168
+ validationEnabled: boolean;
169
+ }
156
170
  /**
157
171
  * A fluent builder for constructing OpenAPI middleware.
158
172
  *
@@ -184,6 +198,12 @@ export declare class OpenApiMiddlewareBuilder {
184
198
  private options;
185
199
  /** Accumulated OpenAPI configuration from builder methods */
186
200
  private config;
201
+ /** Validation configuration */
202
+ private validationConfig;
203
+ /** Store the raw request body schema for validation */
204
+ private requestBodySchema?;
205
+ /** Store the raw query parameter schemas for validation */
206
+ private queryParamSchemas;
187
207
  /**
188
208
  * Creates a new OpenApiMiddlewareBuilder instance.
189
209
  *
@@ -365,6 +385,51 @@ export declare class OpenApiMiddlewareBuilder {
365
385
  withPathParameter(name: string, schema: OpenApiSchemaProperty, options?: {
366
386
  description?: string;
367
387
  }): this;
388
+ /**
389
+ * Enables runtime validation for this route.
390
+ *
391
+ * When enabled, the built middleware will validate incoming requests
392
+ * against the documented schema before the handler runs.
393
+ *
394
+ * @param options - Optional configuration for validation
395
+ * @param options.body - Enable body validation (default: true if request body is defined)
396
+ * @param options.query - Enable query parameter validation (default: true if query params are defined)
397
+ * @param options.enabled - Override the global validation enabled setting
398
+ * @returns The builder instance for chaining
399
+ *
400
+ * @example
401
+ * ```typescript
402
+ * createOpenApiBuilder(options)
403
+ * .withRequestBody<{name: string}>({name: {type: "string", required: true}})
404
+ * .withValidation() // Enable validation
405
+ * .build();
406
+ * ```
407
+ */
408
+ withValidation(options?: {
409
+ body?: boolean;
410
+ query?: boolean;
411
+ enabled?: boolean;
412
+ }): this;
413
+ /**
414
+ * Builds and returns the OpenAPI middleware along with schemas.
415
+ *
416
+ * This method is useful when you want to use asyncHandler's integrated
417
+ * validation instead of separate validation middleware.
418
+ *
419
+ * @returns Object containing middleware and schemas
420
+ *
421
+ * @example
422
+ * ```typescript
423
+ * const {middleware, bodySchema} = createOpenApiBuilder(options)
424
+ * .withRequestBody<{name: string}>({name: {type: "string", required: true}})
425
+ * .buildWithSchemas();
426
+ *
427
+ * router.post("/users", middleware, asyncHandler(async (req, res) => {
428
+ * // handler code
429
+ * }, {bodySchema, validate: true}));
430
+ * ```
431
+ */
432
+ buildWithSchemas(): OpenApiBuildResult;
368
433
  /**
369
434
  * Builds and returns the OpenAPI middleware.
370
435
  *
@@ -372,10 +437,13 @@ export declare class OpenApiMiddlewareBuilder {
372
437
  * that integrates with the OpenAPI documentation system. If no OpenAPI
373
438
  * path is configured in options, returns a no-op middleware.
374
439
  *
440
+ * If validation was enabled via `withValidation()`, returns an array
441
+ * of middleware: [openApiDocMiddleware, validationMiddleware].
442
+ *
375
443
  * Default error responses (400, 401, 403, 404, 405) are automatically
376
444
  * merged with the configured responses.
377
445
  *
378
- * @returns Express middleware function for OpenAPI documentation
446
+ * @returns Express middleware function(s) for OpenAPI documentation and optional validation
379
447
  *
380
448
  * @example
381
449
  * ```typescript
@@ -66,6 +66,7 @@ exports.createOpenApiBuilder = createOpenApiBuilder;
66
66
  var merge_1 = __importDefault(require("lodash/merge"));
67
67
  var logger_1 = require("./logger");
68
68
  var openApi_1 = require("./openApi");
69
+ var openApiValidator_1 = require("./openApiValidator");
69
70
  /**
70
71
  * A fluent builder for constructing OpenAPI middleware.
71
72
  *
@@ -99,10 +100,13 @@ var OpenApiMiddlewareBuilder = /** @class */ (function () {
99
100
  * @param options - Router options containing the OpenAPI path configuration
100
101
  */
101
102
  function OpenApiMiddlewareBuilder(options) {
103
+ /** Store the raw query parameter schemas for validation */
104
+ this.queryParamSchemas = {};
102
105
  this.options = options;
103
106
  this.config = {
104
107
  responses: {},
105
108
  };
109
+ this.validationConfig = {};
106
110
  }
107
111
  /**
108
112
  * Sets the tags for the OpenAPI operation.
@@ -201,6 +205,8 @@ var OpenApiMiddlewareBuilder = /** @class */ (function () {
201
205
  _c),
202
206
  required: (_e = options === null || options === void 0 ? void 0 : options.required) !== null && _e !== void 0 ? _e : true,
203
207
  };
208
+ // Store the schema for validation
209
+ this.requestBodySchema = schema;
204
210
  return this;
205
211
  };
206
212
  /**
@@ -324,6 +330,8 @@ var OpenApiMiddlewareBuilder = /** @class */ (function () {
324
330
  required: (_c = options === null || options === void 0 ? void 0 : options.required) !== null && _c !== void 0 ? _c : false,
325
331
  schema: schema,
326
332
  });
333
+ // Store for validation
334
+ this.queryParamSchemas[name] = __assign(__assign({}, schema), { required: options === null || options === void 0 ? void 0 : options.required });
327
335
  return this;
328
336
  };
329
337
  /**
@@ -358,6 +366,74 @@ var OpenApiMiddlewareBuilder = /** @class */ (function () {
358
366
  });
359
367
  return this;
360
368
  };
369
+ /**
370
+ * Enables runtime validation for this route.
371
+ *
372
+ * When enabled, the built middleware will validate incoming requests
373
+ * against the documented schema before the handler runs.
374
+ *
375
+ * @param options - Optional configuration for validation
376
+ * @param options.body - Enable body validation (default: true if request body is defined)
377
+ * @param options.query - Enable query parameter validation (default: true if query params are defined)
378
+ * @param options.enabled - Override the global validation enabled setting
379
+ * @returns The builder instance for chaining
380
+ *
381
+ * @example
382
+ * ```typescript
383
+ * createOpenApiBuilder(options)
384
+ * .withRequestBody<{name: string}>({name: {type: "string", required: true}})
385
+ * .withValidation() // Enable validation
386
+ * .build();
387
+ * ```
388
+ */
389
+ OpenApiMiddlewareBuilder.prototype.withValidation = function (options) {
390
+ var _c, _d, _e;
391
+ this.validationConfig = {
392
+ enabled: (_c = options === null || options === void 0 ? void 0 : options.enabled) !== null && _c !== void 0 ? _c : true,
393
+ validateBody: (_d = options === null || options === void 0 ? void 0 : options.body) !== null && _d !== void 0 ? _d : true,
394
+ validateQuery: (_e = options === null || options === void 0 ? void 0 : options.query) !== null && _e !== void 0 ? _e : true,
395
+ };
396
+ return this;
397
+ };
398
+ /**
399
+ * Builds and returns the OpenAPI middleware along with schemas.
400
+ *
401
+ * This method is useful when you want to use asyncHandler's integrated
402
+ * validation instead of separate validation middleware.
403
+ *
404
+ * @returns Object containing middleware and schemas
405
+ *
406
+ * @example
407
+ * ```typescript
408
+ * const {middleware, bodySchema} = createOpenApiBuilder(options)
409
+ * .withRequestBody<{name: string}>({name: {type: "string", required: true}})
410
+ * .buildWithSchemas();
411
+ *
412
+ * router.post("/users", middleware, asyncHandler(async (req, res) => {
413
+ * // handler code
414
+ * }, {bodySchema, validate: true}));
415
+ * ```
416
+ */
417
+ OpenApiMiddlewareBuilder.prototype.buildWithSchemas = function () {
418
+ var _c, _d, _e, _f, _g;
419
+ var noop = function (_a, _b, next) { return next(); };
420
+ // Build the OpenAPI documentation middleware only (no validation middleware)
421
+ var openApiMiddleware = noop;
422
+ if ((_c = this.options.openApi) === null || _c === void 0 ? void 0 : _c.path) {
423
+ openApiMiddleware = this.options.openApi.path((0, merge_1.default)(__assign(__assign({}, this.config), { responses: __assign(__assign({}, this.config.responses), openApi_1.defaultOpenApiErrorResponses) }), (_e = (_d = this.options.openApiOverwrite) === null || _d === void 0 ? void 0 : _d.get) !== null && _e !== void 0 ? _e : {}));
424
+ }
425
+ else {
426
+ logger_1.logger.debug("No options.openApi provided, skipping OpenApiMiddleware");
427
+ }
428
+ var globalConfig = (0, openApiValidator_1.getOpenApiValidatorConfig)();
429
+ var validationEnabled = (_f = this.validationConfig.enabled) !== null && _f !== void 0 ? _f : ((0, openApiValidator_1.isOpenApiValidatorConfigured)() && ((_g = globalConfig.validateRequests) !== null && _g !== void 0 ? _g : false));
430
+ return {
431
+ bodySchema: this.requestBodySchema,
432
+ middleware: openApiMiddleware,
433
+ querySchema: Object.keys(this.queryParamSchemas).length > 0 ? this.queryParamSchemas : undefined,
434
+ validationEnabled: validationEnabled,
435
+ };
436
+ };
361
437
  /**
362
438
  * Builds and returns the OpenAPI middleware.
363
439
  *
@@ -365,10 +441,13 @@ var OpenApiMiddlewareBuilder = /** @class */ (function () {
365
441
  * that integrates with the OpenAPI documentation system. If no OpenAPI
366
442
  * path is configured in options, returns a no-op middleware.
367
443
  *
444
+ * If validation was enabled via `withValidation()`, returns an array
445
+ * of middleware: [openApiDocMiddleware, validationMiddleware].
446
+ *
368
447
  * Default error responses (400, 401, 403, 404, 405) are automatically
369
448
  * merged with the configured responses.
370
449
  *
371
- * @returns Express middleware function for OpenAPI documentation
450
+ * @returns Express middleware function(s) for OpenAPI documentation and optional validation
372
451
  *
373
452
  * @example
374
453
  * ```typescript
@@ -381,13 +460,38 @@ var OpenApiMiddlewareBuilder = /** @class */ (function () {
381
460
  * ```
382
461
  */
383
462
  OpenApiMiddlewareBuilder.prototype.build = function () {
384
- var _c, _d, _e;
463
+ var _c, _d, _e, _f, _g;
385
464
  var noop = function (_a, _b, next) { return next(); };
386
- if (!((_c = this.options.openApi) === null || _c === void 0 ? void 0 : _c.path)) {
465
+ // Build the OpenAPI documentation middleware
466
+ var openApiMiddleware = noop;
467
+ if ((_c = this.options.openApi) === null || _c === void 0 ? void 0 : _c.path) {
468
+ openApiMiddleware = this.options.openApi.path((0, merge_1.default)(__assign(__assign({}, this.config), { responses: __assign(__assign({}, this.config.responses), openApi_1.defaultOpenApiErrorResponses) }), (_e = (_d = this.options.openApiOverwrite) === null || _d === void 0 ? void 0 : _d.get) !== null && _e !== void 0 ? _e : {}));
469
+ }
470
+ else {
387
471
  logger_1.logger.debug("No options.openApi provided, skipping OpenApiMiddleware");
388
- return noop;
389
472
  }
390
- return this.options.openApi.path((0, merge_1.default)(__assign(__assign({}, this.config), { responses: __assign(__assign({}, this.config.responses), openApi_1.defaultOpenApiErrorResponses) }), (_e = (_d = this.options.openApiOverwrite) === null || _d === void 0 ? void 0 : _d.get) !== null && _e !== void 0 ? _e : {}));
473
+ // Check if validation should be enabled
474
+ var globalConfig = (0, openApiValidator_1.getOpenApiValidatorConfig)();
475
+ var shouldValidate = (_f = this.validationConfig.enabled) !== null && _f !== void 0 ? _f : ((0, openApiValidator_1.isOpenApiValidatorConfigured)() && ((_g = globalConfig.validateRequests) !== null && _g !== void 0 ? _g : false));
476
+ if (!shouldValidate) {
477
+ return openApiMiddleware;
478
+ }
479
+ // Build validation middleware
480
+ var validators = [openApiMiddleware];
481
+ // Add body validation if we have a request body schema
482
+ if (this.validationConfig.validateBody && this.requestBodySchema) {
483
+ validators.push((0, openApiValidator_1.validateRequestBody)(this.requestBodySchema, { enabled: true }));
484
+ }
485
+ // Add query validation if we have query parameter schemas
486
+ if (this.validationConfig.validateQuery && Object.keys(this.queryParamSchemas).length > 0) {
487
+ validators.push((0, openApiValidator_1.validateQueryParams)(this.queryParamSchemas, { enabled: true }));
488
+ }
489
+ // If only one middleware (the openApi one), return it directly
490
+ if (validators.length === 1) {
491
+ return openApiMiddleware;
492
+ }
493
+ // Return array of middleware to be spread in route definition
494
+ return validators;
391
495
  };
392
496
  return OpenApiMiddlewareBuilder;
393
497
  }());