monapi 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.
package/dist/index.mjs ADDED
@@ -0,0 +1,998 @@
1
+ import { Router } from 'express';
2
+ import { Schema } from 'mongoose';
3
+
4
+ // src/monapi.ts
5
+
6
+ // src/types/query.ts
7
+ var FieldType = /* @__PURE__ */ ((FieldType2) => {
8
+ FieldType2["String"] = "string";
9
+ FieldType2["Number"] = "number";
10
+ FieldType2["Boolean"] = "boolean";
11
+ FieldType2["Date"] = "date";
12
+ FieldType2["ObjectId"] = "objectid";
13
+ FieldType2["Array"] = "array";
14
+ FieldType2["Object"] = "object";
15
+ FieldType2["Mixed"] = "mixed";
16
+ return FieldType2;
17
+ })(FieldType || {});
18
+
19
+ // src/types/schema.ts
20
+ var SchemaType = /* @__PURE__ */ ((SchemaType2) => {
21
+ SchemaType2["Mongoose"] = "mongoose";
22
+ SchemaType2["Typegoose"] = "typegoose";
23
+ SchemaType2["Zod"] = "zod";
24
+ SchemaType2["Joi"] = "joi";
25
+ SchemaType2["Yup"] = "yup";
26
+ SchemaType2["Unknown"] = "unknown";
27
+ return SchemaType2;
28
+ })(SchemaType || {});
29
+
30
+ // src/adapters/schema/MongooseAdapter.ts
31
+ var MongooseAdapter = class {
32
+ constructor(schemaOrModel) {
33
+ if (this.isModel(schemaOrModel)) {
34
+ this.model = schemaOrModel;
35
+ this.schema = schemaOrModel.schema;
36
+ } else {
37
+ this.schema = schemaOrModel;
38
+ }
39
+ }
40
+ /**
41
+ * Type guard to check if value is a Mongoose model
42
+ */
43
+ isModel(value) {
44
+ return value && typeof value === "function" && value.prototype && value.schema;
45
+ }
46
+ /**
47
+ * Get list of all field names in the schema
48
+ */
49
+ getFields() {
50
+ const fields = [];
51
+ this.schema.eachPath((pathname) => {
52
+ if (pathname === "_id" || pathname === "__v") {
53
+ return;
54
+ }
55
+ fields.push(pathname);
56
+ });
57
+ return fields;
58
+ }
59
+ /**
60
+ * Get the type of a specific field
61
+ */
62
+ getFieldType(field) {
63
+ const schemaType = this.schema.path(field);
64
+ if (!schemaType) {
65
+ return "mixed" /* Mixed */;
66
+ }
67
+ const instance = schemaType.instance;
68
+ switch (instance) {
69
+ case "String":
70
+ return "string" /* String */;
71
+ case "Number":
72
+ return "number" /* Number */;
73
+ case "Boolean":
74
+ return "boolean" /* Boolean */;
75
+ case "Date":
76
+ return "date" /* Date */;
77
+ case "ObjectID":
78
+ case "ObjectId":
79
+ return "objectid" /* ObjectId */;
80
+ case "Array":
81
+ return "array" /* Array */;
82
+ case "Embedded":
83
+ case "Subdocument":
84
+ return "object" /* Object */;
85
+ case "Mixed":
86
+ return "mixed" /* Mixed */;
87
+ default:
88
+ return "mixed" /* Mixed */;
89
+ }
90
+ }
91
+ /**
92
+ * Get metadata for a specific field
93
+ */
94
+ getFieldMetadata(field) {
95
+ const schemaType = this.schema.path(field);
96
+ if (!schemaType) {
97
+ return void 0;
98
+ }
99
+ const metadata = {
100
+ name: field,
101
+ type: this.getFieldType(field)
102
+ };
103
+ if (schemaType.isRequired) {
104
+ metadata.required = true;
105
+ }
106
+ const st = schemaType;
107
+ if (st.defaultValue !== void 0) {
108
+ metadata.default = st.defaultValue;
109
+ }
110
+ if (st.enumValues && st.enumValues.length > 0) {
111
+ metadata.enum = st.enumValues;
112
+ }
113
+ return metadata;
114
+ }
115
+ /**
116
+ * Get all fields metadata
117
+ */
118
+ getAllFieldsMetadata() {
119
+ const fields = this.getFields();
120
+ return fields.map((field) => this.getFieldMetadata(field)).filter((meta) => meta !== void 0);
121
+ }
122
+ /**
123
+ * Validate data against the schema
124
+ */
125
+ async validate(data) {
126
+ if (!this.model) {
127
+ return this.basicValidation(data);
128
+ }
129
+ try {
130
+ const doc = new this.model(data);
131
+ await doc.validate();
132
+ return {
133
+ valid: true,
134
+ data: doc.toObject()
135
+ };
136
+ } catch (error) {
137
+ if (error.name === "ValidationError") {
138
+ const errors = Object.keys(error.errors).map((field) => ({
139
+ field,
140
+ message: error.errors[field].message,
141
+ code: error.errors[field].kind
142
+ }));
143
+ return {
144
+ valid: false,
145
+ errors
146
+ };
147
+ }
148
+ return {
149
+ valid: false,
150
+ errors: [
151
+ {
152
+ field: "",
153
+ message: error.message || "Validation failed"
154
+ }
155
+ ]
156
+ };
157
+ }
158
+ }
159
+ /**
160
+ * Basic validation when model is not available
161
+ */
162
+ basicValidation(data) {
163
+ if (typeof data !== "object" || data === null) {
164
+ return {
165
+ valid: false,
166
+ errors: [
167
+ {
168
+ field: "",
169
+ message: "Data must be an object"
170
+ }
171
+ ]
172
+ };
173
+ }
174
+ return {
175
+ valid: true,
176
+ data
177
+ };
178
+ }
179
+ /**
180
+ * Get the underlying Mongoose model if available
181
+ */
182
+ getMongooseModel() {
183
+ return this.model;
184
+ }
185
+ /**
186
+ * Get the underlying Mongoose schema
187
+ */
188
+ getMongooseSchema() {
189
+ return this.schema;
190
+ }
191
+ };
192
+
193
+ // src/adapters/schema/index.ts
194
+ function detectSchemaType(schema) {
195
+ if (schema instanceof Schema) {
196
+ return "mongoose" /* Mongoose */;
197
+ }
198
+ if (typeof schema === "function" && schema.prototype && schema.schema) {
199
+ return "mongoose" /* Mongoose */;
200
+ }
201
+ if (schema && typeof schema === "object" && "_def" in schema && "parse" in schema) {
202
+ return "zod" /* Zod */;
203
+ }
204
+ if (schema && typeof schema === "object" && "isJoi" in schema) {
205
+ return "joi" /* Joi */;
206
+ }
207
+ if (schema && typeof schema === "object" && "__isYupSchema__" in schema) {
208
+ return "yup" /* Yup */;
209
+ }
210
+ if (typeof schema === "function" && schema.prototype) {
211
+ const metadata = Reflect.getMetadata?.("typegoose:properties", schema);
212
+ if (metadata) {
213
+ return "typegoose" /* Typegoose */;
214
+ }
215
+ }
216
+ return "unknown" /* Unknown */;
217
+ }
218
+ function createSchemaAdapter(schema) {
219
+ const schemaType = detectSchemaType(schema);
220
+ switch (schemaType) {
221
+ case "mongoose" /* Mongoose */:
222
+ return new MongooseAdapter(schema);
223
+ case "zod" /* Zod */:
224
+ throw new Error("Zod adapter not yet implemented. Coming in Phase 9.");
225
+ case "joi" /* Joi */:
226
+ throw new Error("Joi adapter not yet implemented. Coming in Phase 9.");
227
+ case "yup" /* Yup */:
228
+ throw new Error("Yup adapter not yet implemented. Coming in Phase 9.");
229
+ case "typegoose" /* Typegoose */:
230
+ throw new Error("Typegoose adapter not yet implemented. Coming in Phase 9.");
231
+ default:
232
+ throw new Error(`Unsupported schema type: ${schemaType}`);
233
+ }
234
+ }
235
+
236
+ // src/utils/errors.ts
237
+ var MonapiError = class _MonapiError extends Error {
238
+ constructor(message, statusCode, code, details) {
239
+ super(message);
240
+ this.name = "MonapiError";
241
+ this.statusCode = statusCode;
242
+ this.code = code;
243
+ this.details = details;
244
+ Object.setPrototypeOf(this, _MonapiError.prototype);
245
+ }
246
+ };
247
+ var NotFoundError = class _NotFoundError extends MonapiError {
248
+ constructor(resource, id) {
249
+ const message = id ? `${resource} with id '${id}' not found` : `${resource} not found`;
250
+ super(message, 404, "NOT_FOUND");
251
+ this.name = "NotFoundError";
252
+ Object.setPrototypeOf(this, _NotFoundError.prototype);
253
+ }
254
+ };
255
+ var ValidationError = class _ValidationError extends MonapiError {
256
+ constructor(message, details) {
257
+ super(message, 400, "VALIDATION_ERROR", details);
258
+ this.name = "ValidationError";
259
+ Object.setPrototypeOf(this, _ValidationError.prototype);
260
+ }
261
+ };
262
+ var ForbiddenError = class _ForbiddenError extends MonapiError {
263
+ constructor(message = "Access denied") {
264
+ super(message, 403, "FORBIDDEN");
265
+ this.name = "ForbiddenError";
266
+ Object.setPrototypeOf(this, _ForbiddenError.prototype);
267
+ }
268
+ };
269
+ var UnauthorizedError = class _UnauthorizedError extends MonapiError {
270
+ constructor(message = "Authentication required") {
271
+ super(message, 401, "UNAUTHORIZED");
272
+ this.name = "UnauthorizedError";
273
+ Object.setPrototypeOf(this, _UnauthorizedError.prototype);
274
+ }
275
+ };
276
+ var BadRequestError = class _BadRequestError extends MonapiError {
277
+ constructor(message, details) {
278
+ super(message, 400, "BAD_REQUEST", details);
279
+ this.name = "BadRequestError";
280
+ Object.setPrototypeOf(this, _BadRequestError.prototype);
281
+ }
282
+ };
283
+
284
+ // src/engine/filter-parser.ts
285
+ var OPERATOR_MAP = {
286
+ eq: "$eq",
287
+ ne: "$ne",
288
+ gt: "$gt",
289
+ gte: "$gte",
290
+ lt: "$lt",
291
+ lte: "$lte",
292
+ in: "$in",
293
+ nin: "$nin",
294
+ like: "$regex",
295
+ exists: "$exists"
296
+ };
297
+ var RESERVED_PARAMS = /* @__PURE__ */ new Set(["page", "limit", "sort", "fields", "filter"]);
298
+ var DEFAULT_MAX_REGEX_LENGTH = 100;
299
+ var DEFAULT_MAX_FILTERS = 20;
300
+ function parseFilters(query, options = {}) {
301
+ const { adapter, queryConfig, maxRegexLength = DEFAULT_MAX_REGEX_LENGTH, maxFilters = DEFAULT_MAX_FILTERS } = options;
302
+ const allowedFields = queryConfig?.allowedFilters ?? adapter?.getFields();
303
+ const filter = {};
304
+ let filterCount = 0;
305
+ if (query.filter && typeof query.filter === "object") {
306
+ for (const [field, ops] of Object.entries(query.filter)) {
307
+ validateField(field, allowedFields);
308
+ if (typeof ops === "object" && ops !== null) {
309
+ for (const [op, val] of Object.entries(ops)) {
310
+ if (++filterCount > maxFilters) {
311
+ throw new BadRequestError(`Too many filters. Maximum allowed: ${maxFilters}`);
312
+ }
313
+ const operator = op;
314
+ validateOperator(operator);
315
+ const fieldType = adapter?.getFieldType(field);
316
+ applyFilter(filter, field, operator, val, fieldType, maxRegexLength);
317
+ }
318
+ } else {
319
+ if (++filterCount > maxFilters) {
320
+ throw new BadRequestError(`Too many filters. Maximum allowed: ${maxFilters}`);
321
+ }
322
+ const fieldType = adapter?.getFieldType(field);
323
+ applyFilter(filter, field, "eq", ops, fieldType, maxRegexLength);
324
+ }
325
+ }
326
+ }
327
+ for (const [key, value] of Object.entries(query)) {
328
+ if (RESERVED_PARAMS.has(key) || key === "filter") continue;
329
+ if (value === void 0 || value === "") continue;
330
+ const { field, operator } = parseParamKey(key);
331
+ validateField(field, allowedFields);
332
+ if (++filterCount > maxFilters) {
333
+ throw new BadRequestError(`Too many filters. Maximum allowed: ${maxFilters}`);
334
+ }
335
+ validateOperator(operator);
336
+ const fieldType = adapter?.getFieldType(field);
337
+ applyFilter(filter, field, operator, value, fieldType, maxRegexLength);
338
+ }
339
+ return filter;
340
+ }
341
+ function parseParamKey(key) {
342
+ const doubleUnderscoreIndex = key.indexOf("__");
343
+ if (doubleUnderscoreIndex === -1) {
344
+ return { field: key, operator: "eq" };
345
+ }
346
+ const field = key.substring(0, doubleUnderscoreIndex);
347
+ const operator = key.substring(doubleUnderscoreIndex + 2);
348
+ return { field, operator };
349
+ }
350
+ function validateField(field, allowedFields) {
351
+ if (field.startsWith("$")) {
352
+ throw new BadRequestError(`Invalid filter field: ${field}`);
353
+ }
354
+ if (allowedFields && !allowedFields.includes(field)) {
355
+ throw new BadRequestError(`Filtering on field '${field}' is not allowed`);
356
+ }
357
+ }
358
+ function validateOperator(operator) {
359
+ if (!(operator in OPERATOR_MAP)) {
360
+ throw new BadRequestError(
361
+ `Unsupported filter operator: '${operator}'. Supported: ${Object.keys(OPERATOR_MAP).join(", ")}`
362
+ );
363
+ }
364
+ }
365
+ function applyFilter(filter, field, operator, rawValue, fieldType, maxRegexLength) {
366
+ const mongoOp = OPERATOR_MAP[operator];
367
+ const value = coerceValue(rawValue, operator, fieldType, maxRegexLength);
368
+ if (operator === "eq") {
369
+ filter[field] = value;
370
+ } else {
371
+ if (!filter[field] || typeof filter[field] !== "object") {
372
+ filter[field] = {};
373
+ }
374
+ filter[field][mongoOp] = value;
375
+ }
376
+ }
377
+ function coerceValue(raw, operator, fieldType, maxRegexLength = DEFAULT_MAX_REGEX_LENGTH) {
378
+ if (operator === "in" || operator === "nin") {
379
+ const items = String(raw).split(",").map((s) => s.trim());
380
+ return items.map((item) => coerceSingleValue(item, fieldType));
381
+ }
382
+ if (operator === "exists") {
383
+ return raw === "true" || raw === "1";
384
+ }
385
+ if (operator === "like") {
386
+ const pattern = String(raw);
387
+ if (pattern.length > maxRegexLength) {
388
+ throw new BadRequestError(
389
+ `Regex pattern too long. Maximum length: ${maxRegexLength}`
390
+ );
391
+ }
392
+ const escaped = escapeRegex(pattern);
393
+ return new RegExp(escaped, "i");
394
+ }
395
+ return coerceSingleValue(raw, fieldType);
396
+ }
397
+ function coerceSingleValue(raw, fieldType) {
398
+ const str = String(raw);
399
+ if (fieldType) {
400
+ switch (fieldType) {
401
+ case "number" /* Number */:
402
+ const num = Number(str);
403
+ if (!isNaN(num)) return num;
404
+ break;
405
+ case "boolean" /* Boolean */:
406
+ if (str === "true" || str === "1") return true;
407
+ if (str === "false" || str === "0") return false;
408
+ break;
409
+ case "date" /* Date */:
410
+ const date = new Date(str);
411
+ if (!isNaN(date.getTime())) return date;
412
+ break;
413
+ }
414
+ }
415
+ if (/^-?\d+(\.\d+)?$/.test(str)) {
416
+ return Number(str);
417
+ }
418
+ if (str === "true") return true;
419
+ if (str === "false") return false;
420
+ if (/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?/.test(str)) {
421
+ const date = new Date(str);
422
+ if (!isNaN(date.getTime())) return date;
423
+ }
424
+ return str;
425
+ }
426
+ function escapeRegex(str) {
427
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
428
+ }
429
+
430
+ // src/engine/query-builder.ts
431
+ var DEFAULT_PAGE = 1;
432
+ var DEFAULT_LIMIT = 10;
433
+ var DEFAULT_MAX_LIMIT = 100;
434
+ function buildQuery(queryParams, options = {}) {
435
+ const {
436
+ adapter,
437
+ queryConfig,
438
+ defaultLimit = queryConfig?.defaultLimit ?? DEFAULT_LIMIT,
439
+ maxLimit = queryConfig?.maxLimit ?? DEFAULT_MAX_LIMIT,
440
+ maxRegexLength
441
+ } = options;
442
+ const filter = parseFilters(queryParams, { adapter, queryConfig, maxRegexLength });
443
+ const sort = parseSort(queryParams.sort, queryConfig, adapter);
444
+ const { skip, limit } = parsePagination(queryParams, defaultLimit, maxLimit);
445
+ const projection = parseProjection(queryParams.fields, adapter);
446
+ const mongoQuery = { filter };
447
+ if (Object.keys(sort).length > 0) {
448
+ mongoQuery.sort = sort;
449
+ }
450
+ mongoQuery.skip = skip;
451
+ mongoQuery.limit = limit;
452
+ if (projection && Object.keys(projection).length > 0) {
453
+ mongoQuery.projection = projection;
454
+ }
455
+ return mongoQuery;
456
+ }
457
+ function parseSort(sortParam, queryConfig, adapter) {
458
+ const sort = {};
459
+ if (!sortParam && queryConfig?.defaultSort) {
460
+ sortParam = queryConfig.defaultSort;
461
+ }
462
+ if (!sortParam) return sort;
463
+ const fields = Array.isArray(sortParam) ? sortParam : sortParam.split(",").map((s) => s.trim());
464
+ const allowedSorts = queryConfig?.allowedSorts ?? adapter?.getFields();
465
+ for (const field of fields) {
466
+ if (!field) continue;
467
+ let direction = 1;
468
+ let fieldName = field;
469
+ if (field.startsWith("-")) {
470
+ direction = -1;
471
+ fieldName = field.substring(1);
472
+ }
473
+ if (fieldName.startsWith("$")) {
474
+ throw new BadRequestError(`Invalid sort field: ${fieldName}`);
475
+ }
476
+ if (allowedSorts && !allowedSorts.includes(fieldName)) {
477
+ throw new BadRequestError(`Sorting by '${fieldName}' is not allowed`);
478
+ }
479
+ sort[fieldName] = direction;
480
+ }
481
+ return sort;
482
+ }
483
+ function parsePagination(queryParams, defaultLimit, maxLimit) {
484
+ let page = parseInt(queryParams.page, 10);
485
+ let limit = parseInt(queryParams.limit, 10);
486
+ if (isNaN(page) || page < 1) page = DEFAULT_PAGE;
487
+ if (isNaN(limit) || limit < 1) limit = defaultLimit;
488
+ if (limit > maxLimit) limit = maxLimit;
489
+ const skip = (page - 1) * limit;
490
+ return { skip, limit, page };
491
+ }
492
+ function parseProjection(fieldsParam, adapter) {
493
+ if (!fieldsParam) return void 0;
494
+ const fields = Array.isArray(fieldsParam) ? fieldsParam : fieldsParam.split(",").map((s) => s.trim());
495
+ const schemaFields = adapter?.getFields();
496
+ const projection = {};
497
+ for (const field of fields) {
498
+ if (!field) continue;
499
+ if (field.startsWith("$")) {
500
+ throw new BadRequestError(`Invalid projection field: ${field}`);
501
+ }
502
+ if (schemaFields && !schemaFields.includes(field)) {
503
+ throw new BadRequestError(`Field '${field}' does not exist in schema`);
504
+ }
505
+ projection[field] = 1;
506
+ }
507
+ return Object.keys(projection).length > 0 ? projection : void 0;
508
+ }
509
+ function buildPaginationMeta(total, page, limit) {
510
+ return {
511
+ page,
512
+ limit,
513
+ total,
514
+ totalPages: Math.ceil(total / limit)
515
+ };
516
+ }
517
+ function extractPagination(queryParams, defaultLimit = DEFAULT_LIMIT, maxLimit = DEFAULT_MAX_LIMIT) {
518
+ let page = parseInt(queryParams.page, 10);
519
+ let limit = parseInt(queryParams.limit, 10);
520
+ if (isNaN(page) || page < 1) page = DEFAULT_PAGE;
521
+ if (isNaN(limit) || limit < 1) limit = defaultLimit;
522
+ if (limit > maxLimit) limit = maxLimit;
523
+ return { page, limit };
524
+ }
525
+
526
+ // src/engine/hook-executor.ts
527
+ function createHookContext(params) {
528
+ const user = params.req.user;
529
+ return {
530
+ collection: params.collection,
531
+ operation: params.operation,
532
+ user,
533
+ query: params.query,
534
+ data: params.data,
535
+ id: params.id,
536
+ result: params.result,
537
+ req: params.req,
538
+ res: params.res,
539
+ meta: {}
540
+ };
541
+ }
542
+ async function executeHook(hooks, hookName, ctx, logger) {
543
+ if (!hooks) return ctx;
544
+ const hookFn = hooks[hookName];
545
+ if (!hookFn) return ctx;
546
+ try {
547
+ await hookFn(ctx);
548
+ } catch (error) {
549
+ if (logger) {
550
+ logger.error(`Hook '${hookName}' failed for collection '${ctx.collection}': ${error.message}`);
551
+ }
552
+ throw error;
553
+ }
554
+ return ctx;
555
+ }
556
+
557
+ // src/engine/crud-handlers.ts
558
+ function createCRUDHandlers(options) {
559
+ const { collectionName, model, adapter, config, defaults, logger } = options;
560
+ return {
561
+ list: config.handlers?.list ?? createListHandler(collectionName, model, adapter, config, defaults, logger),
562
+ get: config.handlers?.get ?? createGetHandler(collectionName, model, adapter, config, logger),
563
+ create: config.handlers?.create ?? createCreateHandler(collectionName, model, adapter, config, logger),
564
+ update: config.handlers?.update ?? createUpdateHandler(collectionName, model, adapter, config, logger),
565
+ patch: config.handlers?.patch ?? createPatchHandler(collectionName, model, adapter, config, logger),
566
+ delete: config.handlers?.delete ?? createDeleteHandler(collectionName, model, config, logger)
567
+ };
568
+ }
569
+ function createListHandler(collectionName, model, adapter, config, defaults, logger) {
570
+ return async (req, res, next) => {
571
+ try {
572
+ const mongoQuery = buildQuery(req.query, {
573
+ adapter,
574
+ queryConfig: config.query ?? defaults?.query,
575
+ defaultLimit: defaults?.pagination?.limit,
576
+ maxLimit: defaults?.pagination?.maxLimit,
577
+ maxRegexLength: defaults?.security?.maxRegexLength
578
+ });
579
+ const ctx = createHookContext({
580
+ collection: collectionName,
581
+ operation: "find",
582
+ req,
583
+ res,
584
+ query: mongoQuery
585
+ });
586
+ await executeHook(config.hooks, "beforeFind", ctx, logger);
587
+ if (ctx.preventDefault) {
588
+ return;
589
+ }
590
+ const query = ctx.query ?? mongoQuery;
591
+ const [docs, total] = await Promise.all([
592
+ model.find(query.filter).sort(query.sort).skip(query.skip ?? 0).limit(query.limit ?? 10).select(query.projection ?? {}).lean().exec(),
593
+ model.countDocuments(query.filter).exec()
594
+ ]);
595
+ const { page, limit } = extractPagination(req.query, defaults?.pagination?.limit, defaults?.pagination?.maxLimit);
596
+ ctx.result = docs;
597
+ await executeHook(config.hooks, "afterFind", ctx, logger);
598
+ const response = {
599
+ data: ctx.result ?? docs,
600
+ meta: buildPaginationMeta(total, page, limit)
601
+ };
602
+ res.json(response);
603
+ } catch (error) {
604
+ next(error);
605
+ }
606
+ };
607
+ }
608
+ function createGetHandler(collectionName, model, _adapter, config, logger) {
609
+ return async (req, res, next) => {
610
+ try {
611
+ const { id } = req.params;
612
+ const fieldsParam = req.query.fields;
613
+ let projection;
614
+ if (fieldsParam) {
615
+ const fields = typeof fieldsParam === "string" ? fieldsParam.split(",") : fieldsParam;
616
+ projection = {};
617
+ for (const f of fields) {
618
+ const trimmed = f.trim();
619
+ if (trimmed && !trimmed.startsWith("$")) {
620
+ projection[trimmed] = 1;
621
+ }
622
+ }
623
+ }
624
+ const ctx = createHookContext({
625
+ collection: collectionName,
626
+ operation: "find",
627
+ req,
628
+ res,
629
+ id
630
+ });
631
+ await executeHook(config.hooks, "beforeFind", ctx, logger);
632
+ if (ctx.preventDefault) return;
633
+ let query = model.findById(id);
634
+ if (projection) query = query.select(projection);
635
+ const doc = await query.lean().exec();
636
+ if (!doc) {
637
+ throw new NotFoundError(collectionName, id);
638
+ }
639
+ ctx.result = doc;
640
+ await executeHook(config.hooks, "afterFind", ctx, logger);
641
+ const response = { data: ctx.result ?? doc };
642
+ res.json(response);
643
+ } catch (error) {
644
+ next(error);
645
+ }
646
+ };
647
+ }
648
+ function createCreateHandler(collectionName, model, adapter, config, logger) {
649
+ return async (req, res, next) => {
650
+ try {
651
+ const data = req.body;
652
+ const validation = await adapter.validate(data);
653
+ if (!validation.valid) {
654
+ throw new ValidationError("Validation failed", validation.errors);
655
+ }
656
+ const ctx = createHookContext({
657
+ collection: collectionName,
658
+ operation: "create",
659
+ req,
660
+ res,
661
+ data: validation.data ?? data
662
+ });
663
+ await executeHook(config.hooks, "beforeCreate", ctx, logger);
664
+ if (ctx.preventDefault) return;
665
+ const doc = await model.create(ctx.data ?? data);
666
+ const result = doc.toObject();
667
+ ctx.result = result;
668
+ await executeHook(config.hooks, "afterCreate", ctx, logger);
669
+ const response = { data: ctx.result ?? result };
670
+ res.status(201).json(response);
671
+ } catch (error) {
672
+ next(error);
673
+ }
674
+ };
675
+ }
676
+ function createUpdateHandler(collectionName, model, adapter, config, logger) {
677
+ return async (req, res, next) => {
678
+ try {
679
+ const { id } = req.params;
680
+ const data = req.body;
681
+ const validation = await adapter.validate(data);
682
+ if (!validation.valid) {
683
+ throw new ValidationError("Validation failed", validation.errors);
684
+ }
685
+ const ctx = createHookContext({
686
+ collection: collectionName,
687
+ operation: "update",
688
+ req,
689
+ res,
690
+ id,
691
+ data: validation.data ?? data
692
+ });
693
+ await executeHook(config.hooks, "beforeUpdate", ctx, logger);
694
+ if (ctx.preventDefault) return;
695
+ const doc = await model.findByIdAndUpdate(id, ctx.data ?? data, { new: true, runValidators: true, overwrite: true }).lean().exec();
696
+ if (!doc) {
697
+ throw new NotFoundError(collectionName, id);
698
+ }
699
+ ctx.result = doc;
700
+ await executeHook(config.hooks, "afterUpdate", ctx, logger);
701
+ const response = { data: ctx.result ?? doc };
702
+ res.json(response);
703
+ } catch (error) {
704
+ next(error);
705
+ }
706
+ };
707
+ }
708
+ function createPatchHandler(collectionName, model, _adapter, config, logger) {
709
+ return async (req, res, next) => {
710
+ try {
711
+ const { id } = req.params;
712
+ const data = req.body;
713
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
714
+ throw new ValidationError("Request body must be an object");
715
+ }
716
+ for (const key of Object.keys(data)) {
717
+ if (key.startsWith("$")) {
718
+ throw new ValidationError(`Invalid field name: ${key}`);
719
+ }
720
+ }
721
+ const ctx = createHookContext({
722
+ collection: collectionName,
723
+ operation: "patch",
724
+ req,
725
+ res,
726
+ id,
727
+ data
728
+ });
729
+ await executeHook(config.hooks, "beforeUpdate", ctx, logger);
730
+ if (ctx.preventDefault) return;
731
+ const doc = await model.findByIdAndUpdate(id, { $set: ctx.data ?? data }, { new: true, runValidators: true }).lean().exec();
732
+ if (!doc) {
733
+ throw new NotFoundError(collectionName, id);
734
+ }
735
+ ctx.result = doc;
736
+ await executeHook(config.hooks, "afterUpdate", ctx, logger);
737
+ const response = { data: ctx.result ?? doc };
738
+ res.json(response);
739
+ } catch (error) {
740
+ next(error);
741
+ }
742
+ };
743
+ }
744
+ function createDeleteHandler(collectionName, model, config, logger) {
745
+ return async (req, res, next) => {
746
+ try {
747
+ const { id } = req.params;
748
+ const ctx = createHookContext({
749
+ collection: collectionName,
750
+ operation: "delete",
751
+ req,
752
+ res,
753
+ id
754
+ });
755
+ await executeHook(config.hooks, "beforeDelete", ctx, logger);
756
+ if (ctx.preventDefault) return;
757
+ const doc = await model.findByIdAndDelete(id).lean().exec();
758
+ if (!doc) {
759
+ throw new NotFoundError(collectionName, id);
760
+ }
761
+ ctx.result = doc;
762
+ await executeHook(config.hooks, "afterDelete", ctx, logger);
763
+ res.json({ data: ctx.result ?? doc });
764
+ } catch (error) {
765
+ next(error);
766
+ }
767
+ };
768
+ }
769
+
770
+ // src/middleware/auth.ts
771
+ var ROUTE_TO_CRUD = {
772
+ list: "find",
773
+ get: "find",
774
+ create: "create",
775
+ update: "update",
776
+ patch: "patch",
777
+ delete: "delete"
778
+ };
779
+ function createPermissionMiddleware(collection, routeOp, permissions) {
780
+ return async (req, _res, next) => {
781
+ try {
782
+ if (!permissions) {
783
+ next();
784
+ return;
785
+ }
786
+ const permission = permissions[routeOp];
787
+ if (!permission) {
788
+ next();
789
+ return;
790
+ }
791
+ const user = req.user;
792
+ if (!user) {
793
+ throw new UnauthorizedError();
794
+ }
795
+ const crudOp = ROUTE_TO_CRUD[routeOp] || routeOp;
796
+ const allowed = await checkPermission(permission, {
797
+ user,
798
+ collection,
799
+ operation: crudOp,
800
+ data: req.body,
801
+ id: req.params.id,
802
+ req
803
+ });
804
+ if (!allowed) {
805
+ throw new ForbiddenError();
806
+ }
807
+ next();
808
+ } catch (error) {
809
+ next(error);
810
+ }
811
+ };
812
+ }
813
+ async function checkPermission(permission, ctx) {
814
+ if (Array.isArray(permission)) {
815
+ if (!ctx.user.roles || ctx.user.roles.length === 0) {
816
+ return false;
817
+ }
818
+ return permission.some((role) => ctx.user.roles?.includes(role));
819
+ }
820
+ if (typeof permission === "function") {
821
+ return permission(ctx);
822
+ }
823
+ return false;
824
+ }
825
+ function createAuthMiddleware(authConfig) {
826
+ if (authConfig?.middleware) {
827
+ return authConfig.middleware;
828
+ }
829
+ return (_req, _res, next) => {
830
+ next();
831
+ };
832
+ }
833
+
834
+ // src/router/express-router.ts
835
+ function createCollectionRouter(options) {
836
+ const { collectionName, model, adapter, config, defaults, logger, authMiddleware } = options;
837
+ const router = Router();
838
+ const handlers = createCRUDHandlers({ collectionName, model, adapter, config, defaults, logger });
839
+ const operations = ["list", "get", "create", "update", "patch", "delete"];
840
+ const middlewareStacks = {};
841
+ for (const op of operations) {
842
+ const stack = [];
843
+ if (authMiddleware) {
844
+ stack.push(authMiddleware);
845
+ }
846
+ if (config.middleware?.all) {
847
+ stack.push(...config.middleware.all);
848
+ }
849
+ const opMiddleware = config.middleware?.[op];
850
+ if (opMiddleware) {
851
+ stack.push(...opMiddleware);
852
+ }
853
+ if (config.permissions) {
854
+ stack.push(createPermissionMiddleware(collectionName, op, config.permissions));
855
+ }
856
+ middlewareStacks[op] = stack;
857
+ }
858
+ router.get("/", ...middlewareStacks.list, handlers.list);
859
+ router.get("/:id", ...middlewareStacks.get, handlers.get);
860
+ router.post("/", ...middlewareStacks.create, handlers.create);
861
+ router.put("/:id", ...middlewareStacks.update, handlers.update);
862
+ router.patch("/:id", ...middlewareStacks.patch, handlers.patch);
863
+ router.delete("/:id", ...middlewareStacks.delete, handlers.delete);
864
+ return router;
865
+ }
866
+
867
+ // src/middleware/error-handler.ts
868
+ function createErrorHandler(logger) {
869
+ return (err, _req, res, _next) => {
870
+ if (err instanceof MonapiError) {
871
+ if (logger) {
872
+ logger.warn(`${err.code}: ${err.message}`, { statusCode: err.statusCode });
873
+ }
874
+ const response2 = {
875
+ error: {
876
+ code: err.code,
877
+ message: err.message,
878
+ details: err.details
879
+ }
880
+ };
881
+ res.status(err.statusCode).json(response2);
882
+ return;
883
+ }
884
+ if (logger) {
885
+ logger.error(`Unhandled error: ${err.message}`, { stack: err.stack });
886
+ }
887
+ const message = process.env.NODE_ENV === "production" ? "Internal server error" : err.message;
888
+ const response = {
889
+ error: {
890
+ code: "INTERNAL_ERROR",
891
+ message
892
+ }
893
+ };
894
+ res.status(500).json(response);
895
+ };
896
+ }
897
+
898
+ // src/utils/logger.ts
899
+ var defaultLogger = {
900
+ info(message, meta) {
901
+ console.log(`[monapi] INFO: ${message}`, meta ? meta : "");
902
+ },
903
+ warn(message, meta) {
904
+ console.warn(`[monapi] WARN: ${message}`, meta ? meta : "");
905
+ },
906
+ error(message, meta) {
907
+ console.error(`[monapi] ERROR: ${message}`, meta ? meta : "");
908
+ },
909
+ debug(message, meta) {
910
+ if (process.env.NODE_ENV !== "production") {
911
+ console.log(`[monapi] DEBUG: ${message}`, meta ? meta : "");
912
+ }
913
+ }
914
+ };
915
+
916
+ // src/monapi.ts
917
+ var Monapi = class {
918
+ constructor(config) {
919
+ this.collections = /* @__PURE__ */ new Map();
920
+ this.config = config;
921
+ this.logger = config.logger ?? defaultLogger;
922
+ if (config.auth) {
923
+ this.authMiddleware = createAuthMiddleware(config.auth);
924
+ }
925
+ }
926
+ /**
927
+ * Register a collection resource.
928
+ * This will auto-generate all CRUD endpoints for the collection.
929
+ */
930
+ resource(name, collectionConfig) {
931
+ const adapter = collectionConfig.adapter ?? createSchemaAdapter(collectionConfig.schema);
932
+ const model = this.resolveModel(name, collectionConfig, adapter);
933
+ this.collections.set(name, { config: collectionConfig, model, adapter });
934
+ this.logger.debug(`Registered resource: ${name}`, {
935
+ fields: adapter.getFields()
936
+ });
937
+ return this;
938
+ }
939
+ /**
940
+ * Generate the Express router with all registered collection routes.
941
+ */
942
+ router() {
943
+ const mainRouter = Router();
944
+ const basePath = this.config.basePath ?? "";
945
+ for (const [name, { config, model, adapter }] of this.collections) {
946
+ const collectionRouter = createCollectionRouter({
947
+ collectionName: name,
948
+ model,
949
+ adapter,
950
+ config,
951
+ defaults: this.config.defaults,
952
+ logger: this.logger,
953
+ authMiddleware: this.authMiddleware
954
+ });
955
+ const path = basePath ? `${basePath}/${name}` : `/${name}`;
956
+ mainRouter.use(path, collectionRouter);
957
+ this.logger.info(`Mounted routes: ${path}`);
958
+ }
959
+ mainRouter.use(createErrorHandler(this.logger));
960
+ return mainRouter;
961
+ }
962
+ /**
963
+ * Get a registered collection's model
964
+ */
965
+ getModel(name) {
966
+ return this.collections.get(name)?.model;
967
+ }
968
+ /**
969
+ * Get a registered collection's adapter
970
+ */
971
+ getAdapter(name) {
972
+ return this.collections.get(name)?.adapter;
973
+ }
974
+ /**
975
+ * Resolve or create a Mongoose model from the collection config.
976
+ */
977
+ resolveModel(name, config, adapter) {
978
+ if (config.model) {
979
+ return config.model;
980
+ }
981
+ const adapterModel = adapter.getMongooseModel?.();
982
+ if (adapterModel) {
983
+ return adapterModel;
984
+ }
985
+ const mongooseSchema = adapter.getMongooseSchema?.();
986
+ if (mongooseSchema) {
987
+ const modelName = name.charAt(0).toUpperCase() + name.slice(1);
988
+ return this.config.connection.model(modelName, mongooseSchema);
989
+ }
990
+ throw new Error(
991
+ `Cannot resolve Mongoose model for collection '${name}'. Provide a Mongoose Model, a Mongoose Schema, or set the model option.`
992
+ );
993
+ }
994
+ };
995
+
996
+ export { BadRequestError, FieldType, ForbiddenError, Monapi, MonapiError, MongooseAdapter, NotFoundError, SchemaType, UnauthorizedError, ValidationError, buildPaginationMeta, buildQuery, createErrorHandler, createSchemaAdapter, detectSchemaType, parseFilters };
997
+ //# sourceMappingURL=index.mjs.map
998
+ //# sourceMappingURL=index.mjs.map