@terreno/api 0.0.17 → 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 (77) 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 +333 -0
  5. package/README.md +76 -7
  6. package/biome.jsonc +1 -1
  7. package/dist/api.d.ts +68 -1
  8. package/dist/api.js +140 -5
  9. package/dist/api.query.test.js +1 -1
  10. package/dist/api.test.js +222 -484
  11. package/dist/auth.js +3 -1
  12. package/dist/errors.js +15 -12
  13. package/dist/example.js +7 -7
  14. package/dist/expressServer.d.ts +8 -2
  15. package/dist/expressServer.js +8 -1
  16. package/dist/githubAuth.d.ts +64 -0
  17. package/dist/githubAuth.js +293 -0
  18. package/dist/githubAuth.test.d.ts +1 -0
  19. package/dist/githubAuth.test.js +351 -0
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.js +3 -0
  22. package/dist/logger.js +1 -1
  23. package/dist/middleware.js +1 -1
  24. package/dist/notifiers/googleChatNotifier.js +1 -1
  25. package/dist/notifiers/googleChatNotifier.test.js +1 -1
  26. package/dist/notifiers/slackNotifier.js +1 -1
  27. package/dist/notifiers/slackNotifier.test.js +1 -1
  28. package/dist/notifiers/zoomNotifier.js +1 -1
  29. package/dist/notifiers/zoomNotifier.test.js +1 -1
  30. package/dist/openApi.test.js +8 -5
  31. package/dist/openApiBuilder.d.ts +69 -1
  32. package/dist/openApiBuilder.js +109 -5
  33. package/dist/openApiValidator.d.ts +296 -0
  34. package/dist/openApiValidator.js +698 -0
  35. package/dist/openApiValidator.test.d.ts +1 -0
  36. package/dist/openApiValidator.test.js +346 -0
  37. package/dist/permissions.js +1 -1
  38. package/dist/plugins.test.js +3 -3
  39. package/dist/terrenoPlugin.d.ts +4 -0
  40. package/dist/terrenoPlugin.js +2 -0
  41. package/dist/tests/bunSetup.js +2 -2
  42. package/dist/tests.js +34 -24
  43. package/package.json +7 -2
  44. package/src/__snapshots__/openApi.test.ts.snap +399 -0
  45. package/src/__snapshots__/openApiBuilder.test.ts.snap +108 -0
  46. package/src/api.query.test.ts +1 -1
  47. package/src/api.test.ts +161 -374
  48. package/src/api.ts +210 -4
  49. package/src/auth.ts +3 -1
  50. package/src/errors.ts +15 -12
  51. package/src/example.ts +7 -7
  52. package/src/expressServer.ts +18 -2
  53. package/src/githubAuth.test.ts +223 -0
  54. package/src/githubAuth.ts +335 -0
  55. package/src/index.ts +3 -0
  56. package/src/logger.ts +1 -1
  57. package/src/middleware.ts +1 -1
  58. package/src/notifiers/googleChatNotifier.test.ts +1 -1
  59. package/src/notifiers/googleChatNotifier.ts +1 -1
  60. package/src/notifiers/slackNotifier.test.ts +1 -1
  61. package/src/notifiers/slackNotifier.ts +1 -1
  62. package/src/notifiers/zoomNotifier.test.ts +1 -1
  63. package/src/notifiers/zoomNotifier.ts +1 -1
  64. package/src/openApi.test.ts +8 -5
  65. package/src/openApiBuilder.ts +188 -15
  66. package/src/openApiValidator.test.ts +241 -0
  67. package/src/openApiValidator.ts +860 -0
  68. package/src/permissions.ts +1 -1
  69. package/src/plugins.test.ts +3 -3
  70. package/src/terrenoPlugin.ts +5 -0
  71. package/src/tests/bunSetup.ts +2 -2
  72. package/src/tests.ts +34 -24
  73. package/CLAUDE.md +0 -107
  74. package/dist/response.d.ts +0 -0
  75. package/dist/response.js +0 -1
  76. package/index.ts +0 -1
  77. package/src/response.ts +0 -0
package/README.md CHANGED
@@ -9,6 +9,14 @@ These APIs integrate with @terreno/rtk to create consistent types on the fronten
9
9
  and backend, and automatically generated React hooks to fetch, query, and modify
10
10
  model instances.
11
11
 
12
+ ## Features
13
+
14
+ - **modelRouter** — Automatic CRUD endpoints for Mongoose models
15
+ - **Authentication** — JWT with email/password and GitHub OAuth support
16
+ - **Permissions** — Fine-grained access control (IsAuthenticated, IsOwner, IsAdmin, etc.)
17
+ - **OpenAPI** — Automatic spec generation from models and routes
18
+ - **Logging** — Winston-based logging with Google Cloud and Sentry support
19
+
12
20
  ## Getting started
13
21
 
14
22
  To install:
@@ -46,12 +54,25 @@ const eventSchema = new Schema({
46
54
  Assuming we have a model:
47
55
 
48
56
  const foodSchema = new Schema<Food>({
49
- name: String,
50
- hidden: {type: Boolean, default: false},
51
- ownerId: {type: "ObjectId", ref: "User"},
57
+ name: {
58
+ description: "Name of the food item",
59
+ type: String,
60
+ },
61
+ hidden: {
62
+ description: "Whether the food is hidden from the list",
63
+ type: Boolean,
64
+ default: false,
65
+ },
66
+ ownerId: {
67
+ description: "The user who added this food",
68
+ type: "ObjectId",
69
+ ref: "User",
70
+ },
52
71
  });
53
72
  export const FoodModel = model("Food", foodSchema);
54
73
 
74
+ **Important:** Every field must include a `description` property. This requirement ensures that the auto-generated OpenAPI specification and SDK have meaningful documentation for all fields.
75
+
55
76
  We can expose this model as an API like this:
56
77
 
57
78
  import express from "express";
@@ -99,14 +120,62 @@ Now we can perform operations on the Food model in a standard REST way. We've al
99
120
 
100
121
  You can create your own permissions functions. Check permissions.ts for some examples of how to write them.
101
122
 
123
+ ## Authentication
124
+
125
+ @terreno/api includes built-in authentication with JWT and OAuth support.
126
+
127
+ ### Email/Password Authentication
128
+
129
+ Built-in email/password authentication using `passport-local-mongoose`:
130
+
131
+ ```typescript
132
+ import {setupServer} from "@terreno/api";
133
+ import {User} from "./models/user";
134
+
135
+ setupServer({
136
+ userModel: User,
137
+ addRoutes: (router) => {
138
+ // Your routes here
139
+ },
140
+ });
141
+ ```
142
+
143
+ This automatically adds:
144
+ - `POST /auth/signup` — User registration
145
+ - `POST /auth/login` — Authentication
146
+ - `POST /auth/refresh_token` — Token refresh
147
+ - `GET /auth/me` — Get current user
148
+ - `PATCH /auth/me` — Update current user
149
+
150
+ ### GitHub OAuth
151
+
152
+ Add GitHub OAuth login to your API:
153
+
154
+ ```typescript
155
+ import {githubUserPlugin, setupServer} from "@terreno/api";
156
+
157
+ // Add GitHub fields to user schema
158
+ userSchema.plugin(githubUserPlugin);
159
+
160
+ setupServer({
161
+ userModel: User,
162
+ githubAuth: {
163
+ clientId: process.env.GITHUB_CLIENT_ID!,
164
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
165
+ callbackURL: process.env.GITHUB_CALLBACK_URL!,
166
+ },
167
+ });
168
+ ```
169
+
170
+ **Learn more:** See the [GitHub OAuth how-to guide](../docs/how-to/add-github-oauth.md) for complete setup instructions.
171
+
102
172
  ## Sentry
103
173
  To enable Sentry, create a "src/sentryInstrumment.ts" file in your project.
104
174
 
105
- ```
106
- // Include dotenv here at the start if you're including configuration from dot files.
107
- import "dotenv/config";
175
+ > **Note:** Bun automatically loads `.env` files before your code runs, so there's no need for `dotenv`. Just place a `.env` file in your project root and `process.env` will have your variables available immediately. See [Bun .env docs](https://bun.sh/docs/runtime/env).
108
176
 
109
- import * as Sentry from "@sentry/node";
177
+ ```
178
+ import * as Sentry from "@sentry/bun";
110
179
  import {nodeProfilingIntegration} from "@sentry/profiling-node";
111
180
 
112
181
  if (process.env.NODE_ENV === "production" && !process.env.SENTRY_DSN) {
package/biome.jsonc CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "root": true,
3
- "$schema": "https://biomejs.dev/schemas/2.3.12/schema.json",
3
+ "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
4
4
  "extends": ["../biome.jsonc"],
5
5
  "files": {
6
6
  "includes": [
package/dist/api.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import express, { type NextFunction, type Request, type Response } from "express";
2
2
  import mongoose, { type Document, type Model } from "mongoose";
3
3
  import { type User } from "./auth";
4
+ import { type ModelRouterValidationOptions } from "./openApiValidator";
4
5
  import { type RESTPermissions } from "./permissions";
5
6
  import type { PopulatePath } from "./populate";
6
7
  import { type TerrenoTransformer } from "./transformers";
@@ -203,6 +204,19 @@ export interface ModelRouterOptions<T> {
203
204
  * that you want to be documented and typed in the SDK.
204
205
  */
205
206
  openApiExtraModelProperties?: any;
207
+ /**
208
+ * Enable runtime validation of request bodies against the OpenAPI schema.
209
+ * When enabled, requests that don't match the documented schema will return 400 errors.
210
+ *
211
+ * Can be set to:
212
+ * - `true`: Enable validation for create and update operations
213
+ * - `false`: Disable validation (default)
214
+ * - Object with `validateCreate` and `validateUpdate` booleans for fine-grained control
215
+ *
216
+ * Note: Global validation can be enabled via `configureOpenApiValidator()`.
217
+ * This option overrides the global setting for this specific router.
218
+ */
219
+ validation?: boolean | ModelRouterValidationOptions;
206
220
  }
207
221
  /**
208
222
  * Create a set of CRUD routes given a Mongoose model and configuration options.
@@ -211,6 +225,59 @@ export interface ModelRouterOptions<T> {
211
225
  * @param options Options for configuring the REST API, such as permissions, transformers, and hooks.
212
226
  */
213
227
  export declare function modelRouter<T>(model: Model<T>, options: ModelRouterOptions<T>): express.Router;
214
- export declare const asyncHandler: (fn: any) => (req: Request, res: Response, next: NextFunction) => Promise<any>;
228
+ /**
229
+ * Options for the asyncHandler function.
230
+ */
231
+ export interface AsyncHandlerOptions {
232
+ /**
233
+ * Schema for validating request body.
234
+ * When provided and validation is enabled, the request body will be validated
235
+ * against this schema before the handler runs.
236
+ */
237
+ bodySchema?: Record<string, import("./openApiBuilder").OpenApiSchemaProperty>;
238
+ /**
239
+ * Schema for validating query parameters.
240
+ * When provided and validation is enabled, query params will be validated
241
+ * against this schema before the handler runs.
242
+ */
243
+ querySchema?: Record<string, import("./openApiBuilder").OpenApiSchemaProperty>;
244
+ /**
245
+ * Override global validation setting for this handler.
246
+ * - `true`: Enable validation regardless of global setting
247
+ * - `false`: Disable validation regardless of global setting
248
+ * - `undefined`: Use global setting
249
+ */
250
+ validate?: boolean;
251
+ }
252
+ /**
253
+ * Wraps async route handlers to properly catch and forward errors.
254
+ *
255
+ * Since Express doesn't handle async routes well, wrap them with this function.
256
+ * Optionally supports integrated request validation.
257
+ *
258
+ * @param fn - The async route handler function
259
+ * @param options - Optional configuration for validation
260
+ * @returns Express middleware function
261
+ *
262
+ * @example
263
+ * ```typescript
264
+ * // Basic usage without validation
265
+ * router.post("/users", asyncHandler(async (req, res) => {
266
+ * // handler code
267
+ * }));
268
+ *
269
+ * // With integrated validation
270
+ * router.post("/users", asyncHandler(async (req, res) => {
271
+ * // handler code - body is already validated
272
+ * }, {
273
+ * bodySchema: {
274
+ * name: {type: "string", required: true},
275
+ * email: {type: "string", format: "email", required: true},
276
+ * },
277
+ * validate: true,
278
+ * }));
279
+ * ```
280
+ */
281
+ export declare const asyncHandler: (fn: any, options?: AsyncHandlerOptions) => (req: Request, res: Response, next: NextFunction) => void;
215
282
  export declare const gooseRestRouter: typeof modelRouter;
216
283
  export type GooseRESTOptions<T> = ModelRouterOptions<T>;
package/dist/api.js CHANGED
@@ -127,7 +127,7 @@ exports.modelRouter = modelRouter;
127
127
  *
128
128
  * @packageDocumentation
129
129
  */
130
- var Sentry = __importStar(require("@sentry/node"));
130
+ var Sentry = __importStar(require("@sentry/bun"));
131
131
  var express_1 = __importDefault(require("express"));
132
132
  var cloneDeep_1 = __importDefault(require("lodash/cloneDeep"));
133
133
  var mongoose_1 = __importDefault(require("mongoose"));
@@ -135,6 +135,7 @@ var auth_1 = require("./auth");
135
135
  var errors_1 = require("./errors");
136
136
  var logger_1 = require("./logger");
137
137
  var openApi_1 = require("./openApi");
138
+ var openApiValidator_1 = require("./openApiValidator");
138
139
  var permissions_1 = require("./permissions");
139
140
  var transformers_1 = require("./transformers");
140
141
  var utils_1 = require("./utils");
@@ -227,6 +228,62 @@ function checkQueryParamAllowed(queryParam, queryParamValue, queryFields) {
227
228
  //
228
229
  // return result;
229
230
  // }
231
+ // Helper to determine if validation should be enabled for a specific operation.
232
+ // When options.validation is not set, returns true — the middleware's own
233
+ // isConfigured check will decide whether to actually validate.
234
+ function shouldValidate(options, operation) {
235
+ var _a, _b, _c;
236
+ // Check route-specific validation option first
237
+ if (options.validation !== undefined) {
238
+ if (typeof options.validation === "boolean") {
239
+ return options.validation;
240
+ }
241
+ if (operation === "create") {
242
+ return (_a = options.validation.validateCreate) !== null && _a !== void 0 ? _a : true;
243
+ }
244
+ if (operation === "update") {
245
+ return (_b = options.validation.validateUpdate) !== null && _b !== void 0 ? _b : true;
246
+ }
247
+ return (_c = options.validation.validateQuery) !== null && _c !== void 0 ? _c : true;
248
+ }
249
+ // Default: let middleware's isConfigured check decide
250
+ return true;
251
+ }
252
+ // Get body validation middleware if validation is enabled
253
+ function getBodyValidationMiddleware(model, options, operation) {
254
+ var validationOptions = {};
255
+ if (!shouldValidate(options, operation)) {
256
+ validationOptions.enabled = false;
257
+ }
258
+ if (typeof options.validation === "object") {
259
+ if (options.validation.onError) {
260
+ validationOptions.onError = options.validation.onError;
261
+ }
262
+ if (options.validation.onAdditionalPropertiesRemoved) {
263
+ validationOptions.onAdditionalPropertiesRemoved =
264
+ options.validation.onAdditionalPropertiesRemoved;
265
+ }
266
+ var excludeFields = operation === "create"
267
+ ? options.validation.excludeFromCreate
268
+ : options.validation.excludeFromUpdate;
269
+ if (excludeFields === null || excludeFields === void 0 ? void 0 : excludeFields.length) {
270
+ validationOptions.excludeFields = excludeFields;
271
+ }
272
+ }
273
+ return (0, openApiValidator_1.validateModelRequestBody)(model, validationOptions);
274
+ }
275
+ // Get query validation middleware if validation is enabled
276
+ function getQueryValidationMiddleware(model, options) {
277
+ var querySchema = (0, openApiValidator_1.buildQuerySchemaFromFields)(model, options.queryFields);
278
+ var validationOptions = {};
279
+ if (!shouldValidate(options, "query")) {
280
+ validationOptions.enabled = false;
281
+ }
282
+ if (typeof options.validation === "object" && options.validation.onError) {
283
+ validationOptions.onError = options.validation.onError;
284
+ }
285
+ return (0, openApiValidator_1.validateQueryParams)(querySchema, validationOptions);
286
+ }
230
287
  /**
231
288
  * Create a set of CRUD routes given a Mongoose model and configuration options.
232
289
  *
@@ -242,10 +299,15 @@ function modelRouter(model, options) {
242
299
  options.endpoints(router);
243
300
  }
244
301
  var responseHandler = (_a = options.responseHandler) !== null && _a !== void 0 ? _a : transformers_1.defaultResponseHandler;
302
+ // Always install validation middleware — they are no-ops until configureOpenApiValidator() is called
303
+ var createValidation = getBodyValidationMiddleware(model, options, "create");
304
+ var updateValidation = getBodyValidationMiddleware(model, options, "update");
305
+ var queryValidation = getQueryValidationMiddleware(model, options);
245
306
  router.post("/", [
246
307
  (0, auth_1.authenticateMiddleware)(options.allowAnonymous),
247
308
  (0, openApi_1.createOpenApiMiddleware)(model, options),
248
309
  (0, permissions_1.permissionMiddleware)(model, options),
310
+ createValidation,
249
311
  ], (0, exports.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
250
312
  var body, error_1, data, error_2, populateQuery, error_3, error_4, serialized, error_5;
251
313
  return __generator(this, function (_a) {
@@ -378,6 +440,7 @@ function modelRouter(model, options) {
378
440
  (0, auth_1.authenticateMiddleware)(options.allowAnonymous),
379
441
  (0, permissions_1.permissionMiddleware)(model, options),
380
442
  (0, openApi_1.listOpenApiMiddleware)(model, options),
443
+ queryValidation,
381
444
  ], (0, exports.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
382
445
  var query, _a, _b, queryParam, _c, _d, queryParam, queryFilter, error_6, limit, builtQuery, total, populatedQuery, data, error_7, serialized, error_8, more, msg;
383
446
  var e_4, _e, e_5, _f;
@@ -594,6 +657,7 @@ function modelRouter(model, options) {
594
657
  (0, auth_1.authenticateMiddleware)(options.allowAnonymous),
595
658
  (0, openApi_1.patchOpenApiMiddleware)(model, options),
596
659
  (0, permissions_1.permissionMiddleware)(model, options),
660
+ updateValidation,
597
661
  ], (0, exports.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
598
662
  var doc, body, error_10, prevDoc, error_11, populateQuery, error_12, serialized, error_13;
599
663
  var _a;
@@ -991,10 +1055,81 @@ function modelRouter(model, options) {
991
1055
  router.use(errors_1.apiErrorMiddleware);
992
1056
  return router;
993
1057
  }
994
- // Since express doesn't handle async routes well, wrap them with this function.
995
- var asyncHandler = function (fn) { return function (req, res, next) {
996
- return Promise.resolve(fn(req, res, next)).catch(next);
997
- }; };
1058
+ /**
1059
+ * Wraps async route handlers to properly catch and forward errors.
1060
+ *
1061
+ * Since Express doesn't handle async routes well, wrap them with this function.
1062
+ * Optionally supports integrated request validation.
1063
+ *
1064
+ * @param fn - The async route handler function
1065
+ * @param options - Optional configuration for validation
1066
+ * @returns Express middleware function
1067
+ *
1068
+ * @example
1069
+ * ```typescript
1070
+ * // Basic usage without validation
1071
+ * router.post("/users", asyncHandler(async (req, res) => {
1072
+ * // handler code
1073
+ * }));
1074
+ *
1075
+ * // With integrated validation
1076
+ * router.post("/users", asyncHandler(async (req, res) => {
1077
+ * // handler code - body is already validated
1078
+ * }, {
1079
+ * bodySchema: {
1080
+ * name: {type: "string", required: true},
1081
+ * email: {type: "string", format: "email", required: true},
1082
+ * },
1083
+ * validate: true,
1084
+ * }));
1085
+ * ```
1086
+ */
1087
+ var asyncHandler = function (fn, options) {
1088
+ var _a, _b;
1089
+ // If no validation options, return simple handler
1090
+ if (!(options === null || options === void 0 ? void 0 : options.bodySchema) && !(options === null || options === void 0 ? void 0 : options.querySchema)) {
1091
+ return function (req, res, next) {
1092
+ return Promise.resolve(fn(req, res, next)).catch(next);
1093
+ };
1094
+ }
1095
+ // Import validation functions dynamically to avoid circular deps at module load
1096
+ var _c = require("./openApiValidator"), validateRequestBody = _c.validateRequestBody, validateQueryParams = _c.validateQueryParams, getOpenApiValidatorConfig = _c.getOpenApiValidatorConfig;
1097
+ // Build validation middleware
1098
+ var validators = [];
1099
+ // Determine if validation should be enabled
1100
+ var shouldValidate = (_b = (_a = options.validate) !== null && _a !== void 0 ? _a : getOpenApiValidatorConfig().validateRequests) !== null && _b !== void 0 ? _b : false;
1101
+ if (shouldValidate) {
1102
+ if (options.bodySchema) {
1103
+ validators.push(validateRequestBody(options.bodySchema, { enabled: true }));
1104
+ }
1105
+ if (options.querySchema) {
1106
+ validators.push(validateQueryParams(options.querySchema, { enabled: true }));
1107
+ }
1108
+ }
1109
+ return function (req, res, next) {
1110
+ // Run validators sequentially, then the handler
1111
+ var runValidators = function (index) {
1112
+ if (index >= validators.length) {
1113
+ // All validators passed, run the actual handler
1114
+ Promise.resolve(fn(req, res, next)).catch(next);
1115
+ return;
1116
+ }
1117
+ try {
1118
+ validators[index](req, res, function (err) {
1119
+ if (err) {
1120
+ next(err);
1121
+ return;
1122
+ }
1123
+ runValidators(index + 1);
1124
+ });
1125
+ }
1126
+ catch (err) {
1127
+ next(err);
1128
+ }
1129
+ };
1130
+ runValidators(0);
1131
+ };
1132
+ };
998
1133
  exports.asyncHandler = asyncHandler;
999
1134
  // For backwards compatibility with the old names.
1000
1135
  exports.gooseRestRouter = modelRouter;
@@ -89,7 +89,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
89
89
  };
90
90
  Object.defineProperty(exports, "__esModule", { value: true });
91
91
  var bun_test_1 = require("bun:test");
92
- var Sentry = __importStar(require("@sentry/node"));
92
+ var Sentry = __importStar(require("@sentry/bun"));
93
93
  var qs_1 = __importDefault(require("qs"));
94
94
  var supertest_1 = __importDefault(require("supertest"));
95
95
  var api_1 = require("./api");