@classytic/arc 2.7.7 → 2.8.1

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 (119) hide show
  1. package/README.md +11 -2
  2. package/dist/{BaseController-CpMfCXdn.mjs → BaseController-DAGGc5Xn.mjs} +76 -25
  3. package/dist/{EventTransport-C4VheKeC.d.mts → EventTransport-CLXJUzyT.d.mts} +37 -1
  4. package/dist/{ResourceRegistry-DsHiG9cL.mjs → ResourceRegistry-Dtcojmu8.mjs} +14 -2
  5. package/dist/adapters/index.d.mts +2 -2
  6. package/dist/adapters/index.mjs +1 -1
  7. package/dist/{adapters-BxGgSHjj.mjs → adapters-BBqAVvPK.mjs} +11 -0
  8. package/dist/audit/index.d.mts +1 -1
  9. package/dist/audit/index.mjs +1 -1
  10. package/dist/audit/mongodb.d.mts +1 -1
  11. package/dist/audit/mongodb.mjs +1 -1
  12. package/dist/auth/index.d.mts +4 -4
  13. package/dist/auth/index.mjs +3 -3
  14. package/dist/auth/redis-session.d.mts +1 -1
  15. package/dist/{betterAuthOpenApi-EkPaMWNM.mjs → betterAuthOpenApi-C5lDyRH2.mjs} +1 -1
  16. package/dist/cache/index.d.mts +2 -2
  17. package/dist/cache/index.mjs +1 -1
  18. package/dist/cli/commands/docs.mjs +2 -2
  19. package/dist/cli/commands/generate.mjs +1 -1
  20. package/dist/cli/commands/introspect.mjs +1 -1
  21. package/dist/core/index.d.mts +2 -2
  22. package/dist/core/index.mjs +4 -3
  23. package/dist/core-CrLDuqoT.mjs +34 -0
  24. package/dist/{core-B_zEeA2b.mjs → createActionRouter-Df1BuawX.mjs} +88 -52
  25. package/dist/{createApp-D7e77m8C.mjs → createApp-p2OThysU.mjs} +10 -10
  26. package/dist/{defineResource-BW2dMCu9.mjs → defineResource-CqeUltrW.mjs} +91 -8
  27. package/dist/docs/index.d.mts +2 -2
  28. package/dist/docs/index.mjs +1 -1
  29. package/dist/dynamic/index.d.mts +2 -2
  30. package/dist/dynamic/index.mjs +1 -1
  31. package/dist/{elevation-By_p2lnn.mjs → elevation-BBGFjzIP.mjs} +1 -1
  32. package/dist/{errorHandler-CH8wk1eD.mjs → errorHandler-Cw34h_om.mjs} +1 -1
  33. package/dist/{errorHandler-pCpEtNd7.d.mts → errorHandler-DJ7OAB2V.d.mts} +1 -1
  34. package/dist/{eventPlugin-CdvUoUna.d.mts → eventPlugin-Cdjwo0Gv.d.mts} +1 -1
  35. package/dist/{eventPlugin-B6U_nCFU.mjs → eventPlugin-XijlQmlL.mjs} +19 -1
  36. package/dist/events/index.d.mts +399 -28
  37. package/dist/events/index.mjs +345 -29
  38. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  39. package/dist/events/transports/redis.d.mts +1 -1
  40. package/dist/factory/index.d.mts +1 -1
  41. package/dist/factory/index.mjs +1 -1
  42. package/dist/hooks/index.d.mts +1 -1
  43. package/dist/hooks/index.mjs +1 -1
  44. package/dist/idempotency/index.d.mts +3 -3
  45. package/dist/idempotency/mongodb.d.mts +1 -1
  46. package/dist/idempotency/redis.d.mts +1 -1
  47. package/dist/{index-C9eYNjGR.d.mts → index-0zj73o2U.d.mts} +1 -1
  48. package/dist/{index-B0extFr4.d.mts → index-CBru2y5Y.d.mts} +3 -3
  49. package/dist/{index-BjShrzoj.d.mts → index-DadoLP51.d.mts} +48 -16
  50. package/dist/index.d.mts +8 -8
  51. package/dist/index.mjs +8 -8
  52. package/dist/integrations/event-gateway.d.mts +1 -1
  53. package/dist/integrations/event-gateway.mjs +1 -1
  54. package/dist/integrations/index.d.mts +1 -1
  55. package/dist/integrations/mcp/index.d.mts +2 -2
  56. package/dist/integrations/mcp/index.mjs +1 -1
  57. package/dist/integrations/mcp/testing.d.mts +1 -1
  58. package/dist/integrations/mcp/testing.mjs +1 -1
  59. package/dist/{interface-B91alUzq.d.mts → interface-CS6d7HiB.d.mts} +693 -110
  60. package/dist/{mongodb-Cgu9F1Nd.d.mts → mongodb-B1eVtFhw.d.mts} +1 -1
  61. package/dist/{mongodb-B7zupyck.d.mts → mongodb-NShVZDMr.d.mts} +1 -1
  62. package/dist/{openapi-D7Z7VODz.mjs → openapi-q6rNKfZy.mjs} +49 -2
  63. package/dist/org/index.d.mts +2 -2
  64. package/dist/permissions/index.d.mts +3 -3
  65. package/dist/plugins/index.d.mts +4 -4
  66. package/dist/plugins/index.mjs +9 -9
  67. package/dist/plugins/tracing-entry.d.mts +1 -1
  68. package/dist/plugins/tracing-entry.mjs +1 -1
  69. package/dist/policies/index.d.mts +1 -1
  70. package/dist/presets/index.d.mts +3 -3
  71. package/dist/presets/multiTenant.d.mts +1 -1
  72. package/dist/{queryCachePlugin-Ckl71mkc.d.mts → queryCachePlugin-BCFVXnxK.d.mts} +1 -1
  73. package/dist/{redis-3TQxm2VZ.d.mts → redis-Bunu3qWg.d.mts} +1 -1
  74. package/dist/{redis-stream-Dag5LFa9.d.mts → redis-stream-BgrYzpeq.d.mts} +1 -1
  75. package/dist/registry/index.d.mts +1 -1
  76. package/dist/registry/index.mjs +2 -2
  77. package/dist/{resourceToTools-BJkoQoUP.mjs → resourceToTools-DNNWnZtx.mjs} +194 -64
  78. package/dist/rpc/index.d.mts +1 -1
  79. package/dist/rpc/index.mjs +1 -1
  80. package/dist/scope/index.d.mts +2 -2
  81. package/dist/scope/index.mjs +1 -1
  82. package/dist/{sse-6W0hjVS_.mjs → sse-CD5Hghpu.mjs} +1 -1
  83. package/dist/testing/index.d.mts +2 -2
  84. package/dist/testing/index.mjs +1 -1
  85. package/dist/types/index.d.mts +5 -5
  86. package/dist/{types-CKB47kiu.d.mts → types-BlOuKTPw.d.mts} +9 -9
  87. package/dist/{types-B4BNthET.d.mts → types-BoaZHr-2.d.mts} +1 -1
  88. package/dist/{types-C5g2oRC7.d.mts → types-D3b7hA00.d.mts} +1 -1
  89. package/dist/utils/index.d.mts +4 -16
  90. package/dist/utils/index.mjs +5 -5
  91. package/dist/{utils-B-l6410F.mjs → utils-7sJ8X83I.mjs} +1 -13
  92. package/package.json +6 -5
  93. package/skills/arc/SKILL.md +23 -4
  94. package/skills/arc/references/integrations.md +1 -1
  95. package/skills/arc/references/mcp.md +2 -0
  96. /package/dist/{HookSystem-BNYKnrXF.mjs → HookSystem-BjFu7zf1.mjs} +0 -0
  97. /package/dist/{caching-5DtLwIqb.mjs → caching-CHH-iHs3.mjs} +0 -0
  98. /package/dist/{circuitBreaker-BBPDt-J_.d.mts → circuitBreaker-BGVoB1hD.d.mts} +0 -0
  99. /package/dist/{circuitBreaker-l18oRgL5.mjs → circuitBreaker-cmi5XDv5.mjs} +0 -0
  100. /package/dist/{elevation-D7WK0RXq.d.mts → elevation-UJO3-NvX.d.mts} +0 -0
  101. /package/dist/{errors-Cg58SLNi.mjs → errors-BF2bIOIS.mjs} +0 -0
  102. /package/dist/{errors-BS6lZvWy.d.mts → errors-BI8kEKsO.d.mts} +0 -0
  103. /package/dist/{externalPaths-iba7jD3d.d.mts → externalPaths-BQ8QijNH.d.mts} +0 -0
  104. /package/dist/{fields-D4nMDqnK.d.mts → fields-DoeDgh2b.d.mts} +0 -0
  105. /package/dist/{interface-CSbZdv_3.d.mts → interface-CkkWm5uR.d.mts} +0 -0
  106. /package/dist/{interface-CG7oRZjX.d.mts → interface-bpoLKKqx.d.mts} +0 -0
  107. /package/dist/{logger-DLg8-Ueg.mjs → logger-CDjpjySd.mjs} +0 -0
  108. /package/dist/{metrics-Qnvwc-LQ.mjs → metrics-DuhiSEZI.mjs} +0 -0
  109. /package/dist/{mongodb-B7X7P1P8.mjs → mongodb-5Ff3w8jy.mjs} +0 -0
  110. /package/dist/{pluralize-Dckfq6US.mjs → pluralize-BneOJkpi.mjs} +0 -0
  111. /package/dist/{queryCachePlugin-CwTpR04-.mjs → queryCachePlugin-D0iIVhW_.mjs} +0 -0
  112. /package/dist/{registry-B3lRFBWo.mjs → registry-B0Wl7uVV.mjs} +0 -0
  113. /package/dist/{replyHelpers-uDUIYh7u.mjs → replyHelpers-CXtJDAZ0.mjs} +0 -0
  114. /package/dist/{requestContext-xHIKedG6.mjs → requestContext-DYvHl113.mjs} +0 -0
  115. /package/dist/{schemaConverter-Y5EejTnJ.mjs → schemaConverter-OxfCshus.mjs} +0 -0
  116. /package/dist/{sessionManager-CEo9jwPI.d.mts → sessionManager-BkzVU8h2.d.mts} +0 -0
  117. /package/dist/{tracing-DEqdGkr-.d.mts → tracing-xqXzWeaf.d.mts} +0 -0
  118. /package/dist/{types--D3vvfdt.d.mts → types-CN6JvmYz.d.mts} +0 -0
  119. /package/dist/{versioning-CdBbFefk.mjs → versioning-CPU_5Xfs.mjs} +0 -0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Database-agnostic resource framework for Fastify. Define resources, get CRUD routes, permissions, presets, caching, events, OpenAPI, and MCP tools — without boilerplate.
4
4
 
5
- **v2.7.3** | Fastify 5+ | Node.js 22+ | ESM only | 260+ test files, 3523+ tests
5
+ **v2.8.1** | Fastify 5+ | Node.js 22+ | ESM only | 260+ test files, 3600+ tests
6
6
 
7
7
  ## Install
8
8
 
@@ -694,7 +694,16 @@ npx @classytic/arc doctor # Health check
694
694
  | `@classytic/arc/docs` | OpenAPI generation |
695
695
  | `@classytic/arc/cli` | CLI commands (programmatic) |
696
696
 
697
- ## v2.7.3 Highlights
697
+ ## v2.8.1 Highlights
698
+
699
+ - **Per-action discriminated validation** — `actions` schemas now enforce required fields via a `oneOf` body schema; missing inputs are rejected at the HTTP layer by AJV (no more silent bypass)
700
+ - **Actions in OpenAPI** — `POST /:id/action` endpoint auto-generated from `ResourceDefinition.actions`, with per-action descriptions and the same discriminated body schema as the runtime router
701
+ - **Route/action metadata preserved** — `mcp: false`, `description`, `annotations` no longer dropped during `routes → additionalRoutes` normalization
702
+ - **Canonical source retained** — `ResourceDefinition.routes` and `ResourceDefinition.actions` now kept as declared, so OpenAPI/MCP/registry can read the original shape
703
+ - **Outbox hardening** — expanded `OutboxStore` contract (`claimPending`, `fail`, write options, dedupe, visibleAt), ownership-mismatch throws, onError reporting, safe multi-worker relay
704
+ - **`slugLookup` fallback** — works with MongoKit's default Repository (no custom `getBySlug` needed)
705
+
706
+ ## v2.8.0 Highlights
698
707
 
699
708
  - **MCP Integration** — expose resources as AI agent tools (stateless by default, service scope, multi-tenancy)
700
709
  - **Reply Helpers** — `reply.ok()`, `reply.fail()`, `reply.paginated()`, `reply.stream()` (opt-in)
@@ -843,9 +843,11 @@ var BaseController = class {
843
843
  status: 400
844
844
  };
845
845
  }
846
+ const deleteMode = req.query?.hard === "true" || req.query?.hard === true || req.body?.mode === "hard" ? "hard" : void 0;
846
847
  const repoDelete = async () => this.repository.delete(repoId, {
847
848
  user,
848
- context: arcContext
849
+ context: arcContext,
850
+ ...deleteMode ? { mode: deleteMode } : {}
849
851
  });
850
852
  let result;
851
853
  if (hooks && this.resourceName) result = await hooks.executeAround(this.resourceName, "delete", existing, repoDelete, {
@@ -854,7 +856,13 @@ var BaseController = class {
854
856
  meta: { id }
855
857
  });
856
858
  else result = await repoDelete();
857
- if (!(typeof result === "object" && result !== null ? result.success : result)) return {
859
+ if (!(() => {
860
+ if (typeof result !== "object" || result === null) return !!result;
861
+ const r = result;
862
+ if (typeof r.success === "boolean") return r.success;
863
+ if (typeof r.deletedCount === "number") return r.deletedCount > 0;
864
+ return true;
865
+ })()) return {
858
866
  success: false,
859
867
  error: "Resource not found",
860
868
  status: 404
@@ -876,16 +884,23 @@ var BaseController = class {
876
884
  };
877
885
  }
878
886
  async getBySlug(req) {
887
+ const slugField = this._presetFields.slugField ?? "slug";
888
+ const slug = req.params[slugField] ?? req.params.slug;
889
+ const options = this.queryResolver.resolve(req, this.meta(req));
879
890
  const repo = this.repository;
880
- if (!repo.getBySlug) return {
891
+ let item = null;
892
+ if (repo.getBySlug) item = await repo.getBySlug(slug, options);
893
+ else if (repo.getOne) {
894
+ const filter = {
895
+ [slugField]: slug,
896
+ ...options?.filter ?? {}
897
+ };
898
+ item = await repo.getOne(filter, options);
899
+ } else return {
881
900
  success: false,
882
- error: "Slug lookup not implemented",
901
+ error: "Slug lookup not implemented — repository needs getBySlug() or getOne()",
883
902
  status: 501
884
903
  };
885
- const slugField = this._presetFields.slugField ?? "slug";
886
- const slug = req.params[slugField] ?? req.params.slug;
887
- const options = this.queryResolver.resolve(req, this.meta(req));
888
- const item = await repo.getBySlug(slug, options);
889
904
  if (!this.accessControl.validateItemAccess(item, req)) return {
890
905
  success: false,
891
906
  error: "Resource not found",
@@ -904,21 +919,25 @@ var BaseController = class {
904
919
  error: "Soft delete not implemented",
905
920
  status: 501
906
921
  };
907
- const options = this.queryResolver.resolve(req, this.meta(req));
908
- const result = await repo.getDeleted(options);
909
- if (Array.isArray(result)) return {
910
- success: true,
911
- data: {
912
- docs: result,
913
- page: 1,
914
- limit: result.length,
915
- total: result.length,
916
- pages: 1,
917
- hasNext: false,
918
- hasPrev: false
919
- },
920
- status: 200
921
- };
922
+ const parsed = this.queryResolver.resolve(req, this.meta(req));
923
+ const result = await repo.getDeleted(parsed, parsed);
924
+ if (Array.isArray(result)) {
925
+ const docs = result;
926
+ return {
927
+ success: true,
928
+ data: {
929
+ method: "offset",
930
+ docs,
931
+ page: 1,
932
+ limit: docs.length,
933
+ total: docs.length,
934
+ pages: 1,
935
+ hasNext: false,
936
+ hasPrev: false
937
+ },
938
+ status: 200
939
+ };
940
+ }
922
941
  return {
923
942
  success: true,
924
943
  data: result,
@@ -950,13 +969,45 @@ var BaseController = class {
950
969
  details: { code: "OWNERSHIP_DENIED" },
951
970
  status: 403
952
971
  };
972
+ const arcContext = this.meta(req);
973
+ const user = req.user;
953
974
  const repoId = this.resolveRepoId(id, existing);
954
- const item = await repo.restore(repoId);
975
+ const hooks = this.getHooks(req);
976
+ if (hooks && this.resourceName) try {
977
+ await hooks.executeBefore(this.resourceName, "restore", existing, {
978
+ user,
979
+ context: arcContext,
980
+ meta: { id }
981
+ });
982
+ } catch (err) {
983
+ return {
984
+ success: false,
985
+ error: "Hook execution failed",
986
+ details: {
987
+ code: "BEFORE_RESTORE_HOOK_ERROR",
988
+ message: err.message
989
+ },
990
+ status: 400
991
+ };
992
+ }
993
+ const repoRestore = () => repo.restore(repoId);
994
+ let item;
995
+ if (hooks && this.resourceName) item = await hooks.executeAround(this.resourceName, "restore", existing, repoRestore, {
996
+ user,
997
+ context: arcContext,
998
+ meta: { id }
999
+ });
1000
+ else item = await repoRestore();
955
1001
  if (!item) return {
956
1002
  success: false,
957
1003
  error: "Resource not found",
958
1004
  status: 404
959
1005
  };
1006
+ if (hooks && this.resourceName) await hooks.executeAfter(this.resourceName, "restore", item, {
1007
+ user,
1008
+ context: arcContext,
1009
+ meta: { id }
1010
+ });
960
1011
  return {
961
1012
  success: true,
962
1013
  data: item,
@@ -1223,7 +1274,7 @@ var BaseController = class {
1223
1274
  };
1224
1275
  return {
1225
1276
  success: true,
1226
- data: await repo.deleteMany(scopedFilter),
1277
+ data: req.query?.hard === "true" || req.query?.hard === true || body.mode === "hard" ? await repo.deleteMany(scopedFilter, { mode: "hard" }) : await repo.deleteMany(scopedFilter),
1227
1278
  status: 200
1228
1279
  };
1229
1280
  }
@@ -61,6 +61,25 @@ interface EventTransport {
61
61
  * Publish an event to the transport
62
62
  */
63
63
  publish(event: DomainEvent): Promise<void>;
64
+ /**
65
+ * Publish a batch of events to the transport (optional, v2.8.1+).
66
+ *
67
+ * Transports that can efficiently batch (Kafka producer, Redis pipeline,
68
+ * RabbitMQ publisher confirms, SQS send-message-batch) should implement
69
+ * this. {@link import('./outbox.js').EventOutbox.relay} auto-detects and
70
+ * uses it for much higher throughput than per-event publishing.
71
+ *
72
+ * **Contract**: the returned `PublishManyResult` must describe the
73
+ * per-event outcome so the caller can acknowledge successes and fail the
74
+ * rest. Partial success is allowed — the transport reports it per event.
75
+ *
76
+ * If not implemented, `EventOutbox.relay` falls back to calling
77
+ * {@link publish} once per event.
78
+ *
79
+ * @param events - Events to publish (in order)
80
+ * @returns Per-event outcome map keyed by `event.meta.id`
81
+ */
82
+ publishMany?(events: readonly DomainEvent[]): Promise<PublishManyResult>;
64
83
  /**
65
84
  * Subscribe to events matching a pattern
66
85
  * @param pattern - Event type pattern (e.g., 'product.*', '*')
@@ -73,6 +92,14 @@ interface EventTransport {
73
92
  */
74
93
  close?(): Promise<void>;
75
94
  }
95
+ /**
96
+ * Per-event outcome returned by {@link EventTransport.publishMany}.
97
+ *
98
+ * The key is `event.meta.id`; the value is `null` for success or an `Error`
99
+ * for per-event failure. Transports MUST include an entry for every event
100
+ * in the input batch.
101
+ */
102
+ type PublishManyResult = ReadonlyMap<string, Error | null>;
76
103
  interface MemoryEventTransportOptions {
77
104
  /** Logger for error/warning messages (default: console) */
78
105
  logger?: EventLogger;
@@ -88,6 +115,15 @@ declare class MemoryEventTransport implements EventTransport {
88
115
  private logger;
89
116
  constructor(options?: MemoryEventTransportOptions);
90
117
  publish(event: DomainEvent): Promise<void>;
118
+ /**
119
+ * Reference `publishMany` implementation — delegates to `publish()` in order.
120
+ *
121
+ * Production transports (Kafka, Redis pipeline, SQS batch) should override
122
+ * this with a single batched network call. Memory transport has nothing to
123
+ * batch, so we just loop — the loop still returns a proper result map so
124
+ * `EventOutbox.relay` can exercise the batched code path in tests.
125
+ */
126
+ publishMany(events: readonly DomainEvent[]): Promise<PublishManyResult>;
91
127
  subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
92
128
  close(): Promise<void>;
93
129
  }
@@ -96,4 +132,4 @@ declare class MemoryEventTransport implements EventTransport {
96
132
  */
97
133
  declare function createEvent<T>(type: string, payload: T, meta?: Partial<DomainEvent["meta"]>): DomainEvent<T>;
98
134
  //#endregion
99
- export { MemoryEventTransport as a, EventTransport as i, EventHandler as n, MemoryEventTransportOptions as o, EventLogger as r, createEvent as s, DomainEvent as t };
135
+ export { MemoryEventTransport as a, createEvent as c, EventTransport as i, EventHandler as n, MemoryEventTransportOptions as o, EventLogger as r, PublishManyResult as s, DomainEvent as t };
@@ -52,6 +52,17 @@ var ResourceRegistry = class {
52
52
  pipelineSteps: extractPipelineSteps(resource.pipe),
53
53
  rateLimit: resource.rateLimit,
54
54
  audit: resource.audit,
55
+ actionPermissions: resource.actionPermissions,
56
+ actions: resource.actions ? Object.entries(resource.actions).map(([name, entry]) => {
57
+ if (typeof entry === "function") return { name };
58
+ return {
59
+ name,
60
+ description: entry.description,
61
+ schema: entry.schema,
62
+ permissions: entry.permissions,
63
+ mcp: entry.mcp
64
+ };
65
+ }) : void 0,
55
66
  plugin: resource.toPlugin()
56
67
  };
57
68
  this._resources.set(resource.name, entry);
@@ -99,11 +110,12 @@ var ResourceRegistry = class {
99
110
  byModule: this._groupBy(resources, "module"),
100
111
  presetUsage: presetCounts,
101
112
  totalRoutes: resources.reduce((sum, r) => {
102
- if (r.disableDefaultRoutes) return sum + (r.additionalRoutes?.length ?? 0);
113
+ const actionsCount = (r.actions?.length ?? 0) > 0 ? 1 : 0;
114
+ if (r.disableDefaultRoutes) return sum + (r.additionalRoutes?.length ?? 0) + actionsCount;
103
115
  const disabledSet = new Set(r.disabledRoutes ?? []);
104
116
  let defaultCount = CRUD_OPERATIONS.filter((route) => !disabledSet.has(route)).length;
105
117
  if (!disabledSet.has("update") && r.updateMethod === "both") defaultCount += 1;
106
- return sum + defaultCount + (r.additionalRoutes?.length ?? 0);
118
+ return sum + defaultCount + (r.additionalRoutes?.length ?? 0) + actionsCount;
107
119
  }, 0),
108
120
  totalEvents: resources.reduce((sum, r) => sum + (r.events?.length ?? 0), 0)
109
121
  };
@@ -1,3 +1,3 @@
1
- import { a as RelationMetadata, c as ValidationResult, i as FieldMetadata, o as RepositoryLike, r as DataAdapter, s as SchemaMetadata, t as AdapterFactory } from "../interface-B91alUzq.mjs";
2
- import { a as PrismaQueryParserOptions, c as MongooseAdapterOptions, i as PrismaQueryParser, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, r as PrismaQueryOptions, s as MongooseAdapter, t as PrismaAdapter } from "../index-C9eYNjGR.mjs";
1
+ import { a as RelationMetadata, c as ValidationResult, i as FieldMetadata, o as RepositoryLike, r as DataAdapter, s as SchemaMetadata, t as AdapterFactory } from "../interface-CS6d7HiB.mjs";
2
+ import { a as PrismaQueryParserOptions, c as MongooseAdapterOptions, i as PrismaQueryParser, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, r as PrismaQueryOptions, s as MongooseAdapter, t as PrismaAdapter } from "../index-0zj73o2U.mjs";
3
3
  export { AdapterFactory, DataAdapter, FieldMetadata, MongooseAdapter, MongooseAdapterOptions, PrismaAdapter, PrismaAdapterOptions, PrismaQueryOptions, PrismaQueryParser, PrismaQueryParserOptions, RelationMetadata, RepositoryLike, SchemaMetadata, ValidationResult, createMongooseAdapter, createPrismaAdapter };
@@ -1,2 +1,2 @@
1
- import { a as createMongooseAdapter, i as MongooseAdapter, n as PrismaQueryParser, r as createPrismaAdapter, t as PrismaAdapter } from "../adapters-BxGgSHjj.mjs";
1
+ import { a as createMongooseAdapter, i as MongooseAdapter, n as PrismaQueryParser, r as createPrismaAdapter, t as PrismaAdapter } from "../adapters-BBqAVvPK.mjs";
2
2
  export { MongooseAdapter, PrismaAdapter, PrismaQueryParser, createMongooseAdapter, createPrismaAdapter };
@@ -90,6 +90,17 @@ var MongooseAdapter = class {
90
90
  if (blockedFields.has(fieldName)) continue;
91
91
  const typeInfo = schemaType;
92
92
  properties[fieldName] = this.mongooseTypeToOpenApi(typeInfo);
93
+ const rule = fieldRules[fieldName];
94
+ if (rule) {
95
+ const prop = properties[fieldName];
96
+ if (rule.minLength != null && prop.minLength == null) prop.minLength = rule.minLength;
97
+ if (rule.maxLength != null && prop.maxLength == null) prop.maxLength = rule.maxLength;
98
+ if (rule.min != null && prop.minimum == null) prop.minimum = rule.min;
99
+ if (rule.max != null && prop.maximum == null) prop.maximum = rule.max;
100
+ if (rule.pattern != null && prop.pattern == null) prop.pattern = rule.pattern;
101
+ if (rule.enum != null && prop.enum == null) prop.enum = rule.enum;
102
+ if (rule.description != null && prop.description == null) prop.description = rule.description;
103
+ }
93
104
  if (typeInfo.isRequired && !optionalSet.has(fieldName) && !fieldRules[fieldName]?.optional) required.push(fieldName);
94
105
  }
95
106
  const readonlyForInput = new Set([...readonlySet]);
@@ -1,4 +1,4 @@
1
- import { a as AuditContext, c as AuditStore, i as AuditAction, l as AuditStoreOptions, n as MongoAuditStoreOptions, o as AuditEntry, r as MongoConnection, s as AuditQueryOptions, u as createAuditEntry } from "../mongodb-Cgu9F1Nd.mjs";
1
+ import { a as AuditContext, c as AuditStore, i as AuditAction, l as AuditStoreOptions, n as MongoAuditStoreOptions, o as AuditEntry, r as MongoConnection, s as AuditQueryOptions, u as createAuditEntry } from "../mongodb-B1eVtFhw.mjs";
2
2
  import { FastifyPluginAsync } from "fastify";
3
3
 
4
4
  //#region src/audit/auditPlugin.d.ts
@@ -1,4 +1,4 @@
1
- import { t as MongoAuditStore } from "../mongodb-B7X7P1P8.mjs";
1
+ import { t as MongoAuditStore } from "../mongodb-5Ff3w8jy.mjs";
2
2
  import fp from "fastify-plugin";
3
3
  //#region src/audit/stores/interface.ts
4
4
  /**
@@ -1,2 +1,2 @@
1
- import { n as MongoAuditStoreOptions, t as MongoAuditStore } from "../mongodb-Cgu9F1Nd.mjs";
1
+ import { n as MongoAuditStoreOptions, t as MongoAuditStore } from "../mongodb-B1eVtFhw.mjs";
2
2
  export { MongoAuditStore, type MongoAuditStoreOptions };
@@ -1,2 +1,2 @@
1
- import { t as MongoAuditStore } from "../mongodb-B7X7P1P8.mjs";
1
+ import { t as MongoAuditStore } from "../mongodb-5Ff3w8jy.mjs";
2
2
  export { MongoAuditStore };
@@ -1,7 +1,7 @@
1
- import { g as AuthPluginOptions, h as AuthHelpers } from "../interface-B91alUzq.mjs";
2
- import { t as PermissionCheck } from "../types-B4BNthET.mjs";
3
- import { t as ExternalOpenApiPaths } from "../externalPaths-iba7jD3d.mjs";
4
- import { a as SessionManagerOptions, c as createSessionManager, i as SessionData, n as MemorySessionStoreOptions, o as SessionManagerResult, r as SessionCookieOptions, s as SessionStore, t as MemorySessionStore } from "../sessionManager-CEo9jwPI.mjs";
1
+ import { b as AuthPluginOptions, y as AuthHelpers } from "../interface-CS6d7HiB.mjs";
2
+ import { t as PermissionCheck } from "../types-BoaZHr-2.mjs";
3
+ import { t as ExternalOpenApiPaths } from "../externalPaths-BQ8QijNH.mjs";
4
+ import { a as SessionManagerOptions, c as createSessionManager, i as SessionData, n as MemorySessionStoreOptions, o as SessionManagerResult, r as SessionCookieOptions, s as SessionStore, t as MemorySessionStore } from "../sessionManager-BkzVU8h2.mjs";
5
5
  import { FastifyPluginAsync, FastifyReply as FastifyReply$1, FastifyRequest as FastifyRequest$1 } from "fastify";
6
6
 
7
7
  //#region src/auth/authPlugin.d.ts
@@ -1,7 +1,7 @@
1
1
  import { n as normalizeRoles, t as getUserRoles } from "../types-ZUu_h0jp.mjs";
2
- import { t as ArcError } from "../errors-Cg58SLNi.mjs";
2
+ import { t as ArcError } from "../errors-BF2bIOIS.mjs";
3
3
  import { h as requireTeamMembership, l as requireOrgMembership, u as requireOrgRole } from "../permissions-CH4cNwJi.mjs";
4
- import { n as extractBetterAuthOpenApi } from "../betterAuthOpenApi-EkPaMWNM.mjs";
4
+ import { n as extractBetterAuthOpenApi } from "../betterAuthOpenApi-C5lDyRH2.mjs";
5
5
  import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
6
6
  import fp from "fastify-plugin";
7
7
  //#region src/auth/authPlugin.ts
@@ -677,7 +677,7 @@ function createBetterAuthAdapter(options) {
677
677
  if (!fastify.hasDecorator("authenticate")) fastify.decorate("authenticate", authenticate);
678
678
  if (!fastify.hasDecorator("optionalAuthenticate")) fastify.decorate("optionalAuthenticate", optionalAuthenticate);
679
679
  if (!extractedOpenApi && openapiOpt !== false && auth.api && typeof auth.api === "object") {
680
- const { extractBetterAuthOpenApi } = await import("../betterAuthOpenApi-EkPaMWNM.mjs").then((n) => n.t);
680
+ const { extractBetterAuthOpenApi } = await import("../betterAuthOpenApi-C5lDyRH2.mjs").then((n) => n.t);
681
681
  extractedOpenApi = extractBetterAuthOpenApi(auth.api, {
682
682
  basePath,
683
683
  userFields
@@ -1,4 +1,4 @@
1
- import { i as SessionData, s as SessionStore } from "../sessionManager-CEo9jwPI.mjs";
1
+ import { i as SessionData, s as SessionStore } from "../sessionManager-BkzVU8h2.mjs";
2
2
 
3
3
  //#region src/auth/redis-session.d.ts
4
4
  /** Minimal Redis client interface — compatible with ioredis */
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
- import { a as toJsonSchema } from "./schemaConverter-Y5EejTnJ.mjs";
2
+ import { a as toJsonSchema } from "./schemaConverter-OxfCshus.mjs";
3
3
  //#region src/auth/betterAuthOpenApi.ts
4
4
  var betterAuthOpenApi_exports = /* @__PURE__ */ __exportAll({ extractBetterAuthOpenApi: () => extractBetterAuthOpenApi });
5
5
  /**
@@ -1,5 +1,5 @@
1
- import { i as CacheStore, n as CacheSetOptions, r as CacheStats, t as CacheLogger } from "../interface-CG7oRZjX.mjs";
2
- import { a as CacheEnvelope, c as QueryCache, i as queryCachePlugin, l as QueryCacheConfig, n as QueryCacheDefaults, o as CacheResult, r as QueryCachePluginOptions, s as CacheStatus, t as CrossResourceRule } from "../queryCachePlugin-Ckl71mkc.mjs";
1
+ import { i as CacheStore, n as CacheSetOptions, r as CacheStats, t as CacheLogger } from "../interface-bpoLKKqx.mjs";
2
+ import { a as CacheEnvelope, c as QueryCache, i as queryCachePlugin, l as QueryCacheConfig, n as QueryCacheDefaults, o as CacheResult, r as QueryCachePluginOptions, s as CacheStatus, t as CrossResourceRule } from "../queryCachePlugin-BCFVXnxK.mjs";
3
3
 
4
4
  //#region src/cache/keys.d.ts
5
5
  /**
@@ -1,6 +1,6 @@
1
1
  import { i as versionKey, n as hashParams, r as tagVersionKey, t as buildQueryKey } from "../keys-qcD-TVJl.mjs";
2
2
  import { t as MemoryCacheStore } from "../memory-Cp7_cAko.mjs";
3
- import { r as QueryCache, t as queryCachePlugin } from "../queryCachePlugin-CwTpR04-.mjs";
3
+ import { r as QueryCache, t as queryCachePlugin } from "../queryCachePlugin-D0iIVhW_.mjs";
4
4
  //#region src/cache/redis.ts
5
5
  /**
6
6
  * Redis-backed cache store.
@@ -1,5 +1,5 @@
1
- import { t as ResourceRegistry } from "../../ResourceRegistry-DsHiG9cL.mjs";
2
- import { t as buildOpenApiSpec } from "../../openapi-D7Z7VODz.mjs";
1
+ import { t as ResourceRegistry } from "../../ResourceRegistry-Dtcojmu8.mjs";
2
+ import { t as buildOpenApiSpec } from "../../openapi-q6rNKfZy.mjs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { mkdirSync, writeFileSync } from "node:fs";
@@ -1,4 +1,4 @@
1
- import { t as pluralize } from "../../pluralize-Dckfq6US.mjs";
1
+ import { t as pluralize } from "../../pluralize-BneOJkpi.mjs";
2
2
  import { join } from "node:path";
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  //#region src/cli/commands/generate.ts
@@ -1,4 +1,4 @@
1
- import { t as ResourceRegistry } from "../../ResourceRegistry-DsHiG9cL.mjs";
1
+ import { t as ResourceRegistry } from "../../ResourceRegistry-Dtcojmu8.mjs";
2
2
  import { resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  //#region src/cli/commands/introspect.ts
@@ -1,3 +1,3 @@
1
- import { At as AccessControl, Dt as QueryResolverConfig, Et as QueryResolver, Ht as defineResource, Ot as BodySanitizer, Tt as BaseControllerOptions, Vt as ResourceDefinition, jt as AccessControlConfig, kt as BodySanitizerConfig, wt as BaseController } from "../interface-B91alUzq.mjs";
2
- import { A as MutationOperation, C as HOOK_PHASES, D as MAX_REGEX_LENGTH, E as MAX_FILTER_DEPTH, M as SYSTEM_FIELDS, O as MAX_SEARCH_LENGTH, S as HOOK_OPERATIONS, T as HookPhase, _ as DEFAULT_LIMIT, a as getControllerScope, b as DEFAULT_TENANT_FIELD, c as createCrudRouter, d as ActionRouterConfig, f as IdempotencyService, g as DEFAULT_ID_FIELD, h as CrudOperation, i as getControllerContext, j as RESERVED_QUERY_PARAMS, k as MUTATION_OPERATIONS, l as createPermissionMiddleware, m as CRUD_OPERATIONS, n as createFastifyHandler, o as sendControllerResponse, p as createActionRouter, r as createRequestContext, s as defineResourceVariants, t as createCrudHandlers, u as ActionHandler, v as DEFAULT_MAX_LIMIT, w as HookOperation, x as DEFAULT_UPDATE_METHOD, y as DEFAULT_SORT } from "../index-BjShrzoj.mjs";
1
+ import { At as BaseController, Ft as BodySanitizerConfig, It as AccessControl, Jt as defineResource, Lt as AccessControlConfig, Mt as QueryResolver, Nt as QueryResolverConfig, Pt as BodySanitizer, jt as BaseControllerOptions, qt as ResourceDefinition } from "../interface-CS6d7HiB.mjs";
2
+ import { A as MutationOperation, C as HOOK_PHASES, D as MAX_REGEX_LENGTH, E as MAX_FILTER_DEPTH, M as SYSTEM_FIELDS, O as MAX_SEARCH_LENGTH, S as HOOK_OPERATIONS, T as HookPhase, _ as DEFAULT_LIMIT, a as getControllerScope, b as DEFAULT_TENANT_FIELD, c as createCrudRouter, d as ActionRouterConfig, f as IdempotencyService, g as DEFAULT_ID_FIELD, h as CrudOperation, i as getControllerContext, j as RESERVED_QUERY_PARAMS, k as MUTATION_OPERATIONS, l as createPermissionMiddleware, m as CRUD_OPERATIONS, n as createFastifyHandler, o as sendControllerResponse, p as createActionRouter, r as createRequestContext, s as defineResourceVariants, t as createCrudHandlers, u as ActionHandler, v as DEFAULT_MAX_LIMIT, w as HookOperation, x as DEFAULT_UPDATE_METHOD, y as DEFAULT_SORT } from "../index-DadoLP51.mjs";
3
3
  export { AccessControl, AccessControlConfig, ActionHandler, ActionRouterConfig, BaseController, BaseControllerOptions, BodySanitizer, BodySanitizerConfig, CRUD_OPERATIONS, CrudOperation, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, HookOperation, HookPhase, IdempotencyService, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MutationOperation, QueryResolver, QueryResolverConfig, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, createActionRouter, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineResource, defineResourceVariants, getControllerContext, getControllerScope, sendControllerResponse };
@@ -1,5 +1,6 @@
1
1
  import { a as DEFAULT_SORT, c as HOOK_OPERATIONS, d as MAX_REGEX_LENGTH, f as MAX_SEARCH_LENGTH, h as SYSTEM_FIELDS, i as DEFAULT_MAX_LIMIT, l as HOOK_PHASES, m as RESERVED_QUERY_PARAMS, n as DEFAULT_ID_FIELD, o as DEFAULT_TENANT_FIELD, p as MUTATION_OPERATIONS, r as DEFAULT_LIMIT, s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS, u as MAX_FILTER_DEPTH } from "../constants-Cxde4rpC.mjs";
2
- import { i as AccessControl, n as QueryResolver, r as BodySanitizer, t as BaseController } from "../BaseController-CpMfCXdn.mjs";
3
- import { n as createActionRouter, t as defineResourceVariants } from "../core-B_zEeA2b.mjs";
4
- import { c as createCrudHandlers, d as getControllerContext, f as getControllerScope, l as createFastifyHandler, n as defineResource, o as createCrudRouter, p as sendControllerResponse, s as createPermissionMiddleware, t as ResourceDefinition, u as createRequestContext } from "../defineResource-BW2dMCu9.mjs";
2
+ import { i as AccessControl, n as QueryResolver, r as BodySanitizer, t as BaseController } from "../BaseController-DAGGc5Xn.mjs";
3
+ import { n as createActionRouter } from "../createActionRouter-Df1BuawX.mjs";
4
+ import { c as createCrudHandlers, d as getControllerContext, f as getControllerScope, l as createFastifyHandler, n as defineResource, o as createCrudRouter, p as sendControllerResponse, s as createPermissionMiddleware, t as ResourceDefinition, u as createRequestContext } from "../defineResource-CqeUltrW.mjs";
5
+ import { t as defineResourceVariants } from "../core-CrLDuqoT.mjs";
5
6
  export { AccessControl, BaseController, BodySanitizer, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, QueryResolver, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, createActionRouter, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineResource, defineResourceVariants, getControllerContext, getControllerScope, sendControllerResponse };
@@ -0,0 +1,34 @@
1
+ import { n as defineResource } from "./defineResource-CqeUltrW.mjs";
2
+ //#region src/core/defineResourceVariants.ts
3
+ /**
4
+ * Define multiple resources from a shared base config and per-variant overrides.
5
+ *
6
+ * Each variant is independently passed through `defineResource()` — the
7
+ * returned `ResourceDefinition`s are real, fully-registered resources.
8
+ * Register each one's plugin in your app:
9
+ *
10
+ * ```typescript
11
+ * await app.register(articlePublic.toPlugin());
12
+ * await app.register(articleAdmin.toPlugin());
13
+ * ```
14
+ *
15
+ * @param base Shared config — adapter, queryParser, schemaOptions, hooks, etc.
16
+ * Must NOT include `name` or `prefix` (those are per-variant).
17
+ * @param variants Map of variant key → override. Each variant must declare
18
+ * its own `name` and `prefix`. Other fields override the base.
19
+ * @returns A record where each key from `variants` maps to a real
20
+ * `ResourceDefinition` ready for `.toPlugin()` registration.
21
+ */
22
+ function defineResourceVariants(base, variants) {
23
+ const out = {};
24
+ for (const key of Object.keys(variants)) {
25
+ const override = variants[key];
26
+ out[key] = defineResource({
27
+ ...base,
28
+ ...override
29
+ });
30
+ }
31
+ return out;
32
+ }
33
+ //#endregion
34
+ export { defineResourceVariants as t };