monapi 0.1.0 → 1.0.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.js CHANGED
@@ -1,9 +1,14 @@
1
1
  'use strict';
2
2
 
3
- var express = require('express');
4
3
  var mongoose = require('mongoose');
4
+ var express = require('express');
5
5
 
6
- // src/monapi.ts
6
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
7
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
8
+ }) : x)(function(x) {
9
+ if (typeof require !== "undefined") return require.apply(this, arguments);
10
+ throw Error('Dynamic require of "' + x + '" is not supported');
11
+ });
7
12
 
8
13
  // src/types/query.ts
9
14
  var FieldType = /* @__PURE__ */ ((FieldType2) => {
@@ -283,6 +288,43 @@ var BadRequestError = class _BadRequestError extends MonapiError {
283
288
  }
284
289
  };
285
290
 
291
+ // src/core/permission-checker.ts
292
+ var ROUTE_TO_CRUD = {
293
+ list: "find",
294
+ get: "find",
295
+ create: "create",
296
+ update: "update",
297
+ patch: "patch",
298
+ delete: "delete"
299
+ };
300
+ async function checkPermissions(req, collection, routeOp, permissions) {
301
+ if (!permissions) return;
302
+ const permission = permissions[routeOp];
303
+ if (!permission) return;
304
+ const user = req.user;
305
+ if (!user) throw new UnauthorizedError();
306
+ const crudOp = ROUTE_TO_CRUD[routeOp] || routeOp;
307
+ const allowed = await evaluatePermission(permission, {
308
+ user,
309
+ collection,
310
+ operation: crudOp,
311
+ data: req.body,
312
+ id: req.params.id,
313
+ req: req.raw
314
+ });
315
+ if (!allowed) throw new ForbiddenError();
316
+ }
317
+ async function evaluatePermission(permission, ctx) {
318
+ if (Array.isArray(permission)) {
319
+ if (!ctx.user.roles || ctx.user.roles.length === 0) return false;
320
+ return permission.some((role) => ctx.user.roles?.includes(role));
321
+ }
322
+ if (typeof permission === "function") {
323
+ return permission(ctx);
324
+ }
325
+ return false;
326
+ }
327
+
286
328
  // src/engine/filter-parser.ts
287
329
  var OPERATOR_MAP = {
288
330
  eq: "$eq",
@@ -525,376 +567,448 @@ function extractPagination(queryParams, defaultLimit = DEFAULT_LIMIT, maxLimit =
525
567
  return { page, limit };
526
568
  }
527
569
 
528
- // src/engine/hook-executor.ts
529
- function createHookContext(params) {
530
- const user = params.req.user;
570
+ // src/core/crud-operations.ts
571
+ function createCtx(collection, operation, req, res, extra = {}) {
531
572
  return {
532
- collection: params.collection,
533
- operation: params.operation,
534
- user,
535
- query: params.query,
536
- data: params.data,
537
- id: params.id,
538
- result: params.result,
539
- req: params.req,
540
- res: params.res,
573
+ collection,
574
+ operation,
575
+ user: req.user,
576
+ query: extra.query,
577
+ data: extra.data,
578
+ id: extra.id,
579
+ result: extra.result,
580
+ req: req.raw,
581
+ res: res.raw,
541
582
  meta: {}
542
583
  };
543
584
  }
544
- async function executeHook(hooks, hookName, ctx, logger) {
545
- if (!hooks) return ctx;
546
- const hookFn = hooks[hookName];
547
- if (!hookFn) return ctx;
585
+ async function runHook(hooks, hookName, ctx, logger) {
586
+ if (!hooks) return;
587
+ const fn = hooks[hookName];
588
+ if (!fn) return;
548
589
  try {
549
- await hookFn(ctx);
590
+ await fn(ctx);
550
591
  } catch (error) {
551
592
  if (logger) {
552
- logger.error(`Hook '${hookName}' failed for collection '${ctx.collection}': ${error.message}`);
593
+ logger.error(`Hook '${hookName}' failed for '${ctx.collection}': ${error.message}`);
553
594
  }
554
595
  throw error;
555
596
  }
556
- return ctx;
557
597
  }
558
-
559
- // src/engine/crud-handlers.ts
560
- function createCRUDHandlers(options) {
561
- const { collectionName, model, adapter, config, defaults, logger } = options;
598
+ async function listDocuments(req, res, opts) {
599
+ const { collectionName, model, adapter, config, defaults, logger } = opts;
600
+ const mongoQuery = buildQuery(req.query, {
601
+ adapter,
602
+ queryConfig: config.query ?? defaults?.query,
603
+ defaultLimit: defaults?.pagination?.limit,
604
+ maxLimit: defaults?.pagination?.maxLimit,
605
+ maxRegexLength: defaults?.security?.maxRegexLength
606
+ });
607
+ const ctx = createCtx(collectionName, "find", req, res, { query: mongoQuery });
608
+ await runHook(config.hooks, "beforeFind", ctx, logger);
609
+ if (ctx.preventDefault) return { statusCode: 0, data: null };
610
+ const query = ctx.query ?? mongoQuery;
611
+ const [docs, total] = await Promise.all([
612
+ model.find(query.filter).sort(query.sort).skip(query.skip ?? 0).limit(query.limit ?? 10).select(query.projection ?? {}).lean().exec(),
613
+ model.countDocuments(query.filter).exec()
614
+ ]);
615
+ const { page, limit } = extractPagination(
616
+ req.query,
617
+ defaults?.pagination?.limit,
618
+ defaults?.pagination?.maxLimit
619
+ );
620
+ ctx.result = docs;
621
+ await runHook(config.hooks, "afterFind", ctx, logger);
562
622
  return {
563
- list: config.handlers?.list ?? createListHandler(collectionName, model, adapter, config, defaults, logger),
564
- get: config.handlers?.get ?? createGetHandler(collectionName, model, adapter, config, logger),
565
- create: config.handlers?.create ?? createCreateHandler(collectionName, model, adapter, config, logger),
566
- update: config.handlers?.update ?? createUpdateHandler(collectionName, model, adapter, config, logger),
567
- patch: config.handlers?.patch ?? createPatchHandler(collectionName, model, adapter, config, logger),
568
- delete: config.handlers?.delete ?? createDeleteHandler(collectionName, model, config, logger)
623
+ statusCode: 200,
624
+ data: ctx.result ?? docs,
625
+ meta: buildPaginationMeta(total, page, limit)
569
626
  };
570
627
  }
571
- function createListHandler(collectionName, model, adapter, config, defaults, logger) {
572
- return async (req, res, next) => {
573
- try {
574
- const mongoQuery = buildQuery(req.query, {
575
- adapter,
576
- queryConfig: config.query ?? defaults?.query,
577
- defaultLimit: defaults?.pagination?.limit,
578
- maxLimit: defaults?.pagination?.maxLimit,
579
- maxRegexLength: defaults?.security?.maxRegexLength
580
- });
581
- const ctx = createHookContext({
582
- collection: collectionName,
583
- operation: "find",
584
- req,
585
- res,
586
- query: mongoQuery
587
- });
588
- await executeHook(config.hooks, "beforeFind", ctx, logger);
589
- if (ctx.preventDefault) {
590
- return;
628
+ async function getDocument(req, res, opts) {
629
+ const { collectionName, model, config, logger } = opts;
630
+ const { id } = req.params;
631
+ let projection;
632
+ if (req.query.fields) {
633
+ const fields = typeof req.query.fields === "string" ? req.query.fields.split(",") : req.query.fields;
634
+ projection = {};
635
+ for (const f of fields) {
636
+ const trimmed = f.trim();
637
+ if (trimmed && !trimmed.startsWith("$")) {
638
+ projection[trimmed] = 1;
591
639
  }
592
- const query = ctx.query ?? mongoQuery;
593
- const [docs, total] = await Promise.all([
594
- model.find(query.filter).sort(query.sort).skip(query.skip ?? 0).limit(query.limit ?? 10).select(query.projection ?? {}).lean().exec(),
595
- model.countDocuments(query.filter).exec()
596
- ]);
597
- const { page, limit } = extractPagination(req.query, defaults?.pagination?.limit, defaults?.pagination?.maxLimit);
598
- ctx.result = docs;
599
- await executeHook(config.hooks, "afterFind", ctx, logger);
600
- const response = {
601
- data: ctx.result ?? docs,
602
- meta: buildPaginationMeta(total, page, limit)
603
- };
604
- res.json(response);
605
- } catch (error) {
606
- next(error);
607
640
  }
608
- };
641
+ }
642
+ const ctx = createCtx(collectionName, "find", req, res, { id });
643
+ await runHook(config.hooks, "beforeFind", ctx, logger);
644
+ if (ctx.preventDefault) return { statusCode: 0, data: null };
645
+ let query = model.findById(id);
646
+ if (projection) query = query.select(projection);
647
+ const doc = await query.lean().exec();
648
+ if (!doc) throw new NotFoundError(collectionName, id);
649
+ ctx.result = doc;
650
+ await runHook(config.hooks, "afterFind", ctx, logger);
651
+ return { statusCode: 200, data: ctx.result ?? doc };
609
652
  }
610
- function createGetHandler(collectionName, model, _adapter, config, logger) {
611
- return async (req, res, next) => {
612
- try {
613
- const { id } = req.params;
614
- const fieldsParam = req.query.fields;
615
- let projection;
616
- if (fieldsParam) {
617
- const fields = typeof fieldsParam === "string" ? fieldsParam.split(",") : fieldsParam;
618
- projection = {};
619
- for (const f of fields) {
620
- const trimmed = f.trim();
621
- if (trimmed && !trimmed.startsWith("$")) {
622
- projection[trimmed] = 1;
623
- }
624
- }
625
- }
626
- const ctx = createHookContext({
627
- collection: collectionName,
628
- operation: "find",
629
- req,
630
- res,
631
- id
632
- });
633
- await executeHook(config.hooks, "beforeFind", ctx, logger);
634
- if (ctx.preventDefault) return;
635
- let query = model.findById(id);
636
- if (projection) query = query.select(projection);
637
- const doc = await query.lean().exec();
638
- if (!doc) {
639
- throw new NotFoundError(collectionName, id);
640
- }
641
- ctx.result = doc;
642
- await executeHook(config.hooks, "afterFind", ctx, logger);
643
- const response = { data: ctx.result ?? doc };
644
- res.json(response);
645
- } catch (error) {
646
- next(error);
647
- }
648
- };
653
+ async function createDocument(req, res, opts) {
654
+ const { collectionName, model, adapter, config, logger } = opts;
655
+ const data = req.body;
656
+ const validation = await adapter.validate(data);
657
+ if (!validation.valid) {
658
+ throw new ValidationError("Validation failed", validation.errors);
659
+ }
660
+ const ctx = createCtx(collectionName, "create", req, res, { data: validation.data ?? data });
661
+ await runHook(config.hooks, "beforeCreate", ctx, logger);
662
+ if (ctx.preventDefault) return { statusCode: 0, data: null };
663
+ const doc = await model.create(ctx.data ?? data);
664
+ const result = doc.toObject();
665
+ ctx.result = result;
666
+ await runHook(config.hooks, "afterCreate", ctx, logger);
667
+ return { statusCode: 201, data: ctx.result ?? result };
649
668
  }
650
- function createCreateHandler(collectionName, model, adapter, config, logger) {
651
- return async (req, res, next) => {
652
- try {
653
- const data = req.body;
654
- const validation = await adapter.validate(data);
655
- if (!validation.valid) {
656
- throw new ValidationError("Validation failed", validation.errors);
657
- }
658
- const ctx = createHookContext({
659
- collection: collectionName,
660
- operation: "create",
661
- req,
662
- res,
663
- data: validation.data ?? data
664
- });
665
- await executeHook(config.hooks, "beforeCreate", ctx, logger);
666
- if (ctx.preventDefault) return;
667
- const doc = await model.create(ctx.data ?? data);
668
- const result = doc.toObject();
669
- ctx.result = result;
670
- await executeHook(config.hooks, "afterCreate", ctx, logger);
671
- const response = { data: ctx.result ?? result };
672
- res.status(201).json(response);
673
- } catch (error) {
674
- next(error);
675
- }
676
- };
669
+ async function updateDocument(req, res, opts) {
670
+ const { collectionName, model, adapter, config, logger } = opts;
671
+ const { id } = req.params;
672
+ const data = req.body;
673
+ const validation = await adapter.validate(data);
674
+ if (!validation.valid) {
675
+ throw new ValidationError("Validation failed", validation.errors);
676
+ }
677
+ const ctx = createCtx(collectionName, "update", req, res, { id, data: validation.data ?? data });
678
+ await runHook(config.hooks, "beforeUpdate", ctx, logger);
679
+ if (ctx.preventDefault) return { statusCode: 0, data: null };
680
+ const doc = await model.findByIdAndUpdate(id, ctx.data ?? data, { new: true, runValidators: true, overwrite: true }).lean().exec();
681
+ if (!doc) throw new NotFoundError(collectionName, id);
682
+ ctx.result = doc;
683
+ await runHook(config.hooks, "afterUpdate", ctx, logger);
684
+ return { statusCode: 200, data: ctx.result ?? doc };
677
685
  }
678
- function createUpdateHandler(collectionName, model, adapter, config, logger) {
679
- return async (req, res, next) => {
680
- try {
681
- const { id } = req.params;
682
- const data = req.body;
683
- const validation = await adapter.validate(data);
684
- if (!validation.valid) {
685
- throw new ValidationError("Validation failed", validation.errors);
686
- }
687
- const ctx = createHookContext({
688
- collection: collectionName,
689
- operation: "update",
690
- req,
691
- res,
692
- id,
693
- data: validation.data ?? data
694
- });
695
- await executeHook(config.hooks, "beforeUpdate", ctx, logger);
696
- if (ctx.preventDefault) return;
697
- const doc = await model.findByIdAndUpdate(id, ctx.data ?? data, { new: true, runValidators: true, overwrite: true }).lean().exec();
698
- if (!doc) {
699
- throw new NotFoundError(collectionName, id);
700
- }
701
- ctx.result = doc;
702
- await executeHook(config.hooks, "afterUpdate", ctx, logger);
703
- const response = { data: ctx.result ?? doc };
704
- res.json(response);
705
- } catch (error) {
706
- next(error);
686
+ async function patchDocument(req, res, opts) {
687
+ const { collectionName, model, config, logger } = opts;
688
+ const { id } = req.params;
689
+ const data = req.body;
690
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
691
+ throw new ValidationError("Request body must be an object");
692
+ }
693
+ for (const key of Object.keys(data)) {
694
+ if (key.startsWith("$")) {
695
+ throw new ValidationError(`Invalid field name: ${key}`);
707
696
  }
708
- };
697
+ }
698
+ const ctx = createCtx(collectionName, "patch", req, res, { id, data });
699
+ await runHook(config.hooks, "beforeUpdate", ctx, logger);
700
+ if (ctx.preventDefault) return { statusCode: 0, data: null };
701
+ const doc = await model.findByIdAndUpdate(id, { $set: ctx.data ?? data }, { new: true, runValidators: true }).lean().exec();
702
+ if (!doc) throw new NotFoundError(collectionName, id);
703
+ ctx.result = doc;
704
+ await runHook(config.hooks, "afterUpdate", ctx, logger);
705
+ return { statusCode: 200, data: ctx.result ?? doc };
709
706
  }
710
- function createPatchHandler(collectionName, model, _adapter, config, logger) {
711
- return async (req, res, next) => {
712
- try {
713
- const { id } = req.params;
714
- const data = req.body;
715
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
716
- throw new ValidationError("Request body must be an object");
717
- }
718
- for (const key of Object.keys(data)) {
719
- if (key.startsWith("$")) {
720
- throw new ValidationError(`Invalid field name: ${key}`);
721
- }
722
- }
723
- const ctx = createHookContext({
724
- collection: collectionName,
725
- operation: "patch",
726
- req,
727
- res,
728
- id,
729
- data
730
- });
731
- await executeHook(config.hooks, "beforeUpdate", ctx, logger);
732
- if (ctx.preventDefault) return;
733
- const doc = await model.findByIdAndUpdate(id, { $set: ctx.data ?? data }, { new: true, runValidators: true }).lean().exec();
734
- if (!doc) {
735
- throw new NotFoundError(collectionName, id);
736
- }
737
- ctx.result = doc;
738
- await executeHook(config.hooks, "afterUpdate", ctx, logger);
739
- const response = { data: ctx.result ?? doc };
740
- res.json(response);
741
- } catch (error) {
742
- next(error);
743
- }
707
+ async function deleteDocument(req, res, opts) {
708
+ const { collectionName, model, config, logger } = opts;
709
+ const { id } = req.params;
710
+ const ctx = createCtx(collectionName, "delete", req, res, { id });
711
+ await runHook(config.hooks, "beforeDelete", ctx, logger);
712
+ if (ctx.preventDefault) return { statusCode: 0, data: null };
713
+ const doc = await model.findByIdAndDelete(id).lean().exec();
714
+ if (!doc) throw new NotFoundError(collectionName, id);
715
+ ctx.result = doc;
716
+ await runHook(config.hooks, "afterDelete", ctx, logger);
717
+ return { statusCode: 200, data: ctx.result ?? doc };
718
+ }
719
+
720
+ // src/adapters/framework/express.ts
721
+ function toMonapiRequest(req) {
722
+ return {
723
+ params: req.params,
724
+ query: req.query,
725
+ body: req.body,
726
+ headers: req.headers,
727
+ method: req.method,
728
+ path: req.path,
729
+ user: req.user,
730
+ raw: req
744
731
  };
745
732
  }
746
- function createDeleteHandler(collectionName, model, config, logger) {
747
- return async (req, res, next) => {
748
- try {
749
- const { id } = req.params;
750
- const ctx = createHookContext({
751
- collection: collectionName,
752
- operation: "delete",
753
- req,
754
- res,
755
- id
756
- });
757
- await executeHook(config.hooks, "beforeDelete", ctx, logger);
758
- if (ctx.preventDefault) return;
759
- const doc = await model.findByIdAndDelete(id).lean().exec();
760
- if (!doc) {
761
- throw new NotFoundError(collectionName, id);
762
- }
763
- ctx.result = doc;
764
- await executeHook(config.hooks, "afterDelete", ctx, logger);
765
- res.json({ data: ctx.result ?? doc });
766
- } catch (error) {
767
- next(error);
733
+ function toMonapiResponse(res) {
734
+ const wrapper = {
735
+ raw: res,
736
+ status(code) {
737
+ res.status(code);
738
+ return wrapper;
739
+ },
740
+ json(data) {
741
+ res.json(data);
742
+ },
743
+ setHeader(key, value) {
744
+ res.setHeader(key, value);
745
+ return wrapper;
768
746
  }
769
747
  };
748
+ return wrapper;
770
749
  }
771
-
772
- // src/middleware/auth.ts
773
- var ROUTE_TO_CRUD = {
774
- list: "find",
775
- get: "find",
776
- create: "create",
777
- update: "update",
778
- patch: "patch",
779
- delete: "delete"
780
- };
781
- function createPermissionMiddleware(collection, routeOp, permissions) {
782
- return async (req, _res, next) => {
783
- try {
784
- if (!permissions) {
785
- next();
786
- return;
750
+ var ExpressAdapter = class {
751
+ constructor() {
752
+ this.name = "express";
753
+ }
754
+ createRouter(collections, options) {
755
+ const mainRouter = express.Router();
756
+ const basePath = options?.basePath ?? "";
757
+ for (const [name, ctx] of collections) {
758
+ const collectionRouter = this.buildCollectionRouter(ctx, options?.authMiddleware);
759
+ const path = basePath ? `${basePath}/${name}` : `/${name}`;
760
+ mainRouter.use(path, collectionRouter);
761
+ }
762
+ return mainRouter;
763
+ }
764
+ wrapHandler(handler) {
765
+ return async (req, res, next) => {
766
+ try {
767
+ await handler(toMonapiRequest(req), toMonapiResponse(res));
768
+ } catch (error) {
769
+ next(error);
787
770
  }
788
- const permission = permissions[routeOp];
789
- if (!permission) {
790
- next();
771
+ };
772
+ }
773
+ createErrorHandler(logger) {
774
+ return (err, _req, res, _next) => {
775
+ if (err instanceof MonapiError) {
776
+ if (logger) logger.warn(`${err.code}: ${err.message}`, { statusCode: err.statusCode });
777
+ res.status(err.statusCode).json({
778
+ error: { code: err.code, message: err.message, details: err.details }
779
+ });
791
780
  return;
792
781
  }
793
- const user = req.user;
794
- if (!user) {
795
- throw new UnauthorizedError();
782
+ if (logger) logger.error(`Unhandled error: ${err.message}`, { stack: err.stack });
783
+ const message = process.env.NODE_ENV === "production" ? "Internal server error" : err.message;
784
+ res.status(500).json({ error: { code: "INTERNAL_ERROR", message } });
785
+ };
786
+ }
787
+ buildCollectionRouter(ctx, authMiddleware) {
788
+ const router = express.Router();
789
+ const { name, config } = ctx;
790
+ const ops = ["list", "get", "create", "update", "patch", "delete"];
791
+ const opHandlers = {
792
+ list: (mReq, mRes) => this.handleOp(listDocuments, mReq, mRes, ctx),
793
+ get: (mReq, mRes) => this.handleOp(getDocument, mReq, mRes, ctx),
794
+ create: (mReq, mRes) => this.handleOp(createDocument, mReq, mRes, ctx),
795
+ update: (mReq, mRes) => this.handleOp(updateDocument, mReq, mRes, ctx),
796
+ patch: (mReq, mRes) => this.handleOp(patchDocument, mReq, mRes, ctx),
797
+ delete: (mReq, mRes) => this.handleOp(deleteDocument, mReq, mRes, ctx)
798
+ };
799
+ for (const op of ops) {
800
+ const stack = [];
801
+ if (authMiddleware) stack.push(authMiddleware);
802
+ if (config.middleware?.all) stack.push(...config.middleware.all);
803
+ if (config.middleware?.[op]) stack.push(...config.middleware[op]);
804
+ if (config.permissions) {
805
+ stack.push(async (req, _res, next) => {
806
+ try {
807
+ await checkPermissions(toMonapiRequest(req), name, op, config.permissions);
808
+ next();
809
+ } catch (error) {
810
+ next(error);
811
+ }
812
+ });
796
813
  }
797
- const crudOp = ROUTE_TO_CRUD[routeOp] || routeOp;
798
- const allowed = await checkPermission(permission, {
799
- user,
800
- collection,
801
- operation: crudOp,
802
- data: req.body,
803
- id: req.params.id,
804
- req
805
- });
806
- if (!allowed) {
807
- throw new ForbiddenError();
814
+ const handler = config.handlers?.[op] ? config.handlers[op] : this.wrapHandler(opHandlers[op]);
815
+ switch (op) {
816
+ case "list":
817
+ router.get("/", ...stack, handler);
818
+ break;
819
+ case "get":
820
+ router.get("/:id", ...stack, handler);
821
+ break;
822
+ case "create":
823
+ router.post("/", ...stack, handler);
824
+ break;
825
+ case "update":
826
+ router.put("/:id", ...stack, handler);
827
+ break;
828
+ case "patch":
829
+ router.patch("/:id", ...stack, handler);
830
+ break;
831
+ case "delete":
832
+ router.delete("/:id", ...stack, handler);
833
+ break;
808
834
  }
809
- next();
810
- } catch (error) {
811
- next(error);
812
835
  }
836
+ return router;
837
+ }
838
+ async handleOp(operation, mReq, mRes, ctx) {
839
+ const result = await operation(mReq, mRes, {
840
+ collectionName: ctx.name,
841
+ model: ctx.model,
842
+ adapter: ctx.adapter,
843
+ config: ctx.config,
844
+ defaults: ctx.defaults,
845
+ logger: ctx.logger
846
+ });
847
+ if (result.statusCode === 0) return;
848
+ if (result.meta) {
849
+ mRes.status(result.statusCode).json({ data: result.data, meta: result.meta });
850
+ } else {
851
+ mRes.status(result.statusCode).json({ data: result.data });
852
+ }
853
+ }
854
+ };
855
+
856
+ // src/adapters/framework/hono.ts
857
+ function toMonapiRequest2(c) {
858
+ return {
859
+ params: c.req.param() || {},
860
+ query: c.req.query() || {},
861
+ body: null,
862
+ // body is async in Hono - set separately
863
+ headers: Object.fromEntries(c.req.raw.headers.entries()),
864
+ method: c.req.method,
865
+ path: c.req.path,
866
+ user: c.get("user"),
867
+ raw: c
813
868
  };
814
869
  }
815
- async function checkPermission(permission, ctx) {
816
- if (Array.isArray(permission)) {
817
- if (!ctx.user.roles || ctx.user.roles.length === 0) {
818
- return false;
870
+ function toMonapiResponse2(c) {
871
+ let statusCode = 200;
872
+ let responseData = null;
873
+ const wrapper = {
874
+ raw: c,
875
+ status(code) {
876
+ statusCode = code;
877
+ return wrapper;
878
+ },
879
+ json(data) {
880
+ responseData = data;
881
+ },
882
+ setHeader(key, value) {
883
+ c.header(key, value);
884
+ return wrapper;
819
885
  }
820
- return permission.some((role) => ctx.user.roles?.includes(role));
886
+ };
887
+ wrapper._getResponse = () => ({ statusCode, data: responseData });
888
+ return wrapper;
889
+ }
890
+ var HonoAdapter = class {
891
+ constructor() {
892
+ this.name = "hono";
821
893
  }
822
- if (typeof permission === "function") {
823
- return permission(ctx);
894
+ /**
895
+ * Returns a Hono app instance with all collection routes registered.
896
+ * Expects Hono to be available at runtime (peer dependency).
897
+ */
898
+ createRouter(collections, options) {
899
+ let Hono;
900
+ try {
901
+ Hono = __require("hono").Hono;
902
+ } catch {
903
+ throw new Error(
904
+ "Hono is required for the Hono adapter. Install it: npm install hono"
905
+ );
906
+ }
907
+ const app = new Hono();
908
+ if (options?.authMiddleware) {
909
+ app.use("*", options.authMiddleware);
910
+ }
911
+ app.onError((err, c) => {
912
+ if (err instanceof MonapiError) {
913
+ return c.json(
914
+ { error: { code: err.code, message: err.message, details: err.details } },
915
+ err.statusCode
916
+ );
917
+ }
918
+ const message = process.env.NODE_ENV === "production" ? "Internal server error" : err.message;
919
+ return c.json({ error: { code: "INTERNAL_ERROR", message } }, 500);
920
+ });
921
+ for (const [name, ctx] of collections) {
922
+ this.registerCollectionRoutes(app, `/${name}`, ctx);
923
+ }
924
+ return app;
824
925
  }
825
- return false;
826
- }
827
- function createAuthMiddleware(authConfig) {
828
- if (authConfig?.middleware) {
829
- return authConfig.middleware;
926
+ wrapHandler(handler) {
927
+ return async (c) => {
928
+ const mReq = toMonapiRequest2(c);
929
+ if (["POST", "PUT", "PATCH"].includes(c.req.method)) {
930
+ try {
931
+ mReq.body = await c.req.json();
932
+ } catch {
933
+ mReq.body = {};
934
+ }
935
+ }
936
+ const mRes = toMonapiResponse2(c);
937
+ await handler(mReq, mRes);
938
+ const { statusCode, data } = mRes._getResponse();
939
+ if (data !== null) {
940
+ return c.json(data, statusCode);
941
+ }
942
+ };
830
943
  }
831
- return (_req, _res, next) => {
832
- next();
833
- };
834
- }
835
-
836
- // src/router/express-router.ts
837
- function createCollectionRouter(options) {
838
- const { collectionName, model, adapter, config, defaults, logger, authMiddleware } = options;
839
- const router = express.Router();
840
- const handlers = createCRUDHandlers({ collectionName, model, adapter, config, defaults, logger });
841
- const operations = ["list", "get", "create", "update", "patch", "delete"];
842
- const middlewareStacks = {};
843
- for (const op of operations) {
844
- const stack = [];
845
- if (authMiddleware) {
846
- stack.push(authMiddleware);
847
- }
848
- if (config.middleware?.all) {
849
- stack.push(...config.middleware.all);
850
- }
851
- const opMiddleware = config.middleware?.[op];
852
- if (opMiddleware) {
853
- stack.push(...opMiddleware);
854
- }
855
- if (config.permissions) {
856
- stack.push(createPermissionMiddleware(collectionName, op, config.permissions));
857
- }
858
- middlewareStacks[op] = stack;
859
- }
860
- router.get("/", ...middlewareStacks.list, handlers.list);
861
- router.get("/:id", ...middlewareStacks.get, handlers.get);
862
- router.post("/", ...middlewareStacks.create, handlers.create);
863
- router.put("/:id", ...middlewareStacks.update, handlers.update);
864
- router.patch("/:id", ...middlewareStacks.patch, handlers.patch);
865
- router.delete("/:id", ...middlewareStacks.delete, handlers.delete);
866
- return router;
867
- }
868
-
869
- // src/middleware/error-handler.ts
870
- function createErrorHandler(logger) {
871
- return (err, _req, res, _next) => {
872
- if (err instanceof MonapiError) {
873
- if (logger) {
874
- logger.warn(`${err.code}: ${err.message}`, { statusCode: err.statusCode });
944
+ createErrorHandler(logger) {
945
+ return (err, c) => {
946
+ if (err instanceof MonapiError) {
947
+ if (logger) logger.warn(`${err.code}: ${err.message}`);
948
+ return c.json(
949
+ { error: { code: err.code, message: err.message, details: err.details } },
950
+ err.statusCode
951
+ );
875
952
  }
876
- const response2 = {
877
- error: {
878
- code: err.code,
879
- message: err.message,
880
- details: err.details
953
+ if (logger) logger.error(`Unhandled error: ${err.message}`);
954
+ const message = process.env.NODE_ENV === "production" ? "Internal server error" : err.message;
955
+ return c.json({ error: { code: "INTERNAL_ERROR", message } }, 500);
956
+ };
957
+ }
958
+ registerCollectionRoutes(app, prefix, ctx) {
959
+ const { name, config } = ctx;
960
+ const createHandler = (op, handler) => {
961
+ return async (c) => {
962
+ const mReq = toMonapiRequest2(c);
963
+ if (["POST", "PUT", "PATCH"].includes(c.req.method)) {
964
+ try {
965
+ mReq.body = await c.req.json();
966
+ } catch {
967
+ mReq.body = {};
968
+ }
969
+ }
970
+ if (config.permissions) {
971
+ await checkPermissions(mReq, name, op, config.permissions);
972
+ }
973
+ const mRes = toMonapiResponse2(c);
974
+ const result = await handler(mReq, mRes, {
975
+ collectionName: ctx.name,
976
+ model: ctx.model,
977
+ adapter: ctx.adapter,
978
+ config: ctx.config,
979
+ defaults: ctx.defaults,
980
+ logger: ctx.logger
981
+ });
982
+ if (result.statusCode === 0) return c.body(null, 204);
983
+ if (result.meta) {
984
+ return c.json({ data: result.data, meta: result.meta }, result.statusCode);
881
985
  }
986
+ return c.json({ data: result.data }, result.statusCode);
882
987
  };
883
- res.status(err.statusCode).json(response2);
884
- return;
885
- }
886
- if (logger) {
887
- logger.error(`Unhandled error: ${err.message}`, { stack: err.stack });
888
- }
889
- const message = process.env.NODE_ENV === "production" ? "Internal server error" : err.message;
890
- const response = {
891
- error: {
892
- code: "INTERNAL_ERROR",
893
- message
894
- }
895
988
  };
896
- res.status(500).json(response);
897
- };
989
+ app.get(prefix, createHandler("list", listDocuments));
990
+ app.get(`${prefix}/:id`, createHandler("get", getDocument));
991
+ app.post(prefix, createHandler("create", createDocument));
992
+ app.put(`${prefix}/:id`, createHandler("update", updateDocument));
993
+ app.patch(`${prefix}/:id`, createHandler("patch", patchDocument));
994
+ app.delete(`${prefix}/:id`, createHandler("delete", deleteDocument));
995
+ }
996
+ };
997
+
998
+ // src/adapters/framework/index.ts
999
+ function resolveFrameworkAdapter(framework) {
1000
+ if (!framework || framework === "express") {
1001
+ return new ExpressAdapter();
1002
+ }
1003
+ if (typeof framework === "string") {
1004
+ switch (framework) {
1005
+ case "hono":
1006
+ return new HonoAdapter();
1007
+ default:
1008
+ throw new Error(`Unknown framework: ${framework}. Use 'express', 'hono', or pass a custom FrameworkAdapter.`);
1009
+ }
1010
+ }
1011
+ return framework;
898
1012
  }
899
1013
 
900
1014
  // src/utils/logger.ts
@@ -921,9 +1035,10 @@ var Monapi = class {
921
1035
  this.collections = /* @__PURE__ */ new Map();
922
1036
  this.config = config;
923
1037
  this.logger = config.logger ?? defaultLogger;
924
- if (config.auth) {
925
- this.authMiddleware = createAuthMiddleware(config.auth);
926
- }
1038
+ this.frameworkAdapter = resolveFrameworkAdapter(
1039
+ config.framework
1040
+ );
1041
+ this.logger.debug(`Using framework adapter: ${this.frameworkAdapter.name}`);
927
1042
  }
928
1043
  /**
929
1044
  * Register a collection resource.
@@ -932,43 +1047,56 @@ var Monapi = class {
932
1047
  resource(name, collectionConfig) {
933
1048
  const adapter = collectionConfig.adapter ?? createSchemaAdapter(collectionConfig.schema);
934
1049
  const model = this.resolveModel(name, collectionConfig, adapter);
935
- this.collections.set(name, { config: collectionConfig, model, adapter });
1050
+ this.collections.set(name, {
1051
+ name,
1052
+ model,
1053
+ adapter,
1054
+ config: collectionConfig,
1055
+ defaults: this.config.defaults,
1056
+ logger: this.logger
1057
+ });
936
1058
  this.logger.debug(`Registered resource: ${name}`, {
937
1059
  fields: adapter.getFields()
938
1060
  });
939
1061
  return this;
940
1062
  }
941
1063
  /**
942
- * Generate the Express router with all registered collection routes.
1064
+ * Generate the framework-specific router with all registered collection routes.
1065
+ *
1066
+ * - Express: returns an Express Router
1067
+ * - Fastify: returns a Fastify plugin function
1068
+ * - Hono: returns a Hono app instance
1069
+ * - NestJS: returns a dynamic module definition
943
1070
  */
944
1071
  router() {
945
- const mainRouter = express.Router();
946
- const basePath = this.config.basePath ?? "";
947
- for (const [name, { config, model, adapter }] of this.collections) {
948
- const collectionRouter = createCollectionRouter({
949
- collectionName: name,
950
- model,
951
- adapter,
952
- config,
953
- defaults: this.config.defaults,
954
- logger: this.logger,
955
- authMiddleware: this.authMiddleware
956
- });
1072
+ const result = this.frameworkAdapter.createRouter(this.collections, {
1073
+ basePath: this.config.basePath,
1074
+ authMiddleware: this.config.auth?.middleware
1075
+ });
1076
+ if (this.frameworkAdapter.name === "express") {
1077
+ result.use(this.frameworkAdapter.createErrorHandler(this.logger));
1078
+ }
1079
+ for (const [name] of this.collections) {
1080
+ const basePath = this.config.basePath ?? "";
957
1081
  const path = basePath ? `${basePath}/${name}` : `/${name}`;
958
- mainRouter.use(path, collectionRouter);
959
- this.logger.info(`Mounted routes: ${path}`);
1082
+ this.logger.info(`Mounted routes: ${path} [${this.frameworkAdapter.name}]`);
960
1083
  }
961
- mainRouter.use(createErrorHandler(this.logger));
962
- return mainRouter;
1084
+ return result;
1085
+ }
1086
+ /**
1087
+ * Get the framework adapter instance.
1088
+ */
1089
+ getFrameworkAdapter() {
1090
+ return this.frameworkAdapter;
963
1091
  }
964
1092
  /**
965
- * Get a registered collection's model
1093
+ * Get a registered collection's model.
966
1094
  */
967
1095
  getModel(name) {
968
1096
  return this.collections.get(name)?.model;
969
1097
  }
970
1098
  /**
971
- * Get a registered collection's adapter
1099
+ * Get a registered collection's adapter.
972
1100
  */
973
1101
  getAdapter(name) {
974
1102
  return this.collections.get(name)?.adapter;
@@ -995,9 +1123,42 @@ var Monapi = class {
995
1123
  }
996
1124
  };
997
1125
 
1126
+ // src/middleware/error-handler.ts
1127
+ function createErrorHandler(logger) {
1128
+ return (err, _req, res, _next) => {
1129
+ if (err instanceof MonapiError) {
1130
+ if (logger) {
1131
+ logger.warn(`${err.code}: ${err.message}`, { statusCode: err.statusCode });
1132
+ }
1133
+ const response2 = {
1134
+ error: {
1135
+ code: err.code,
1136
+ message: err.message,
1137
+ details: err.details
1138
+ }
1139
+ };
1140
+ res.status(err.statusCode).json(response2);
1141
+ return;
1142
+ }
1143
+ if (logger) {
1144
+ logger.error(`Unhandled error: ${err.message}`, { stack: err.stack });
1145
+ }
1146
+ const message = process.env.NODE_ENV === "production" ? "Internal server error" : err.message;
1147
+ const response = {
1148
+ error: {
1149
+ code: "INTERNAL_ERROR",
1150
+ message
1151
+ }
1152
+ };
1153
+ res.status(500).json(response);
1154
+ };
1155
+ }
1156
+
998
1157
  exports.BadRequestError = BadRequestError;
1158
+ exports.ExpressAdapter = ExpressAdapter;
999
1159
  exports.FieldType = FieldType;
1000
1160
  exports.ForbiddenError = ForbiddenError;
1161
+ exports.HonoAdapter = HonoAdapter;
1001
1162
  exports.Monapi = Monapi;
1002
1163
  exports.MonapiError = MonapiError;
1003
1164
  exports.MongooseAdapter = MongooseAdapter;
@@ -1007,9 +1168,17 @@ exports.UnauthorizedError = UnauthorizedError;
1007
1168
  exports.ValidationError = ValidationError;
1008
1169
  exports.buildPaginationMeta = buildPaginationMeta;
1009
1170
  exports.buildQuery = buildQuery;
1171
+ exports.checkPermissions = checkPermissions;
1172
+ exports.createDocument = createDocument;
1010
1173
  exports.createErrorHandler = createErrorHandler;
1011
1174
  exports.createSchemaAdapter = createSchemaAdapter;
1175
+ exports.deleteDocument = deleteDocument;
1012
1176
  exports.detectSchemaType = detectSchemaType;
1177
+ exports.getDocument = getDocument;
1178
+ exports.listDocuments = listDocuments;
1013
1179
  exports.parseFilters = parseFilters;
1180
+ exports.patchDocument = patchDocument;
1181
+ exports.resolveFrameworkAdapter = resolveFrameworkAdapter;
1182
+ exports.updateDocument = updateDocument;
1014
1183
  //# sourceMappingURL=index.js.map
1015
1184
  //# sourceMappingURL=index.js.map