@terreno/api 0.0.18 → 0.2.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 (71) hide show
  1. package/README.md +73 -3
  2. package/dist/api.d.ts +96 -3
  3. package/dist/api.js +159 -11
  4. package/dist/api.test.js +906 -2
  5. package/dist/auth.js +3 -1
  6. package/dist/betterAuth.d.ts +91 -0
  7. package/dist/betterAuth.js +8 -0
  8. package/dist/betterAuth.test.d.ts +1 -0
  9. package/dist/betterAuth.test.js +181 -0
  10. package/dist/betterAuthApp.d.ts +22 -0
  11. package/dist/betterAuthApp.js +38 -0
  12. package/dist/betterAuthApp.test.d.ts +1 -0
  13. package/dist/betterAuthApp.test.js +242 -0
  14. package/dist/betterAuthSetup.d.ts +60 -0
  15. package/dist/betterAuthSetup.js +278 -0
  16. package/dist/betterAuthSetup.test.d.ts +1 -0
  17. package/dist/betterAuthSetup.test.js +684 -0
  18. package/dist/errors.js +14 -11
  19. package/dist/example.js +7 -7
  20. package/dist/expressServer.js +2 -2
  21. package/dist/githubAuth.test.js +3 -3
  22. package/dist/index.d.ts +6 -0
  23. package/dist/index.js +6 -0
  24. package/dist/openApi.test.js +8 -5
  25. package/dist/openApiBuilder.d.ts +69 -1
  26. package/dist/openApiBuilder.js +109 -5
  27. package/dist/openApiValidator.d.ts +296 -0
  28. package/dist/openApiValidator.js +698 -0
  29. package/dist/openApiValidator.test.d.ts +1 -0
  30. package/dist/openApiValidator.test.js +346 -0
  31. package/dist/plugins.test.js +3 -3
  32. package/dist/terrenoApp.d.ts +189 -0
  33. package/dist/terrenoApp.js +352 -0
  34. package/dist/terrenoApp.test.d.ts +1 -0
  35. package/dist/terrenoApp.test.js +264 -0
  36. package/dist/terrenoPlugin.d.ts +38 -0
  37. package/dist/terrenoPlugin.js +2 -0
  38. package/dist/tests.js +34 -24
  39. package/package.json +8 -2
  40. package/src/__snapshots__/openApi.test.ts.snap +399 -0
  41. package/src/__snapshots__/openApiBuilder.test.ts.snap +108 -0
  42. package/src/api.test.ts +743 -2
  43. package/src/api.ts +270 -6
  44. package/src/auth.ts +3 -1
  45. package/src/betterAuth.test.ts +160 -0
  46. package/src/betterAuth.ts +104 -0
  47. package/src/betterAuthApp.test.ts +114 -0
  48. package/src/betterAuthApp.ts +60 -0
  49. package/src/betterAuthSetup.test.ts +485 -0
  50. package/src/betterAuthSetup.ts +251 -0
  51. package/src/errors.ts +14 -11
  52. package/src/example.ts +7 -7
  53. package/src/expressServer.ts +4 -5
  54. package/src/githubAuth.test.ts +3 -3
  55. package/src/index.ts +6 -0
  56. package/src/openApi.test.ts +8 -5
  57. package/src/openApiBuilder.ts +188 -15
  58. package/src/openApiValidator.test.ts +241 -0
  59. package/src/openApiValidator.ts +860 -0
  60. package/src/plugins.test.ts +3 -3
  61. package/src/terrenoApp.test.ts +201 -0
  62. package/src/terrenoApp.ts +347 -0
  63. package/src/terrenoPlugin.ts +39 -0
  64. package/src/tests.ts +34 -24
  65. package/.cursorrules +0 -107
  66. package/.windsurfrules +0 -107
  67. package/AGENTS.md +0 -313
  68. package/dist/response.d.ts +0 -0
  69. package/dist/response.js +0 -1
  70. package/index.ts +0 -1
  71. 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,6 +120,55 @@ 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
 
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,14 +204,106 @@ 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;
220
+ }
221
+ /**
222
+ * Registration object returned by modelRouter when called with a path.
223
+ *
224
+ * Used with `TerrenoApp.register()` to mount model routers at specific paths.
225
+ * Contains the Express router and the path it should be mounted at.
226
+ *
227
+ * @see modelRouter for creating registrations
228
+ * @see TerrenoApp for registering routers
229
+ */
230
+ export interface ModelRouterRegistration {
231
+ /** Internal type discriminator for registration detection */
232
+ __type: "modelRouter";
233
+ /** The path where the router should be mounted (e.g., "/todos") */
234
+ path: string;
235
+ /** The Express router containing CRUD endpoints */
236
+ router: express.Router;
206
237
  }
207
238
  /**
208
239
  * Create a set of CRUD routes given a Mongoose model and configuration options.
209
240
  *
210
- * @param model A Mongoose Model
211
- * @param options Options for configuring the REST API, such as permissions, transformers, and hooks.
241
+ * When called with a path as the first argument, returns a `ModelRouterRegistration` that can be
242
+ * passed to `TerrenoApp.register()`.
243
+ *
244
+ * @example
245
+ * // Traditional usage (returns express.Router):
246
+ * router.use("/todos", modelRouter(Todo, options));
247
+ *
248
+ * // Registration usage (returns ModelRouterRegistration):
249
+ * const todoRouter = modelRouter("/todos", Todo, options);
250
+ * app.register(todoRouter);
212
251
  */
252
+ export declare function modelRouter<T>(path: string, model: Model<T>, options: ModelRouterOptions<T>): ModelRouterRegistration;
213
253
  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>;
254
+ /**
255
+ * Options for the asyncHandler function.
256
+ */
257
+ export interface AsyncHandlerOptions {
258
+ /**
259
+ * Schema for validating request body.
260
+ * When provided and validation is enabled, the request body will be validated
261
+ * against this schema before the handler runs.
262
+ */
263
+ bodySchema?: Record<string, import("./openApiBuilder").OpenApiSchemaProperty>;
264
+ /**
265
+ * Schema for validating query parameters.
266
+ * When provided and validation is enabled, query params will be validated
267
+ * against this schema before the handler runs.
268
+ */
269
+ querySchema?: Record<string, import("./openApiBuilder").OpenApiSchemaProperty>;
270
+ /**
271
+ * Override global validation setting for this handler.
272
+ * - `true`: Enable validation regardless of global setting
273
+ * - `false`: Disable validation regardless of global setting
274
+ * - `undefined`: Use global setting
275
+ */
276
+ validate?: boolean;
277
+ }
278
+ /**
279
+ * Wraps async route handlers to properly catch and forward errors.
280
+ *
281
+ * Since Express doesn't handle async routes well, wrap them with this function.
282
+ * Optionally supports integrated request validation.
283
+ *
284
+ * @param fn - The async route handler function
285
+ * @param options - Optional configuration for validation
286
+ * @returns Express middleware function
287
+ *
288
+ * @example
289
+ * ```typescript
290
+ * // Basic usage without validation
291
+ * router.post("/users", asyncHandler(async (req, res) => {
292
+ * // handler code
293
+ * }));
294
+ *
295
+ * // With integrated validation
296
+ * router.post("/users", asyncHandler(async (req, res) => {
297
+ * // handler code - body is already validated
298
+ * }, {
299
+ * bodySchema: {
300
+ * name: {type: "string", required: true},
301
+ * email: {type: "string", format: "email", required: true},
302
+ * },
303
+ * validate: true,
304
+ * }));
305
+ * ```
306
+ */
307
+ export declare const asyncHandler: (fn: any, options?: AsyncHandlerOptions) => (req: Request, res: Response, next: NextFunction) => void;
215
308
  export declare const gooseRestRouter: typeof modelRouter;
216
309
  export type GooseRESTOptions<T> = ModelRouterOptions<T>;
package/dist/api.js CHANGED
@@ -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,13 +228,82 @@ function checkQueryParamAllowed(queryParam, queryParamValue, queryFields) {
227
228
  //
228
229
  // return result;
229
230
  // }
230
- /**
231
- * Create a set of CRUD routes given a Mongoose model and configuration options.
232
- *
233
- * @param model A Mongoose Model
234
- * @param options Options for configuring the REST API, such as permissions, transformers, and hooks.
235
- */
236
- function modelRouter(model, options) {
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
+ }
287
+ function modelRouter(pathOrModel, modelOrOptions, maybeOptions) {
288
+ var model;
289
+ var options;
290
+ var path;
291
+ if (typeof pathOrModel === "string") {
292
+ path = pathOrModel;
293
+ model = modelOrOptions;
294
+ options = maybeOptions;
295
+ }
296
+ else {
297
+ model = pathOrModel;
298
+ options = modelOrOptions;
299
+ }
300
+ var router = _buildModelRouter(model, options);
301
+ if (path !== undefined) {
302
+ return { __type: "modelRouter", path: path, router: router };
303
+ }
304
+ return router;
305
+ }
306
+ function _buildModelRouter(model, options) {
237
307
  var _this = this;
238
308
  var _a;
239
309
  var router = express_1.default.Router();
@@ -242,10 +312,15 @@ function modelRouter(model, options) {
242
312
  options.endpoints(router);
243
313
  }
244
314
  var responseHandler = (_a = options.responseHandler) !== null && _a !== void 0 ? _a : transformers_1.defaultResponseHandler;
315
+ // Always install validation middleware — they are no-ops until configureOpenApiValidator() is called
316
+ var createValidation = getBodyValidationMiddleware(model, options, "create");
317
+ var updateValidation = getBodyValidationMiddleware(model, options, "update");
318
+ var queryValidation = getQueryValidationMiddleware(model, options);
245
319
  router.post("/", [
246
320
  (0, auth_1.authenticateMiddleware)(options.allowAnonymous),
247
321
  (0, openApi_1.createOpenApiMiddleware)(model, options),
248
322
  (0, permissions_1.permissionMiddleware)(model, options),
323
+ createValidation,
249
324
  ], (0, exports.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
250
325
  var body, error_1, data, error_2, populateQuery, error_3, error_4, serialized, error_5;
251
326
  return __generator(this, function (_a) {
@@ -378,6 +453,7 @@ function modelRouter(model, options) {
378
453
  (0, auth_1.authenticateMiddleware)(options.allowAnonymous),
379
454
  (0, permissions_1.permissionMiddleware)(model, options),
380
455
  (0, openApi_1.listOpenApiMiddleware)(model, options),
456
+ queryValidation,
381
457
  ], (0, exports.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
382
458
  var query, _a, _b, queryParam, _c, _d, queryParam, queryFilter, error_6, limit, builtQuery, total, populatedQuery, data, error_7, serialized, error_8, more, msg;
383
459
  var e_4, _e, e_5, _f;
@@ -594,6 +670,7 @@ function modelRouter(model, options) {
594
670
  (0, auth_1.authenticateMiddleware)(options.allowAnonymous),
595
671
  (0, openApi_1.patchOpenApiMiddleware)(model, options),
596
672
  (0, permissions_1.permissionMiddleware)(model, options),
673
+ updateValidation,
597
674
  ], (0, exports.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
598
675
  var doc, body, error_10, prevDoc, error_11, populateQuery, error_12, serialized, error_13;
599
676
  var _a;
@@ -991,10 +1068,81 @@ function modelRouter(model, options) {
991
1068
  router.use(errors_1.apiErrorMiddleware);
992
1069
  return router;
993
1070
  }
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
- }; };
1071
+ /**
1072
+ * Wraps async route handlers to properly catch and forward errors.
1073
+ *
1074
+ * Since Express doesn't handle async routes well, wrap them with this function.
1075
+ * Optionally supports integrated request validation.
1076
+ *
1077
+ * @param fn - The async route handler function
1078
+ * @param options - Optional configuration for validation
1079
+ * @returns Express middleware function
1080
+ *
1081
+ * @example
1082
+ * ```typescript
1083
+ * // Basic usage without validation
1084
+ * router.post("/users", asyncHandler(async (req, res) => {
1085
+ * // handler code
1086
+ * }));
1087
+ *
1088
+ * // With integrated validation
1089
+ * router.post("/users", asyncHandler(async (req, res) => {
1090
+ * // handler code - body is already validated
1091
+ * }, {
1092
+ * bodySchema: {
1093
+ * name: {type: "string", required: true},
1094
+ * email: {type: "string", format: "email", required: true},
1095
+ * },
1096
+ * validate: true,
1097
+ * }));
1098
+ * ```
1099
+ */
1100
+ var asyncHandler = function (fn, options) {
1101
+ var _a, _b;
1102
+ // If no validation options, return simple handler
1103
+ if (!(options === null || options === void 0 ? void 0 : options.bodySchema) && !(options === null || options === void 0 ? void 0 : options.querySchema)) {
1104
+ return function (req, res, next) {
1105
+ return Promise.resolve(fn(req, res, next)).catch(next);
1106
+ };
1107
+ }
1108
+ // Import validation functions dynamically to avoid circular deps at module load
1109
+ var _c = require("./openApiValidator"), validateRequestBody = _c.validateRequestBody, validateQueryParams = _c.validateQueryParams, getOpenApiValidatorConfig = _c.getOpenApiValidatorConfig;
1110
+ // Build validation middleware
1111
+ var validators = [];
1112
+ // Determine if validation should be enabled
1113
+ var shouldValidate = (_b = (_a = options.validate) !== null && _a !== void 0 ? _a : getOpenApiValidatorConfig().validateRequests) !== null && _b !== void 0 ? _b : false;
1114
+ if (shouldValidate) {
1115
+ if (options.bodySchema) {
1116
+ validators.push(validateRequestBody(options.bodySchema, { enabled: true }));
1117
+ }
1118
+ if (options.querySchema) {
1119
+ validators.push(validateQueryParams(options.querySchema, { enabled: true }));
1120
+ }
1121
+ }
1122
+ return function (req, res, next) {
1123
+ // Run validators sequentially, then the handler
1124
+ var runValidators = function (index) {
1125
+ if (index >= validators.length) {
1126
+ // All validators passed, run the actual handler
1127
+ Promise.resolve(fn(req, res, next)).catch(next);
1128
+ return;
1129
+ }
1130
+ try {
1131
+ validators[index](req, res, function (err) {
1132
+ if (err) {
1133
+ next(err);
1134
+ return;
1135
+ }
1136
+ runValidators(index + 1);
1137
+ });
1138
+ }
1139
+ catch (err) {
1140
+ next(err);
1141
+ }
1142
+ };
1143
+ runValidators(0);
1144
+ };
1145
+ };
998
1146
  exports.asyncHandler = asyncHandler;
999
1147
  // For backwards compatibility with the old names.
1000
1148
  exports.gooseRestRouter = modelRouter;