@classytic/arc 2.6.2 → 2.6.3

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 (38) hide show
  1. package/README.md +11 -0
  2. package/dist/{BaseController-AbbRx3e0.mjs → BaseController-DzRtluEF.mjs} +88 -8
  3. package/dist/adapters/index.d.mts +2 -2
  4. package/dist/adapters/index.mjs +1 -1
  5. package/dist/{adapters-CTn28N4y.mjs → adapters-gM-WYjNe.mjs} +6 -4
  6. package/dist/auth/index.d.mts +1 -1
  7. package/dist/core/index.d.mts +2 -2
  8. package/dist/core/index.mjs +2 -2
  9. package/dist/{defineResource-Ckxg6HrZ.mjs → defineResource-wWMBB4GP.mjs} +39 -27
  10. package/dist/docs/index.d.mts +1 -1
  11. package/dist/dynamic/index.d.mts +1 -1
  12. package/dist/dynamic/index.mjs +1 -1
  13. package/dist/factory/index.d.mts +1 -1
  14. package/dist/hooks/index.d.mts +1 -1
  15. package/dist/{index-DrCqa3Jq.d.mts → index-CHeJa4Zd.d.mts} +3 -3
  16. package/dist/{index-B4uZm82R.d.mts → index-gz6iuzCp.d.mts} +1 -1
  17. package/dist/index.d.mts +3 -3
  18. package/dist/index.mjs +4 -4
  19. package/dist/integrations/index.d.mts +1 -1
  20. package/dist/integrations/mcp/index.d.mts +2 -2
  21. package/dist/integrations/mcp/index.mjs +1 -1
  22. package/dist/integrations/mcp/testing.d.mts +1 -1
  23. package/dist/integrations/mcp/testing.mjs +1 -1
  24. package/dist/{interface-CrN45qz1.d.mts → interface-DYH8AXGe.d.mts} +54 -4
  25. package/dist/org/index.d.mts +1 -1
  26. package/dist/plugins/index.d.mts +1 -1
  27. package/dist/plugins/tracing-entry.mjs +1 -1
  28. package/dist/presets/index.d.mts +1 -1
  29. package/dist/presets/multiTenant.d.mts +1 -1
  30. package/dist/registry/index.d.mts +1 -1
  31. package/dist/{resourceToTools-DH3c3e-T.mjs → resourceToTools-nCJWnG1r.mjs} +250 -13
  32. package/dist/testing/index.d.mts +2 -2
  33. package/dist/types/index.d.mts +1 -1
  34. package/dist/{types-DurlBP2N.d.mts → types-B4_TDdPe.d.mts} +1 -1
  35. package/dist/{types-C1Z28coa.d.mts → types-By-5mIfn.d.mts} +1 -1
  36. package/dist/utils/index.d.mts +1 -1
  37. package/package.json +18 -18
  38. package/skills/arc/SKILL.md +25 -0
package/README.md CHANGED
@@ -108,6 +108,17 @@ const productResource = defineResource({
108
108
  // Plus preset routes: GET /deleted, POST /:id/restore, GET /slug/:slug
109
109
  ```
110
110
 
111
+ **Custom primary key?** Use `idField` for resources keyed by UUIDs, slugs, or business identifiers:
112
+
113
+ ```typescript
114
+ defineResource({
115
+ name: 'job',
116
+ adapter: createMongooseAdapter(JobModel, jobRepository),
117
+ idField: 'jobId', // routes + BaseController lookups + OpenAPI + MCP tools all use this
118
+ });
119
+ // GET /jobs/job-5219f346-a4d → 200 (no ObjectId pattern enforcement)
120
+ ```
121
+
111
122
  ## Authentication
112
123
 
113
124
  Auth uses a discriminated union — pick a `type`:
@@ -96,9 +96,12 @@ var AccessControl = class AccessControl {
96
96
  */
97
97
  async fetchWithAccessControl(id, req, repository, queryOptions) {
98
98
  const compoundFilter = this.buildIdFilter(id, req);
99
- const hasCompoundFilters = Object.keys(compoundFilter).length > 1;
99
+ const needsCompoundLookup = Object.keys(compoundFilter).length > 1 || this.idField !== "_id";
100
100
  try {
101
- if (hasCompoundFilters && typeof repository.getOne === "function") return await repository.getOne(compoundFilter, queryOptions);
101
+ if (needsCompoundLookup && typeof repository.getOne === "function") return await repository.getOne(compoundFilter, queryOptions);
102
+ if (this.idField !== "_id") {
103
+ if (typeof repository.getOne !== "function") throw new Error(`Resource with idField="${this.idField}" requires repository.getOne() to look up by custom field. Arc's BaseController cannot fall back to getById() because it would query by _id.`);
104
+ }
102
105
  const item = await repository.getById(id, queryOptions);
103
106
  if (!item) return null;
104
107
  const arcContext = this._meta(req);
@@ -719,6 +722,7 @@ var BaseController = class {
719
722
  details: { code: "OWNERSHIP_DENIED" },
720
723
  status: 403
721
724
  };
725
+ const repoId = this.idField !== "_id" && existing ? String(existing["_id"] ?? id) : id;
722
726
  const hooks = this.getHooks(req);
723
727
  let processedData = data;
724
728
  if (hooks && this.resourceName) try {
@@ -741,7 +745,7 @@ var BaseController = class {
741
745
  status: 400
742
746
  };
743
747
  }
744
- const repoUpdate = async () => this.repository.update(id, processedData, {
748
+ const repoUpdate = async () => this.repository.update(repoId, processedData, {
745
749
  user,
746
750
  context: arcContext
747
751
  });
@@ -797,6 +801,7 @@ var BaseController = class {
797
801
  details: { code: "OWNERSHIP_DENIED" },
798
802
  status: 403
799
803
  };
804
+ const repoId = this.idField !== "_id" && existing ? String(existing["_id"] ?? id) : id;
800
805
  const hooks = this.getHooks(req);
801
806
  if (hooks && this.resourceName) try {
802
807
  await hooks.executeBefore(this.resourceName, "delete", existing, {
@@ -815,7 +820,7 @@ var BaseController = class {
815
820
  status: 400
816
821
  };
817
822
  }
818
- const repoDelete = async () => this.repository.delete(id, {
823
+ const repoDelete = async () => this.repository.delete(repoId, {
819
824
  user,
820
825
  context: arcContext
821
826
  });
@@ -922,7 +927,8 @@ var BaseController = class {
922
927
  details: { code: "OWNERSHIP_DENIED" },
923
928
  status: 403
924
929
  };
925
- const item = await repo.restore(id);
930
+ const repoId = this.idField !== "_id" && existing ? String(existing["_id"] ?? id) : id;
931
+ const item = await repo.restore(repoId);
926
932
  if (!item) return {
927
933
  success: false,
928
934
  error: "Resource not found",
@@ -978,7 +984,33 @@ var BaseController = class {
978
984
  error: "Bulk create requires a non-empty items array",
979
985
  status: 400
980
986
  };
981
- const created = await repo.createMany(items);
987
+ let scopedItems = items;
988
+ if (this.tenantField) {
989
+ const scope = this.meta(req)?._scope;
990
+ if (scope) {
991
+ if (scope.kind === "public") return {
992
+ success: false,
993
+ error: "Organization context required to bulk-create resources",
994
+ details: { code: "ORG_CONTEXT_REQUIRED" },
995
+ status: 403
996
+ };
997
+ if (!isElevated(scope)) {
998
+ const orgId = getOrgId(scope);
999
+ if (!orgId) return {
1000
+ success: false,
1001
+ error: "Organization context required to bulk-create resources",
1002
+ details: { code: "ORG_CONTEXT_REQUIRED" },
1003
+ status: 403
1004
+ };
1005
+ const tenantField = this.tenantField;
1006
+ scopedItems = items.map((item) => ({
1007
+ ...item,
1008
+ [tenantField]: orgId
1009
+ }));
1010
+ }
1011
+ }
1012
+ }
1013
+ const created = await repo.createMany(scopedItems);
982
1014
  return {
983
1015
  success: true,
984
1016
  data: created,
@@ -986,6 +1018,40 @@ var BaseController = class {
986
1018
  meta: { count: created.length }
987
1019
  };
988
1020
  }
1021
+ /**
1022
+ * Build a tenant-scoped filter for bulk update/delete.
1023
+ *
1024
+ * Mirrors `AccessControl.buildIdFilter` semantics for single-doc ops:
1025
+ * - Always merge `_policyFilters` (from permission middleware)
1026
+ * - When `tenantField` is set AND a `member` scope is present, add the
1027
+ * org filter so cross-tenant data can't be touched.
1028
+ * - When the scope is `elevated` (platform admin), no org filter is
1029
+ * applied — admins can bulk-update across orgs intentionally.
1030
+ * - When the scope is `public` on a tenant-scoped resource, deny.
1031
+ * - When NO scope is present at all (e.g., direct controller calls in
1032
+ * unit tests, or app routes without auth middleware), the controller
1033
+ * stays lenient — it's the middleware layer's job to fail-close.
1034
+ * Apps that want fail-close on bulk routes should run the multi-tenant
1035
+ * preset middleware (or equivalent) ahead of these handlers.
1036
+ *
1037
+ * Returns the merged filter, or `null` when access must be denied.
1038
+ */
1039
+ buildBulkFilter(userFilter, req) {
1040
+ const filter = { ...userFilter };
1041
+ const arcContext = this.meta(req);
1042
+ const policyFilters = arcContext?._policyFilters;
1043
+ if (policyFilters) Object.assign(filter, policyFilters);
1044
+ if (this.tenantField) {
1045
+ const scope = arcContext?._scope;
1046
+ if (!scope) return filter;
1047
+ if (scope.kind === "public") return null;
1048
+ if (isElevated(scope)) return filter;
1049
+ const orgId = getOrgId(scope);
1050
+ if (!orgId) return null;
1051
+ filter[this.tenantField] = orgId;
1052
+ }
1053
+ return filter;
1054
+ }
989
1055
  async bulkUpdate(req) {
990
1056
  const repo = this.repository;
991
1057
  if (!repo.updateMany) return {
@@ -1004,9 +1070,16 @@ var BaseController = class {
1004
1070
  error: "Bulk update requires non-empty data",
1005
1071
  status: 400
1006
1072
  };
1073
+ const scopedFilter = this.buildBulkFilter(body.filter, req);
1074
+ if (scopedFilter === null) return {
1075
+ success: false,
1076
+ error: "Organization context required for bulk update",
1077
+ details: { code: "ORG_CONTEXT_REQUIRED" },
1078
+ status: 403
1079
+ };
1007
1080
  return {
1008
1081
  success: true,
1009
- data: await repo.updateMany(body.filter, body.data),
1082
+ data: await repo.updateMany(scopedFilter, body.data),
1010
1083
  status: 200
1011
1084
  };
1012
1085
  }
@@ -1023,9 +1096,16 @@ var BaseController = class {
1023
1096
  error: "Bulk delete requires a non-empty filter",
1024
1097
  status: 400
1025
1098
  };
1099
+ const scopedFilter = this.buildBulkFilter(body.filter, req);
1100
+ if (scopedFilter === null) return {
1101
+ success: false,
1102
+ error: "Organization context required for bulk delete",
1103
+ details: { code: "ORG_CONTEXT_REQUIRED" },
1104
+ status: 403
1105
+ };
1026
1106
  return {
1027
1107
  success: true,
1028
- data: await repo.deleteMany(body.filter),
1108
+ data: await repo.deleteMany(scopedFilter),
1029
1109
  status: 200
1030
1110
  };
1031
1111
  }
@@ -1,3 +1,3 @@
1
- import { a as RepositoryLike, i as RelationMetadata, n as DataAdapter, o as SchemaMetadata, r as FieldMetadata, s as ValidationResult, t as AdapterFactory } from "../interface-CrN45qz1.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-DrCqa3Jq.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-DYH8AXGe.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-CHeJa4Zd.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-CTn28N4y.mjs";
1
+ import { a as createMongooseAdapter, i as MongooseAdapter, n as PrismaQueryParser, r as createPrismaAdapter, t as PrismaAdapter } from "../adapters-gM-WYjNe.mjs";
2
2
  export { MongooseAdapter, PrismaAdapter, PrismaQueryParser, createMongooseAdapter, createPrismaAdapter };
@@ -71,9 +71,9 @@ var MongooseAdapter = class {
71
71
  * If a `schemaGenerator` plugin was provided (e.g. MongoKit's buildCrudSchemasFromModel),
72
72
  * it is used instead of the built-in basic conversion.
73
73
  */
74
- generateSchemas(schemaOptions) {
74
+ generateSchemas(schemaOptions, context) {
75
75
  try {
76
- if (this.schemaGenerator) return this.schemaGenerator(this.model, schemaOptions);
76
+ if (this.schemaGenerator) return this.schemaGenerator(this.model, schemaOptions, context);
77
77
  const paths = this.model.schema.paths;
78
78
  const properties = {};
79
79
  const required = [];
@@ -104,11 +104,13 @@ var MongooseAdapter = class {
104
104
  createBody: {
105
105
  type: "object",
106
106
  properties: inputProperties,
107
- required: inputRequired.length > 0 ? inputRequired : void 0
107
+ required: inputRequired.length > 0 ? inputRequired : void 0,
108
+ additionalProperties: true
108
109
  },
109
110
  updateBody: {
110
111
  type: "object",
111
- properties: updateProperties
112
+ properties: updateProperties,
113
+ additionalProperties: true
112
114
  },
113
115
  response: {
114
116
  type: "object",
@@ -1,4 +1,4 @@
1
- import { h as AuthPluginOptions, m as AuthHelpers } from "../interface-CrN45qz1.mjs";
1
+ import { g as AuthPluginOptions, h as AuthHelpers } from "../interface-DYH8AXGe.mjs";
2
2
  import { t as PermissionCheck } from "../types-BNUccdcf.mjs";
3
3
  import { t as ExternalOpenApiPaths } from "../externalPaths-DpO-s7r8.mjs";
4
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-wbkYj2HL.mjs";
@@ -1,3 +1,3 @@
1
- import { At as AccessControlConfig, Bt as ResourceDefinition, Ct as BaseController, Dt as BodySanitizer, Et as QueryResolverConfig, Ot as BodySanitizerConfig, Tt as QueryResolver, Vt as defineResource, kt as AccessControl, wt as BaseControllerOptions } from "../interface-CrN45qz1.mjs";
2
- import { A as RESERVED_QUERY_PARAMS, C as HookOperation, D as MAX_SEARCH_LENGTH, E as MAX_REGEX_LENGTH, O as MUTATION_OPERATIONS, S as HOOK_PHASES, T as MAX_FILTER_DEPTH, _ as DEFAULT_MAX_LIMIT, a as getControllerScope, b as DEFAULT_UPDATE_METHOD, c as createPermissionMiddleware, d as IdempotencyService, f as createActionRouter, g as DEFAULT_LIMIT, h as DEFAULT_ID_FIELD, i as getControllerContext, j as SYSTEM_FIELDS, k as MutationOperation, l as ActionHandler, m as CrudOperation, n as createFastifyHandler, o as sendControllerResponse, p as CRUD_OPERATIONS, r as createRequestContext, s as createCrudRouter, t as createCrudHandlers, u as ActionRouterConfig, v as DEFAULT_SORT, w as HookPhase, x as HOOK_OPERATIONS, y as DEFAULT_TENANT_FIELD } from "../index-B4uZm82R.mjs";
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-DYH8AXGe.mjs";
2
+ import { A as RESERVED_QUERY_PARAMS, C as HookOperation, D as MAX_SEARCH_LENGTH, E as MAX_REGEX_LENGTH, O as MUTATION_OPERATIONS, S as HOOK_PHASES, T as MAX_FILTER_DEPTH, _ as DEFAULT_MAX_LIMIT, a as getControllerScope, b as DEFAULT_UPDATE_METHOD, c as createPermissionMiddleware, d as IdempotencyService, f as createActionRouter, g as DEFAULT_LIMIT, h as DEFAULT_ID_FIELD, i as getControllerContext, j as SYSTEM_FIELDS, k as MutationOperation, l as ActionHandler, m as CrudOperation, n as createFastifyHandler, o as sendControllerResponse, p as CRUD_OPERATIONS, r as createRequestContext, s as createCrudRouter, t as createCrudHandlers, u as ActionRouterConfig, v as DEFAULT_SORT, w as HookPhase, x as HOOK_OPERATIONS, y as DEFAULT_TENANT_FIELD } from "../index-gz6iuzCp.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, getControllerContext, getControllerScope, sendControllerResponse };
@@ -1,5 +1,5 @@
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-AbbRx3e0.mjs";
2
+ import { i as AccessControl, n as QueryResolver, r as BodySanitizer, t as BaseController } from "../BaseController-DzRtluEF.mjs";
3
3
  import { t as createActionRouter } from "../core-C1XCMtqM.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-Ckxg6HrZ.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-wWMBB4GP.mjs";
5
5
  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, getControllerContext, getControllerScope, sendControllerResponse };
@@ -1,6 +1,6 @@
1
1
  import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-Cxde4rpC.mjs";
2
2
  import { d as isElevated, f as isMember, n as PUBLIC_SCOPE } from "./types-BhtYdxZU.mjs";
3
- import { t as BaseController } from "./BaseController-AbbRx3e0.mjs";
3
+ import { t as BaseController } from "./BaseController-DzRtluEF.mjs";
4
4
  import { i as resolveEffectiveRoles, t as applyFieldReadPermissions } from "./fields-ipsbIRPK.mjs";
5
5
  import { t as getUserRoles } from "./types-ZUu_h0jp.mjs";
6
6
  import { t as requestContext } from "./requestContext-DYtmNpm5.mjs";
@@ -1015,9 +1015,38 @@ function defineResource(config) {
1015
1015
  if (!config.skipRegistry) try {
1016
1016
  let openApiSchemas;
1017
1017
  if (config.adapter?.generateSchemas) {
1018
- const generated = config.adapter.generateSchemas(config.schemaOptions);
1018
+ const adapterContext = {
1019
+ idField: config.idField,
1020
+ resourceName: config.name
1021
+ };
1022
+ const generated = config.adapter.generateSchemas(config.schemaOptions, adapterContext);
1019
1023
  if (generated) openApiSchemas = generated;
1020
1024
  }
1025
+ if (config.idField && config.idField !== "_id" && openApiSchemas?.params && typeof openApiSchemas.params === "object") {
1026
+ const params = openApiSchemas.params;
1027
+ const properties = params.properties;
1028
+ const idProp = properties?.id;
1029
+ if (idProp && typeof idProp === "object") {
1030
+ const pattern = idProp.pattern;
1031
+ if (typeof pattern === "string" && (pattern === "^[0-9a-fA-F]{24}$" || pattern === "^[a-f\\d]{24}$" || pattern === "^[a-fA-F0-9]{24}$" || /^\^\[[a-fA-F0-9\\d]+\]\{24\}\$$/.test(pattern))) {
1032
+ const cleanedId = { ...idProp };
1033
+ delete cleanedId.pattern;
1034
+ delete cleanedId.minLength;
1035
+ delete cleanedId.maxLength;
1036
+ if (!cleanedId.description) cleanedId.description = `${config.idField} (custom ID field)`;
1037
+ openApiSchemas = {
1038
+ ...openApiSchemas,
1039
+ params: {
1040
+ ...params,
1041
+ properties: {
1042
+ ...properties,
1043
+ id: cleanedId
1044
+ }
1045
+ }
1046
+ };
1047
+ }
1048
+ }
1049
+ }
1021
1050
  const queryParser = config.queryParser;
1022
1051
  if (queryParser?.getQuerySchema) {
1023
1052
  const querySchema = queryParser.getQuerySchema();
@@ -1156,7 +1185,7 @@ var ResourceDefinition = class {
1156
1185
  const openApi = self._registryMeta?.openApiSchemas;
1157
1186
  if (openApi && (!self.customSchemas || Object.keys(self.customSchemas).length === 0)) {
1158
1187
  const generated = {};
1159
- const { createBody, updateBody, params, response } = openApi;
1188
+ const { createBody, updateBody, params } = openApi;
1160
1189
  const safeBody = (schema) => {
1161
1190
  if (schema && typeof schema === "object" && schema.type === "object") return {
1162
1191
  additionalProperties: true,
@@ -1189,39 +1218,22 @@ var ResourceDefinition = class {
1189
1218
  }
1190
1219
  const listQuerySchema = self._registryMeta?.openApiSchemas?.listQuery;
1191
1220
  if (listQuerySchema) {
1192
- const KEEP_TYPE = new Set([
1221
+ const KEEP_AS_IS = new Set([
1193
1222
  "page",
1194
1223
  "limit",
1195
1224
  "sort",
1196
1225
  "search",
1197
1226
  "select",
1198
- "after"
1199
- ]);
1200
- const TYPE_DEPENDENT = new Set([
1201
- "type",
1202
- "minimum",
1203
- "maximum",
1204
- "minLength",
1205
- "maxLength",
1206
- "pattern",
1207
- "format",
1208
- "exclusiveMinimum",
1209
- "exclusiveMaximum",
1210
- "multipleOf",
1211
- "minItems",
1212
- "maxItems",
1213
- "uniqueItems"
1227
+ "after",
1228
+ "populate",
1229
+ "lookup",
1230
+ "aggregate"
1214
1231
  ]);
1215
1232
  const props = listQuerySchema.properties;
1216
1233
  const normalizedProps = props ? { ...props } : void 0;
1217
1234
  if (normalizedProps) for (const key of Object.keys(normalizedProps)) {
1218
- if (KEEP_TYPE.has(key)) continue;
1219
- const prop = normalizedProps[key];
1220
- if (prop && typeof prop === "object" && "type" in prop) {
1221
- const cleaned = {};
1222
- for (const [k, v] of Object.entries(prop)) if (!TYPE_DEPENDENT.has(k)) cleaned[k] = v;
1223
- normalizedProps[key] = Object.keys(cleaned).length > 0 ? cleaned : {};
1224
- }
1235
+ if (KEEP_AS_IS.has(key)) continue;
1236
+ normalizedProps[key] = {};
1225
1237
  }
1226
1238
  const normalizedSchema = {
1227
1239
  ...listQuerySchema,
@@ -1,4 +1,4 @@
1
- import { $ as RegistryEntry } from "../interface-CrN45qz1.mjs";
1
+ import { et as RegistryEntry } from "../interface-DYH8AXGe.mjs";
2
2
  import { t as ExternalOpenApiPaths } from "../externalPaths-DpO-s7r8.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
 
@@ -1,4 +1,4 @@
1
- import { Bt as ResourceDefinition, n as DataAdapter } from "../interface-CrN45qz1.mjs";
1
+ import { Vt as ResourceDefinition, r as DataAdapter } from "../interface-DYH8AXGe.mjs";
2
2
  import { t as PermissionCheck } from "../types-BNUccdcf.mjs";
3
3
 
4
4
  //#region src/dynamic/ArcDynamicLoader.d.ts
@@ -1,5 +1,5 @@
1
1
  import { t as ArcQueryParser } from "../queryParser-CgCtsjti.mjs";
2
- import { n as defineResource } from "../defineResource-Ckxg6HrZ.mjs";
2
+ import { n as defineResource } from "../defineResource-wWMBB4GP.mjs";
3
3
  import { S as readOnly, _ as fullPublic, b as publicRead, g as authenticated, h as adminOnly, v as ownerWithAdminBypass, x as publicReadAdminWrite } from "../permissions-C8ImI8gC.mjs";
4
4
  //#region src/dynamic/ArcDynamicLoader.ts
5
5
  const VALID_FIELD_TYPES = new Set([
@@ -1,4 +1,4 @@
1
- import { a as CustomPluginAuthOption, c as RawBodyOptions, d as ResourceLike, f as loadResources, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as LoadResourcesOptions } from "../types-C1Z28coa.mjs";
1
+ import { a as CustomPluginAuthOption, c as RawBodyOptions, d as ResourceLike, f as loadResources, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as LoadResourcesOptions } from "../types-By-5mIfn.mjs";
2
2
  import { FastifyInstance } from "fastify";
3
3
 
4
4
  //#region src/factory/createApp.d.ts
@@ -1,2 +1,2 @@
1
- import { an as HookPhase, cn as HookSystemOptions, dn as afterUpdate, fn as beforeCreate, gn as defineHook, hn as createHookSystem, in as HookOperation, ln as afterCreate, mn as beforeUpdate, nn as HookContext, on as HookRegistration, pn as beforeDelete, rn as HookHandler, sn as HookSystem, tn as DefineHookOptions, un as afterDelete } from "../interface-CrN45qz1.mjs";
1
+ import { _n as defineHook, an as HookOperation, cn as HookSystem, dn as afterDelete, fn as afterUpdate, gn as createHookSystem, hn as beforeUpdate, in as HookHandler, ln as HookSystemOptions, mn as beforeDelete, nn as DefineHookOptions, on as HookPhase, pn as beforeCreate, rn as HookContext, sn as HookRegistration, un as afterCreate } from "../interface-DYH8AXGe.mjs";
2
2
  export { type DefineHookOptions, type HookContext, type HookHandler, type HookOperation, type HookPhase, type HookRegistration, HookSystem, type HookSystemOptions, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
@@ -1,4 +1,4 @@
1
- import { Ht as CrudRepository, K as ParsedQuery, W as OpenApiSchemas, Z as QueryParserInterface, a as RepositoryLike, dt as RouteSchemaOptions, n as DataAdapter, o as SchemaMetadata, s as ValidationResult } from "./interface-CrN45qz1.mjs";
1
+ import { G as OpenApiSchemas, Q as QueryParserInterface, Ut as CrudRepository, c as ValidationResult, ft as RouteSchemaOptions, n as AdapterSchemaContext, o as RepositoryLike, q as ParsedQuery, r as DataAdapter, s as SchemaMetadata } from "./interface-DYH8AXGe.mjs";
2
2
  import { Model } from "mongoose";
3
3
 
4
4
  //#region src/adapters/mongoose.d.ts
@@ -29,7 +29,7 @@ interface MongooseAdapterOptions<TDoc = unknown> {
29
29
  * });
30
30
  * ```
31
31
  */
32
- schemaGenerator?: (model: Model<TDoc>, options?: RouteSchemaOptions) => OpenApiSchemas | Record<string, unknown>;
32
+ schemaGenerator?: (model: Model<TDoc>, options?: RouteSchemaOptions, context?: AdapterSchemaContext) => OpenApiSchemas | Record<string, unknown>;
33
33
  }
34
34
  /**
35
35
  * Mongoose data adapter with proper type safety
@@ -53,7 +53,7 @@ declare class MongooseAdapter<TDoc = unknown> implements DataAdapter<TDoc> {
53
53
  * If a `schemaGenerator` plugin was provided (e.g. MongoKit's buildCrudSchemasFromModel),
54
54
  * it is used instead of the built-in basic conversion.
55
55
  */
56
- generateSchemas(schemaOptions?: RouteSchemaOptions): OpenApiSchemas | Record<string, unknown> | null;
56
+ generateSchemas(schemaOptions?: RouteSchemaOptions, context?: AdapterSchemaContext): OpenApiSchemas | Record<string, unknown> | null;
57
57
  /**
58
58
  * Extract relation metadata
59
59
  */
@@ -1,5 +1,5 @@
1
1
  import { s as RequestScope } from "./elevation-C_taLQrM.mjs";
2
- import { Ft as IControllerResponse, It as IRequestContext, O as FastifyWithDecorators, Pt as IController, S as CrudRouterOptions, b as CrudController, rt as RequestWithExtras, tt as RequestContext } from "./interface-CrN45qz1.mjs";
2
+ import { C as CrudRouterOptions, Ft as IController, It as IControllerResponse, Lt as IRequestContext, it as RequestWithExtras, k as FastifyWithDecorators, nt as RequestContext, x as CrudController } from "./interface-DYH8AXGe.mjs";
3
3
  import { t as PermissionCheck } from "./types-BNUccdcf.mjs";
4
4
  import { FastifyInstance, FastifyReply, FastifyRequest, RouteHandlerMethod } from "fastify";
5
5
 
package/dist/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
- import { $ as RegistryEntry, $t as PipelineStep, A as GracefulShutdownOptions, Bt as ResourceDefinition, C as CrudSchemas, Ct as BaseController, D as FastifyWithAuth, E as FastifyRequestExtras, F as InferResourceDoc, Ft as IControllerResponse, G as OwnershipCheck, H as MiddlewareHandler, Ht as CrudRepository, I as IntrospectionData, It as IRequestContext, J as PresetFunction, Jt as Interceptor, Kt as QueryOptions, L as IntrospectionPluginOptions, Lt as RouteHandler, M as HealthOptions, Mt as ControllerLike, N as InferAdapterDoc, O as FastifyWithDecorators, P as InferDocType, Pt as IController, Q as RateLimitConfig, Qt as PipelineContext, R as JWTPayload, S as CrudRouterOptions, V as MiddlewareConfig, Vt as defineResource, Wt as PaginatedResult, X as PresetResult, Xt as OperationFilter, Yt as NextFunction, Zt as PipelineConfig, a as RepositoryLike, at as ResourceConfig, b as CrudController, bt as ValidationResult$1, c as AdditionalRoute, ct as ResourceMetadata, dt as RouteSchemaOptions, en as Transform, et as RegistryStats, f as ArcInternalMetadata, ft as ServiceContext, gt as TypedResourceConfig, h as AuthPluginOptions, ht as TypedRepository, i as RelationMetadata, j as HealthCheck, k as FieldRule, l as AnyRecord, mt as TypedController, n as DataAdapter, nt as RequestIdOptions, o as SchemaMetadata, p as ArcRequest, qt as Guard, r as FieldMetadata, rt as RequestWithExtras, s as ValidationResult, tt as RequestContext, u as ApiResponse, ut as RouteHandlerMethod, v as ConfigError, vt as UserOrganization, w as EventDefinition, wt as BaseControllerOptions, x as CrudRouteKey, xt as envelope, yt as ValidateOptions } from "./interface-CrN45qz1.mjs";
1
+ import { $ as RateLimitConfig, $t as PipelineContext, A as FieldRule, C as CrudRouterOptions, D as FastifyRequestExtras, F as InferDocType, Ft as IController, Gt as PaginatedResult, H as MiddlewareConfig, Ht as defineResource, I as InferResourceDoc, It as IControllerResponse, Jt as Guard, K as OwnershipCheck, L as IntrospectionData, Lt as IRequestContext, M as HealthCheck, N as HealthOptions, Nt as ControllerLike, O as FastifyWithAuth, P as InferAdapterDoc, Qt as PipelineConfig, R as IntrospectionPluginOptions, Rt as RouteHandler, S as CrudRouteKey, St as envelope, T as EventDefinition, Tt as BaseControllerOptions, U as MiddlewareHandler, Ut as CrudRepository, Vt as ResourceDefinition, Xt as NextFunction, Y as PresetFunction, Yt as Interceptor, Z as PresetResult, Zt as OperationFilter, _t as TypedResourceConfig, a as RelationMetadata, bt as ValidateOptions, c as ValidationResult, d as ApiResponse, dt as RouteHandlerMethod, en as PipelineStep, et as RegistryEntry, ft as RouteSchemaOptions, g as AuthPluginOptions, gt as TypedRepository, ht as TypedController, i as FieldMetadata, it as RequestWithExtras, j as GracefulShutdownOptions, k as FastifyWithDecorators, l as AdditionalRoute, lt as ResourceMetadata, m as ArcRequest, nt as RequestContext, o as RepositoryLike, ot as ResourceConfig, p as ArcInternalMetadata, pt as ServiceContext, qt as QueryOptions, r as DataAdapter, rt as RequestIdOptions, s as SchemaMetadata, tn as Transform, tt as RegistryStats, u as AnyRecord, w as CrudSchemas, wt as BaseController, x as CrudController, xt as ValidationResult$1, y as ConfigError, yt as UserOrganization, z as JWTPayload } from "./interface-DYH8AXGe.mjs";
2
2
  import { a as applyFieldWritePermissions, i as applyFieldReadPermissions, n as FieldPermissionMap, o as fields, t as FieldPermission } from "./fields-DFwdaWCq.mjs";
3
3
  import { i as UserBase, n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "./types-BNUccdcf.mjs";
4
- import { l as createMongooseAdapter, o as createPrismaAdapter, s as MongooseAdapter, t as PrismaAdapter } from "./index-DrCqa3Jq.mjs";
5
- import { A as RESERVED_QUERY_PARAMS, C as HookOperation, D as MAX_SEARCH_LENGTH, E as MAX_REGEX_LENGTH, O as MUTATION_OPERATIONS, S as HOOK_PHASES, T as MAX_FILTER_DEPTH, _ as DEFAULT_MAX_LIMIT, a as getControllerScope, b as DEFAULT_UPDATE_METHOD, g as DEFAULT_LIMIT, h as DEFAULT_ID_FIELD, j as SYSTEM_FIELDS, k as MutationOperation, m as CrudOperation, p as CRUD_OPERATIONS, v as DEFAULT_SORT, w as HookPhase, x as HOOK_OPERATIONS, y as DEFAULT_TENANT_FIELD } from "./index-B4uZm82R.mjs";
4
+ import { l as createMongooseAdapter, o as createPrismaAdapter, s as MongooseAdapter, t as PrismaAdapter } from "./index-CHeJa4Zd.mjs";
5
+ import { A as RESERVED_QUERY_PARAMS, C as HookOperation, D as MAX_SEARCH_LENGTH, E as MAX_REGEX_LENGTH, O as MUTATION_OPERATIONS, S as HOOK_PHASES, T as MAX_FILTER_DEPTH, _ as DEFAULT_MAX_LIMIT, a as getControllerScope, b as DEFAULT_UPDATE_METHOD, g as DEFAULT_LIMIT, h as DEFAULT_ID_FIELD, j as SYSTEM_FIELDS, k as MutationOperation, m as CrudOperation, p as CRUD_OPERATIONS, v as DEFAULT_SORT, w as HookPhase, x as HOOK_OPERATIONS, y as DEFAULT_TENANT_FIELD } from "./index-gz6iuzCp.mjs";
6
6
  import { C as presets_d_exports, E as readOnly, S as ownerWithAdminBypass, T as publicReadAdminWrite, a as allOf, b as authenticated, c as createDynamicPermissionMatrix, d as requireAuth, f as requireOrgMembership, g as requireTeamMembership, h as requireRoles, l as createOrgPermissions, m as requireOwnership, n as DynamicPermissionMatrix, o as allowPublic, p as requireOrgRole, r as DynamicPermissionMatrixConfig, s as anyOf, u as denyAll, v as when, w as publicRead, x as fullPublic, y as adminOnly } from "./index-NGZksqM5.mjs";
7
7
  import { a as NotFoundError, d as ValidationError, f as createDomainError, i as ForbiddenError, t as ArcError, u as UnauthorizedError } from "./errors-CcVbl1-T.mjs";
8
8
  import { AsyncLocalStorage } from "node:async_hooks";
package/dist/index.mjs CHANGED
@@ -1,11 +1,11 @@
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 { a as createMongooseAdapter, i as MongooseAdapter, r as createPrismaAdapter, t as PrismaAdapter } from "./adapters-CTn28N4y.mjs";
3
- import { t as BaseController } from "./BaseController-AbbRx3e0.mjs";
2
+ import { a as createMongooseAdapter, i as MongooseAdapter, r as createPrismaAdapter, t as PrismaAdapter } from "./adapters-gM-WYjNe.mjs";
3
+ import { t as BaseController } from "./BaseController-DzRtluEF.mjs";
4
4
  import { envelope } from "./types/index.mjs";
5
5
  import { n as applyFieldWritePermissions, r as fields, t as applyFieldReadPermissions } from "./fields-ipsbIRPK.mjs";
6
6
  import { t as requestContext } from "./requestContext-DYtmNpm5.mjs";
7
7
  import { d as createDomainError, i as NotFoundError, l as UnauthorizedError, r as ForbiddenError, t as ArcError, u as ValidationError } from "./errors-NoQKsbAT.mjs";
8
- import { a as validateResourceConfig, f as getControllerScope, i as formatValidationErrors, m as pipe, n as defineResource, r as assertValidConfig, t as ResourceDefinition } from "./defineResource-Ckxg6HrZ.mjs";
8
+ import { a as validateResourceConfig, f as getControllerScope, i as formatValidationErrors, m as pipe, n as defineResource, r as assertValidConfig, t as ResourceDefinition } from "./defineResource-wWMBB4GP.mjs";
9
9
  import { S as readOnly, _ as fullPublic, a as createOrgPermissions, b as publicRead, c as requireOrgMembership, d as requireRoles, f as requireTeamMembership, g as authenticated, h as adminOnly, i as createDynamicPermissionMatrix, l as requireOrgRole, m as when, n as allowPublic, o as denyAll, r as anyOf, s as requireAuth, t as allOf, u as requireOwnership, v as ownerWithAdminBypass, x as publicReadAdminWrite, y as presets_exports } from "./permissions-C8ImI8gC.mjs";
10
10
  import { n as configureArcLogger, t as arcLog } from "./logger-Dz3j1ItV.mjs";
11
11
  //#region src/middleware/middleware.ts
@@ -127,6 +127,6 @@ function transform(name, handlerOrOptions) {
127
127
  }
128
128
  //#endregion
129
129
  //#region src/index.ts
130
- const version = "2.6.2";
130
+ const version = "2.6.3";
131
131
  //#endregion
132
132
  export { ArcError, BaseController, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, ForbiddenError, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MongooseAdapter, NotFoundError, PrismaAdapter, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, UnauthorizedError, ValidationError, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, arcLog, assertValidConfig, authenticated, configureArcLogger, createDomainError, createDynamicPermissionMatrix, createMongooseAdapter, createOrgPermissions, createPrismaAdapter, defineResource, denyAll, envelope, fields, formatValidationErrors, fullPublic, getControllerScope, guard, intercept, middleware, ownerWithAdminBypass, presets_exports as permissions, pipe, publicRead, publicReadAdminWrite, readOnly, requestContext, requireAuth, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireTeamMembership, sortMiddlewares, transform, validateResourceConfig, version, when };
@@ -1,7 +1,7 @@
1
1
  import { WebSocketClient, WebSocketMessage, WebSocketPluginOptions } from "./websocket.mjs";
2
2
  import { EventGatewayOptions } from "./event-gateway.mjs";
3
3
  import { JobDefinition, JobDispatchOptions, JobDispatcher, JobMeta, JobsPluginOptions, QueueStats } from "./jobs.mjs";
4
- import { c as McpResourceConfig, f as ToolAnnotations, i as CrudOperation, l as PromptDefinition, m as ToolDefinition, n as CallToolResult, o as McpAuthResult, p as ToolContext, r as CreateMcpServerConfig, s as McpPluginOptions, t as BetterAuthHandler } from "../types-DurlBP2N.mjs";
4
+ import { c as McpResourceConfig, f as ToolAnnotations, i as CrudOperation, l as PromptDefinition, m as ToolDefinition, n as CallToolResult, o as McpAuthResult, p as ToolContext, r as CreateMcpServerConfig, s as McpPluginOptions, t as BetterAuthHandler } from "../types-B4_TDdPe.mjs";
5
5
  import { StreamlinePluginOptions, WorkflowLike, WorkflowRunLike } from "./streamline.mjs";
6
6
  import { WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription } from "./webhooks.mjs";
7
7
  export { type BetterAuthHandler, type CallToolResult, type CreateMcpServerConfig, type CrudOperation, type EventGatewayOptions, type JobDefinition, type JobDispatchOptions, type JobDispatcher, type JobMeta, type JobsPluginOptions, type McpAuthResult, type McpPluginOptions, type McpResourceConfig, type PromptDefinition, type QueueStats, type StreamlinePluginOptions, type ToolAnnotations, type ToolContext, type ToolDefinition, type WebSocketClient, type WebSocketMessage, type WebSocketPluginOptions, type WebhookDeliveryRecord, type WebhookManager, type WebhookPluginOptions, type WebhookStore, type WebhookSubscription, type WorkflowLike, type WorkflowRunLike };
@@ -1,5 +1,5 @@
1
- import { Bt as ResourceDefinition } from "../../interface-CrN45qz1.mjs";
2
- import { a as McpAuthResolver, c as McpResourceConfig, d as SessionEntry, f as ToolAnnotations, i as CrudOperation, l as PromptDefinition, m as ToolDefinition, n as CallToolResult, o as McpAuthResult, p as ToolContext, r as CreateMcpServerConfig, s as McpPluginOptions, t as BetterAuthHandler, u as PromptResult } from "../../types-DurlBP2N.mjs";
1
+ import { Vt as ResourceDefinition } from "../../interface-DYH8AXGe.mjs";
2
+ import { a as McpAuthResolver, c as McpResourceConfig, d as SessionEntry, f as ToolAnnotations, i as CrudOperation, l as PromptDefinition, m as ToolDefinition, n as CallToolResult, o as McpAuthResult, p as ToolContext, r as CreateMcpServerConfig, s as McpPluginOptions, t as BetterAuthHandler, u as PromptResult } from "../../types-B4_TDdPe.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
  import { z } from "zod";
5
5
 
@@ -1,4 +1,4 @@
1
- import { n as fieldRulesToZod, r as createMcpServer, t as resourceToTools } from "../../resourceToTools-DH3c3e-T.mjs";
1
+ import { n as fieldRulesToZod, r as createMcpServer, t as resourceToTools } from "../../resourceToTools-nCJWnG1r.mjs";
2
2
  import { createHash } from "node:crypto";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/integrations/mcp/definePrompt.ts
@@ -1,4 +1,4 @@
1
- import { o as McpAuthResult, s as McpPluginOptions } from "../../types-DurlBP2N.mjs";
1
+ import { o as McpAuthResult, s as McpPluginOptions } from "../../types-B4_TDdPe.mjs";
2
2
 
3
3
  //#region src/integrations/mcp/testing.d.ts
4
4
  interface TestMcpClientOptions {
@@ -1,4 +1,4 @@
1
- import { r as createMcpServer, t as resourceToTools } from "../../resourceToTools-DH3c3e-T.mjs";
1
+ import { r as createMcpServer, t as resourceToTools } from "../../resourceToTools-nCJWnG1r.mjs";
2
2
  //#region src/integrations/mcp/testing.ts
3
3
  /**
4
4
  * @classytic/arc/mcp/testing — MCP Test Utilities
@@ -1037,6 +1037,25 @@ declare class BaseController<TDoc = AnyRecord, TRepository extends RepositoryLik
1037
1037
  getTree(req: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
1038
1038
  getChildren(req: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
1039
1039
  bulkCreate(req: IRequestContext): Promise<IControllerResponse<TDoc[]>>;
1040
+ /**
1041
+ * Build a tenant-scoped filter for bulk update/delete.
1042
+ *
1043
+ * Mirrors `AccessControl.buildIdFilter` semantics for single-doc ops:
1044
+ * - Always merge `_policyFilters` (from permission middleware)
1045
+ * - When `tenantField` is set AND a `member` scope is present, add the
1046
+ * org filter so cross-tenant data can't be touched.
1047
+ * - When the scope is `elevated` (platform admin), no org filter is
1048
+ * applied — admins can bulk-update across orgs intentionally.
1049
+ * - When the scope is `public` on a tenant-scoped resource, deny.
1050
+ * - When NO scope is present at all (e.g., direct controller calls in
1051
+ * unit tests, or app routes without auth middleware), the controller
1052
+ * stays lenient — it's the middleware layer's job to fail-close.
1053
+ * Apps that want fail-close on bulk routes should run the multi-tenant
1054
+ * preset middleware (or equivalent) ahead of these handlers.
1055
+ *
1056
+ * Returns the merged filter, or `null` when access must be denied.
1057
+ */
1058
+ private buildBulkFilter;
1040
1059
  bulkUpdate(req: IRequestContext): Promise<IControllerResponse<{
1041
1060
  matchedCount: number;
1042
1061
  modifiedCount: number;
@@ -1523,9 +1542,22 @@ interface ResourceConfig<TDoc = AnyRecord> {
1523
1542
  tenantField?: string | false;
1524
1543
  /**
1525
1544
  * Primary key field name (default: '_id').
1526
- * Override for non-MongoDB adapters (e.g., 'id' for SQL databases).
1545
+ *
1546
+ * Type-narrowed to `keyof TDoc` when `defineResource<TDoc>` is called with
1547
+ * a typed document interface — gives autocomplete for valid field names —
1548
+ * while still accepting any string when TDoc is `unknown` / `AnyRecord` so
1549
+ * adapters with dynamic shapes still work.
1550
+ *
1551
+ * @example
1552
+ * ```ts
1553
+ * defineResource<IJob>({ idField: 'jobId' }) // ← autocompletes from IJob fields
1554
+ * defineResource({ idField: 'sku' }) // ← any string allowed
1555
+ * ```
1556
+ *
1557
+ * Override for non-MongoDB adapters (e.g., 'id' for SQL databases) or
1558
+ * resources keyed by a business identifier (slug, sku, orderNumber).
1527
1559
  */
1528
- idField?: string;
1560
+ idField?: (keyof TDoc & string) | (string & {});
1529
1561
  module?: string;
1530
1562
  events?: Record<string, EventDefinition>;
1531
1563
  skipValidation?: boolean;
@@ -2367,9 +2399,15 @@ interface DataAdapter<TDoc = unknown> {
2367
2399
  * For example, Mongoose adapter can use mongokit to generate schemas from Mongoose models.
2368
2400
  *
2369
2401
  * @param options - Schema generation options (field rules, populate settings, etc.)
2402
+ * @param context - Resource-level context: idField (for params schema), resourceName.
2403
+ * Adapters should honor `context.idField` when producing the params
2404
+ * schema — e.g. skip the ObjectId pattern when idField is a custom
2405
+ * string field. Backwards compatible: legacy adapters ignoring the
2406
+ * context still work because Arc strips the mismatched pattern as
2407
+ * a safety net.
2370
2408
  * @returns OpenAPI schemas for CRUD operations or null if not supported
2371
2409
  */
2372
- generateSchemas?(options?: RouteSchemaOptions): OpenApiSchemas | Record<string, unknown> | null;
2410
+ generateSchemas?(options?: RouteSchemaOptions, context?: AdapterSchemaContext): OpenApiSchemas | Record<string, unknown> | null;
2373
2411
  /** Extract schema metadata for OpenAPI/introspection */
2374
2412
  getSchemaMetadata?(): SchemaMetadata | null;
2375
2413
  /** Validate data against schema before persistence */
@@ -2385,6 +2423,18 @@ interface DataAdapter<TDoc = unknown> {
2385
2423
  /** Close/cleanup resources */
2386
2424
  close?(): Promise<void>;
2387
2425
  }
2426
+ /**
2427
+ * Context passed to `adapter.generateSchemas()` so adapters can shape the
2428
+ * output to match resource-level configuration (idField overrides, etc).
2429
+ * All fields are optional — adapters are free to ignore this argument, in
2430
+ * which case Arc applies safety-net normalization to the generated schemas.
2431
+ */
2432
+ interface AdapterSchemaContext {
2433
+ /** The idField configured on the resource. Defaults to "_id". */
2434
+ idField?: string;
2435
+ /** Resource name (for error messages / logging). */
2436
+ resourceName?: string;
2437
+ }
2388
2438
  interface SchemaMetadata {
2389
2439
  name: string;
2390
2440
  fields: Record<string, FieldMetadata>;
@@ -2426,4 +2476,4 @@ interface ValidationResult {
2426
2476
  }
2427
2477
  type AdapterFactory<TDoc> = (config: unknown) => DataAdapter<TDoc>;
2428
2478
  //#endregion
2429
- export { RegistryEntry as $, PipelineStep as $t, GracefulShutdownOptions as A, AccessControlConfig as At, LookupOption as B, ResourceDefinition as Bt, CrudSchemas as C, BaseController as Ct, FastifyWithAuth as D, BodySanitizer as Dt, FastifyRequestExtras as E, QueryResolverConfig as Et, InferResourceDoc as F, IControllerResponse as Ft, OwnershipCheck as G, PaginationParams as Gt, MiddlewareHandler as H, CrudRepository as Ht, IntrospectionData as I, IRequestContext as It, PresetFunction as J, Interceptor as Jt, ParsedQuery as K, QueryOptions as Kt, IntrospectionPluginOptions as L, RouteHandler as Lt, HealthOptions as M, ControllerLike as Mt, InferAdapterDoc as N, FastifyHandler as Nt, FastifyWithDecorators as O, BodySanitizerConfig as Ot, InferDocType as P, IController as Pt, RateLimitConfig as Q, PipelineContext as Qt, JWTPayload as R, RegisterOptions as Rt, CrudRouterOptions as S, getUserId as St, EventsDecorator as T, QueryResolver as Tt, ObjectId as U, InferDoc as Ut, MiddlewareConfig as V, defineResource as Vt, OpenApiSchemas as W, PaginatedResult as Wt, PresetResult as X, OperationFilter as Xt, PresetHook as Y, NextFunction as Yt, QueryParserInterface as Z, PipelineConfig as Zt, AuthenticatorContext as _, UserLike as _t, RepositoryLike as a, HookPhase as an, ResourceConfig as at, CrudController as b, ValidationResult$1 as bt, AdditionalRoute as c, HookSystemOptions as cn, ResourceMetadata as ct, ArcDecorator as d, afterUpdate as dn, RouteSchemaOptions as dt, Transform as en, RegistryStats as et, ArcInternalMetadata as f, beforeCreate as fn, ServiceContext as ft, Authenticator as g, defineHook as gn, TypedResourceConfig as gt, AuthPluginOptions as h, createHookSystem as hn, TypedRepository as ht, RelationMetadata as i, HookOperation as in, ResourceCacheConfig as it, HealthCheck as j, ControllerHandler as jt, FieldRule as k, AccessControl as kt, AnyRecord as l, afterCreate as ln, ResourcePermissions as lt, AuthHelpers as m, beforeUpdate as mn, TypedController as mt, DataAdapter as n, HookContext as nn, RequestIdOptions as nt, SchemaMetadata as o, HookRegistration as on, ResourceHookContext as ot, ArcRequest as p, beforeDelete as pn, TokenPair as pt, PopulateOption as q, Guard as qt, FieldMetadata as r, HookHandler as rn, RequestWithExtras as rt, ValidationResult as s, HookSystem as sn, ResourceHooks as st, AdapterFactory as t, DefineHookOptions as tn, RequestContext as tt, ApiResponse as u, afterDelete as un, RouteHandlerMethod$1 as ut, ConfigError as v, UserOrganization as vt, EventDefinition as w, BaseControllerOptions as wt, CrudRouteKey as x, envelope as xt, ControllerQueryOptions as y, ValidateOptions as yt, JwtContext as z, ResourceRegistry as zt };
2479
+ export { RateLimitConfig as $, PipelineContext as $t, FieldRule as A, AccessControl as At, JwtContext as B, ResourceRegistry as Bt, CrudRouterOptions as C, getUserId as Ct, FastifyRequestExtras as D, QueryResolverConfig as Dt, EventsDecorator as E, QueryResolver as Et, InferDocType as F, IController as Ft, OpenApiSchemas as G, PaginatedResult as Gt, MiddlewareConfig as H, defineResource as Ht, InferResourceDoc as I, IControllerResponse as It, PopulateOption as J, Guard as Jt, OwnershipCheck as K, PaginationParams as Kt, IntrospectionData as L, IRequestContext as Lt, HealthCheck as M, ControllerHandler as Mt, HealthOptions as N, ControllerLike as Nt, FastifyWithAuth as O, BodySanitizer as Ot, InferAdapterDoc as P, FastifyHandler as Pt, QueryParserInterface as Q, PipelineConfig as Qt, IntrospectionPluginOptions as R, RouteHandler as Rt, CrudRouteKey as S, envelope as St, EventDefinition as T, BaseControllerOptions as Tt, MiddlewareHandler as U, CrudRepository as Ut, LookupOption as V, ResourceDefinition as Vt, ObjectId as W, InferDoc as Wt, PresetHook as X, NextFunction as Xt, PresetFunction as Y, Interceptor as Yt, PresetResult as Z, OperationFilter as Zt, Authenticator as _, defineHook as _n, TypedResourceConfig as _t, RelationMetadata as a, HookOperation as an, ResourceCacheConfig as at, ControllerQueryOptions as b, ValidateOptions as bt, ValidationResult as c, HookSystem as cn, ResourceHooks as ct, ApiResponse as d, afterDelete as dn, RouteHandlerMethod$1 as dt, PipelineStep as en, RegistryEntry as et, ArcDecorator as f, afterUpdate as fn, RouteSchemaOptions as ft, AuthPluginOptions as g, createHookSystem as gn, TypedRepository as gt, AuthHelpers as h, beforeUpdate as hn, TypedController as ht, FieldMetadata as i, HookHandler as in, RequestWithExtras as it, GracefulShutdownOptions as j, AccessControlConfig as jt, FastifyWithDecorators as k, BodySanitizerConfig as kt, AdditionalRoute as l, HookSystemOptions as ln, ResourceMetadata as lt, ArcRequest as m, beforeDelete as mn, TokenPair as mt, AdapterSchemaContext as n, DefineHookOptions as nn, RequestContext as nt, RepositoryLike as o, HookPhase as on, ResourceConfig as ot, ArcInternalMetadata as p, beforeCreate as pn, ServiceContext as pt, ParsedQuery as q, QueryOptions as qt, DataAdapter as r, HookContext as rn, RequestIdOptions as rt, SchemaMetadata as s, HookRegistration as sn, ResourceHookContext as st, AdapterFactory as t, Transform as tn, RegistryStats as tt, AnyRecord as u, afterCreate as un, ResourcePermissions as ut, AuthenticatorContext as v, UserLike as vt, CrudSchemas as w, BaseController as wt, CrudController as x, ValidationResult$1 as xt, ConfigError as y, UserOrganization as yt, JWTPayload as z, RegisterOptions as zt };
@@ -1,4 +1,4 @@
1
- import { Lt as RouteHandler } from "../interface-CrN45qz1.mjs";
1
+ import { Rt as RouteHandler } from "../interface-DYH8AXGe.mjs";
2
2
  import { i as UserBase } from "../types-BNUccdcf.mjs";
3
3
  import { InvitationAdapter, InvitationDoc, MemberDoc, OrgAdapter, OrgDoc, OrgPermissionStatement, OrgRole, OrganizationPluginOptions } from "./types.mjs";
4
4
  import { FastifyPluginAsync, RouteHandlerMethod } from "fastify";
@@ -1,4 +1,4 @@
1
- import { V as MiddlewareConfig, Y as PresetHook, c as AdditionalRoute, dt as RouteSchemaOptions, l as AnyRecord, sn as HookSystem, zt as ResourceRegistry } from "../interface-CrN45qz1.mjs";
1
+ import { Bt as ResourceRegistry, H as MiddlewareConfig, X as PresetHook, cn as HookSystem, ft as RouteSchemaOptions, l as AdditionalRoute, u as AnyRecord } from "../interface-DYH8AXGe.mjs";
2
2
  import { t as ExternalOpenApiPaths } from "../externalPaths-DpO-s7r8.mjs";
3
3
  import { _ as cachingPlugin, a as versioningPlugin, c as MetricsOptions, d as SSEOptions, f as _default$6, g as _default$1, h as CachingRule, i as _default$7, l as _default$4, m as CachingOptions, n as errorHandlerPlugin, o as MetricEntry, p as ssePlugin, r as VersioningOptions, s as MetricsCollector, t as ErrorHandlerOptions, u as metricsPlugin } from "../errorHandler-Do4vVQ1f.mjs";
4
4
  import { t as TracingOptions } from "../tracing-bz_U4EM1.mjs";
@@ -44,7 +44,7 @@ try {
44
44
  function createTracerProvider(options) {
45
45
  if (!isAvailable) return null;
46
46
  const { serviceName = "@classytic/arc", serviceVersion, exporterUrl = "http://localhost:4318/v1/traces" } = options;
47
- const resolvedVersion = serviceVersion ?? "2.6.2";
47
+ const resolvedVersion = serviceVersion ?? "2.6.3";
48
48
  const exporter = new OTLPTraceExporter({ url: exporterUrl });
49
49
  const provider = new NodeTracerProvider({ resource: { attributes: {
50
50
  "service.name": serviceName,
@@ -1,4 +1,4 @@
1
- import { Ft as IControllerResponse, It as IRequestContext, Wt as PaginatedResult, X as PresetResult, at as ResourceConfig, l as AnyRecord } from "../interface-CrN45qz1.mjs";
1
+ import { Gt as PaginatedResult, It as IControllerResponse, Lt as IRequestContext, Z as PresetResult, ot as ResourceConfig, u as AnyRecord } from "../interface-DYH8AXGe.mjs";
2
2
  import { MultiTenantOptions, multiTenantPreset } from "./multiTenant.mjs";
3
3
 
4
4
  //#region src/presets/ownedByUser.d.ts
@@ -1,4 +1,4 @@
1
- import { X as PresetResult, x as CrudRouteKey } from "../interface-CrN45qz1.mjs";
1
+ import { S as CrudRouteKey, Z as PresetResult } from "../interface-DYH8AXGe.mjs";
2
2
 
3
3
  //#region src/presets/multiTenant.d.ts
4
4
  interface MultiTenantOptions {
@@ -1,4 +1,4 @@
1
- import { L as IntrospectionPluginOptions, Rt as RegisterOptions, zt as ResourceRegistry } from "../interface-CrN45qz1.mjs";
1
+ import { Bt as ResourceRegistry, R as IntrospectionPluginOptions, zt as RegisterOptions } from "../interface-DYH8AXGe.mjs";
2
2
  import { FastifyPluginAsync } from "fastify";
3
3
 
4
4
  //#region src/registry/introspectionPlugin.d.ts
@@ -1,4 +1,4 @@
1
- import { t as BaseController } from "./BaseController-AbbRx3e0.mjs";
1
+ import { t as BaseController } from "./BaseController-DzRtluEF.mjs";
2
2
  import { t as pluralize } from "./pluralize-CcT6qF0a.mjs";
3
3
  import { z } from "zod";
4
4
  //#region src/integrations/mcp/createMcpServer.ts
@@ -314,6 +314,146 @@ function buildScope(auth) {
314
314
  };
315
315
  }
316
316
  //#endregion
317
+ //#region src/integrations/mcp/jsonSchemaToZod.ts
318
+ /**
319
+ * @classytic/arc — JSON Schema → Zod shape converter
320
+ *
321
+ * Converts an adapter-emitted JSON Schema body shape (`createBody` / `updateBody`)
322
+ * to a flat Zod shape compatible with the MCP SDK's `registerTool({ inputSchema })`
323
+ * contract.
324
+ *
325
+ * Why this exists:
326
+ * - The MCP SDK expects a flat `Record<string, ZodType>` shape (it wraps it in
327
+ * z.object() internally).
328
+ * - When users don't supply explicit `schemaOptions.fieldRules`, MCP would
329
+ * otherwise see an empty schema and silently strip every body field — that's
330
+ * a real DX footgun.
331
+ * - Adapters (Mongoose, MongoKit's buildCrudSchemasFromModel, custom) already
332
+ * emit JSON Schema describing the body. We translate it to Zod so MCP tools
333
+ * can validate input the same way REST routes do.
334
+ *
335
+ * Supported JSON Schema features:
336
+ * - Primitives: string, number, integer, boolean, null (skipped)
337
+ * - Constraints: minLength, maxLength, minimum, maximum, pattern, enum, format
338
+ * - Arrays: typed items + nested object items
339
+ * - Nested objects with `properties` (recursive)
340
+ * - Type unions: ["string", "null"] → string (null skipped)
341
+ * - Composition: oneOf / anyOf / allOf → first viable branch
342
+ * - $ref → permissive (z.unknown()) — refs are not resolved
343
+ * - Unknown types → z.unknown() (lenient — let the controller validate)
344
+ *
345
+ * NOT supported (intentionally — keeps the surface small + deterministic):
346
+ * - Conditional schemas (if/then/else)
347
+ * - dependencies / dependentRequired
348
+ * - Custom keywords beyond standard JSON Schema
349
+ */
350
+ /**
351
+ * Convert a JSON Schema **object** body to a flat Zod shape.
352
+ * Returns `undefined` if the input has no usable properties.
353
+ *
354
+ * @param schema Top-level JSON Schema (must be `type: 'object'` with `properties`)
355
+ * @param mode 'create' enforces required fields, 'update' makes everything optional
356
+ */
357
+ function jsonSchemaToZodShape(schema, mode = "create") {
358
+ if (!schema || typeof schema !== "object") return void 0;
359
+ if (!schema.properties || typeof schema.properties !== "object") return void 0;
360
+ const requiredSet = new Set(schema.required ?? []);
361
+ const shape = {};
362
+ for (const [name, propSchema] of Object.entries(schema.properties)) {
363
+ if (!propSchema || typeof propSchema !== "object") continue;
364
+ const fieldZod = jsonSchemaPropertyToZod(propSchema);
365
+ if (!fieldZod) continue;
366
+ shape[name] = mode === "create" && requiredSet.has(name) ? fieldZod : fieldZod.optional();
367
+ }
368
+ return Object.keys(shape).length > 0 ? shape : void 0;
369
+ }
370
+ /**
371
+ * Convert one JSON Schema node to a Zod type.
372
+ * Handles primitives, arrays, nested objects, type unions, and composition.
373
+ */
374
+ function jsonSchemaPropertyToZod(prop) {
375
+ if (prop.$ref) return applyDescription(z.unknown(), prop);
376
+ if (Array.isArray(prop.oneOf) && prop.oneOf.length > 0) for (const branch of prop.oneOf) {
377
+ const z1 = jsonSchemaPropertyToZod(branch);
378
+ if (z1) return applyDescription(z1, prop);
379
+ }
380
+ if (Array.isArray(prop.anyOf) && prop.anyOf.length > 0) for (const branch of prop.anyOf) {
381
+ const z1 = jsonSchemaPropertyToZod(branch);
382
+ if (z1) return applyDescription(z1, prop);
383
+ }
384
+ if (Array.isArray(prop.allOf) && prop.allOf.length > 0) for (let i = prop.allOf.length - 1; i >= 0; i--) {
385
+ const branch = prop.allOf[i];
386
+ const z1 = jsonSchemaPropertyToZod(branch);
387
+ if (z1) return applyDescription(z1, prop);
388
+ }
389
+ if (Array.isArray(prop.enum) && prop.enum.length > 0) {
390
+ const stringValues = prop.enum.filter((v) => typeof v === "string");
391
+ if (stringValues.length > 0) return applyDescription(z.enum(stringValues), prop);
392
+ return applyDescription(z.number(), prop);
393
+ }
394
+ const typeCandidates = pickEffectiveType(prop.type);
395
+ for (const t of typeCandidates) switch (t) {
396
+ case "string": return applyStringConstraints(z.string(), prop);
397
+ case "number":
398
+ case "integer": return applyNumberConstraints(z.number(), prop);
399
+ case "boolean": return applyDescription(z.boolean(), prop);
400
+ case "array": return applyDescription(arrayToZod(prop), prop);
401
+ case "object": return applyDescription(objectToZod(prop), prop);
402
+ case "null": continue;
403
+ default: break;
404
+ }
405
+ return applyDescription(z.unknown(), prop);
406
+ }
407
+ function pickEffectiveType(rawType) {
408
+ if (!rawType) return [];
409
+ if (Array.isArray(rawType)) return rawType.filter((t) => t !== "null");
410
+ return rawType === "null" ? [] : [rawType];
411
+ }
412
+ function arrayToZod(prop) {
413
+ const items = prop.items;
414
+ if (items && typeof items === "object") {
415
+ const itemZod = jsonSchemaPropertyToZod(items);
416
+ if (itemZod) return z.array(itemZod);
417
+ }
418
+ return z.array(z.unknown());
419
+ }
420
+ function objectToZod(prop) {
421
+ if (prop.properties && typeof prop.properties === "object") {
422
+ const requiredSet = new Set(prop.required ?? []);
423
+ const innerShape = {};
424
+ for (const [k, v] of Object.entries(prop.properties)) {
425
+ if (!v || typeof v !== "object") continue;
426
+ const inner = jsonSchemaPropertyToZod(v);
427
+ if (!inner) continue;
428
+ innerShape[k] = requiredSet.has(k) ? inner : inner.optional();
429
+ }
430
+ if (Object.keys(innerShape).length > 0) return z.object(innerShape);
431
+ }
432
+ return z.record(z.string(), z.unknown());
433
+ }
434
+ function applyStringConstraints(base, prop) {
435
+ let s = base;
436
+ if (typeof prop.minLength === "number") s = s.min(prop.minLength);
437
+ if (typeof prop.maxLength === "number") s = s.max(prop.maxLength);
438
+ if (typeof prop.pattern === "string") try {
439
+ s = s.regex(new RegExp(prop.pattern));
440
+ } catch {}
441
+ if (prop.format === "email") s = s.email();
442
+ if (prop.format === "uuid") s = s.uuid();
443
+ if (prop.format === "uri" || prop.format === "url") s = s.url();
444
+ return applyDescription(s, prop);
445
+ }
446
+ function applyNumberConstraints(base, prop) {
447
+ let n = base;
448
+ if (typeof prop.minimum === "number") n = n.min(prop.minimum);
449
+ if (typeof prop.maximum === "number") n = n.max(prop.maximum);
450
+ return applyDescription(n, prop);
451
+ }
452
+ function applyDescription(zodType, prop) {
453
+ if (typeof prop.description === "string" && prop.description.length > 0) return zodType.describe(prop.description);
454
+ return zodType;
455
+ }
456
+ //#endregion
317
457
  //#region src/integrations/mcp/resourceToTools.ts
318
458
  /**
319
459
  * @classytic/arc — Resource → MCP Tools Generator
@@ -359,9 +499,11 @@ const ANNOTATIONS = {
359
499
  function resourceToTools(resource, config = {}) {
360
500
  const controller = resource.controller ?? (resource.adapter ? createMcpController(resource) : void 0);
361
501
  if (!controller) return [];
362
- const fieldRules = resource.schemaOptions?.fieldRules;
502
+ const explicitFieldRules = resource.schemaOptions?.fieldRules;
363
503
  const hiddenFields = resource.schemaOptions?.hiddenFields;
364
504
  const readonlyFields = resource.schemaOptions?.readonlyFields;
505
+ const adapterBodies = explicitFieldRules ? void 0 : getAdapterBodies(resource);
506
+ const fieldRules = explicitFieldRules ?? deriveFieldRulesFromAdapter(resource);
365
507
  const filterableFields = resource.schemaOptions?.filterableFields ?? resource.queryParser?.allowedFilterFields;
366
508
  const sortableFields = resource.queryParser?.allowedSortFields;
367
509
  const allowedOperators = resource.queryParser?.allowedOperators;
@@ -388,7 +530,8 @@ function resourceToTools(resource, config = {}) {
388
530
  readonlyFields,
389
531
  extraHideFields: config.hideFields,
390
532
  filterableFields,
391
- allowedOperators
533
+ allowedOperators,
534
+ adapterBodies
392
535
  }),
393
536
  handler: createHandler(op, controller, resource.name, resource.permissions)
394
537
  });
@@ -442,20 +585,57 @@ function buildInputSchema(op, fieldRules, opts) {
442
585
  ...opts
443
586
  });
444
587
  case "get": return { id: z.string().describe("Resource ID") };
445
- case "create": return fieldRulesToZod(fieldRules, {
446
- mode: "create",
447
- ...opts
448
- });
449
- case "update": return {
450
- id: z.string().describe("Resource ID"),
451
- ...fieldRulesToZod(fieldRules, {
452
- mode: "update",
588
+ case "create":
589
+ if (!fieldRules && opts.adapterBodies?.createBody) {
590
+ const shape = jsonSchemaToZodShape(opts.adapterBodies.createBody, "create");
591
+ if (shape) return shape;
592
+ }
593
+ return fieldRulesToZod(fieldRules, {
594
+ mode: "create",
453
595
  ...opts
454
- })
455
- };
596
+ });
597
+ case "update": {
598
+ const idShape = { id: z.string().describe("Resource ID") };
599
+ if (!fieldRules && opts.adapterBodies?.updateBody) {
600
+ const shape = jsonSchemaToZodShape(opts.adapterBodies.updateBody, "update");
601
+ if (shape) return {
602
+ ...idShape,
603
+ ...shape
604
+ };
605
+ }
606
+ return {
607
+ ...idShape,
608
+ ...fieldRulesToZod(fieldRules, {
609
+ mode: "update",
610
+ ...opts
611
+ })
612
+ };
613
+ }
456
614
  case "delete": return { id: z.string().describe("Resource ID") };
457
615
  }
458
616
  }
617
+ /**
618
+ * Pull the adapter's `createBody` / `updateBody` schemas, if any.
619
+ * Returns `undefined` when the adapter doesn't generate schemas or throws.
620
+ */
621
+ function getAdapterBodies(resource) {
622
+ const adapter = resource.adapter;
623
+ if (!adapter || typeof adapter.generateSchemas !== "function") return void 0;
624
+ try {
625
+ const generated = adapter.generateSchemas(resource.schemaOptions, {
626
+ idField: resource.idField,
627
+ resourceName: resource.name
628
+ });
629
+ if (!generated || typeof generated !== "object") return void 0;
630
+ const schemas = generated;
631
+ return {
632
+ createBody: schemas.createBody,
633
+ updateBody: schemas.updateBody
634
+ };
635
+ } catch {
636
+ return;
637
+ }
638
+ }
459
639
  function createHandler(op, controller, resourceName, permissions) {
460
640
  const ctrl = controller;
461
641
  return async (input, ctx) => {
@@ -550,6 +730,63 @@ async function evaluatePermission(check, session, resource, action, input) {
550
730
  if (!permResult.granted) return false;
551
731
  return permResult.filters ?? null;
552
732
  }
733
+ /**
734
+ * Derive a fieldRules-shaped object from the adapter's auto-generated body
735
+ * schemas. Used as a fallback when the resource doesn't supply explicit
736
+ * fieldRules — this lets MCP create/update tools accept the same body fields
737
+ * that the REST routes already accept.
738
+ *
739
+ * Returns `undefined` if no usable schema can be extracted, in which case
740
+ * `fieldRulesToZod` falls back to its own behavior (empty shape).
741
+ */
742
+ function deriveFieldRulesFromAdapter(resource) {
743
+ const adapter = resource.adapter;
744
+ if (!adapter || typeof adapter.generateSchemas !== "function") return void 0;
745
+ let generated;
746
+ try {
747
+ generated = adapter.generateSchemas(resource.schemaOptions, {
748
+ idField: resource.idField,
749
+ resourceName: resource.name
750
+ });
751
+ } catch {
752
+ return;
753
+ }
754
+ if (!generated || typeof generated !== "object") return void 0;
755
+ const schemas = generated;
756
+ const createBody = schemas.createBody;
757
+ const updateBody = schemas.updateBody;
758
+ const properties = createBody?.properties ?? updateBody?.properties;
759
+ if (!properties || typeof properties !== "object") return void 0;
760
+ const requiredSet = new Set(createBody?.required ?? []);
761
+ const rules = {};
762
+ for (const [name, propSchema] of Object.entries(properties)) {
763
+ if (!propSchema || typeof propSchema !== "object") continue;
764
+ const prop = propSchema;
765
+ const rawType = prop.type;
766
+ const rule = { type: mapJsonSchemaTypeToArcType((Array.isArray(rawType) ? rawType.filter((t) => typeof t === "string") : typeof rawType === "string" ? [rawType] : [])[0]) };
767
+ if (requiredSet.has(name)) rule.required = true;
768
+ if (typeof prop.description === "string") rule.description = prop.description;
769
+ if (Array.isArray(prop.enum)) rule.enum = prop.enum.filter((v) => typeof v === "string");
770
+ if (typeof prop.minLength === "number") rule.minLength = prop.minLength;
771
+ if (typeof prop.maxLength === "number") rule.maxLength = prop.maxLength;
772
+ if (typeof prop.minimum === "number") rule.min = prop.minimum;
773
+ if (typeof prop.maximum === "number") rule.max = prop.maximum;
774
+ if (typeof prop.pattern === "string") rule.pattern = prop.pattern;
775
+ rules[name] = rule;
776
+ }
777
+ return Object.keys(rules).length > 0 ? rules : void 0;
778
+ }
779
+ function mapJsonSchemaTypeToArcType(jsonType) {
780
+ switch (jsonType) {
781
+ case "string": return "string";
782
+ case "number":
783
+ case "integer": return "number";
784
+ case "boolean": return "boolean";
785
+ case "array": return "array";
786
+ case "object": return "object";
787
+ default: return "string";
788
+ }
789
+ }
553
790
  function toCallToolResult(result) {
554
791
  if (!result.success) return {
555
792
  content: [{
@@ -1,5 +1,5 @@
1
- import { Bt as ResourceDefinition, Ht as CrudRepository, l as AnyRecord } from "../interface-CrN45qz1.mjs";
2
- import { d as ResourceLike, r as CreateAppOptions } from "../types-C1Z28coa.mjs";
1
+ import { Ut as CrudRepository, Vt as ResourceDefinition, u as AnyRecord } from "../interface-DYH8AXGe.mjs";
2
+ import { d as ResourceLike, r as CreateAppOptions } from "../types-By-5mIfn.mjs";
3
3
  import Fastify, { FastifyInstance, FastifyServerOptions } from "fastify";
4
4
  import { Connection } from "mongoose";
5
5
  import { Mock } from "vitest";
@@ -1,4 +1,4 @@
1
1
  import { _ as isMember, a as AUTHENTICATED_SCOPE, d as getTeamId, g as isElevated, h as isAuthenticated, l as getOrgId, m as hasOrgAccess, n as ElevationOptions, o as PUBLIC_SCOPE, s as RequestScope, t as ElevationEvent, u as getOrgRoles } from "../elevation-C_taLQrM.mjs";
2
- import { $ as RegistryEntry, A as GracefulShutdownOptions, B as LookupOption, C as CrudSchemas, D as FastifyWithAuth, E as FastifyRequestExtras, F as InferResourceDoc, Ft as IControllerResponse, G as OwnershipCheck, Gt as PaginationParams, H as MiddlewareHandler, Ht as CrudRepository, I as IntrospectionData, It as IRequestContext, J as PresetFunction, K as ParsedQuery, Kt as QueryOptions, L as IntrospectionPluginOptions, Lt as RouteHandler, M as HealthOptions, Mt as ControllerLike, N as InferAdapterDoc, Nt as FastifyHandler, O as FastifyWithDecorators, P as InferDocType, Pt as IController, Q as RateLimitConfig, R as JWTPayload, S as CrudRouterOptions, St as getUserId, T as EventsDecorator, U as ObjectId, Ut as InferDoc, V as MiddlewareConfig, W as OpenApiSchemas, Wt as PaginatedResult, X as PresetResult, Y as PresetHook, Z as QueryParserInterface, _ as AuthenticatorContext, _t as UserLike, at as ResourceConfig, b as CrudController, bt as ValidationResult, c as AdditionalRoute, ct as ResourceMetadata, d as ArcDecorator, dt as RouteSchemaOptions, et as RegistryStats, f as ArcInternalMetadata, ft as ServiceContext, g as Authenticator, gt as TypedResourceConfig, h as AuthPluginOptions, ht as TypedRepository, it as ResourceCacheConfig, j as HealthCheck, jt as ControllerHandler, k as FieldRule, l as AnyRecord, lt as ResourcePermissions, m as AuthHelpers, mt as TypedController, nt as RequestIdOptions, ot as ResourceHookContext, p as ArcRequest, pt as TokenPair, q as PopulateOption, rt as RequestWithExtras, st as ResourceHooks, tt as RequestContext, u as ApiResponse, ut as RouteHandlerMethod, v as ConfigError, vt as UserOrganization, w as EventDefinition, wt as BaseControllerOptions, x as CrudRouteKey, xt as envelope, y as ControllerQueryOptions, yt as ValidateOptions, z as JwtContext } from "../interface-CrN45qz1.mjs";
2
+ import { $ as RateLimitConfig, A as FieldRule, B as JwtContext, C as CrudRouterOptions, Ct as getUserId, D as FastifyRequestExtras, E as EventsDecorator, F as InferDocType, Ft as IController, G as OpenApiSchemas, Gt as PaginatedResult, H as MiddlewareConfig, I as InferResourceDoc, It as IControllerResponse, J as PopulateOption, K as OwnershipCheck, Kt as PaginationParams, L as IntrospectionData, Lt as IRequestContext, M as HealthCheck, Mt as ControllerHandler, N as HealthOptions, Nt as ControllerLike, O as FastifyWithAuth, P as InferAdapterDoc, Pt as FastifyHandler, Q as QueryParserInterface, R as IntrospectionPluginOptions, Rt as RouteHandler, S as CrudRouteKey, St as envelope, T as EventDefinition, Tt as BaseControllerOptions, U as MiddlewareHandler, Ut as CrudRepository, V as LookupOption, W as ObjectId, Wt as InferDoc, X as PresetHook, Y as PresetFunction, Z as PresetResult, _ as Authenticator, _t as TypedResourceConfig, at as ResourceCacheConfig, b as ControllerQueryOptions, bt as ValidateOptions, ct as ResourceHooks, d as ApiResponse, dt as RouteHandlerMethod, et as RegistryEntry, f as ArcDecorator, ft as RouteSchemaOptions, g as AuthPluginOptions, gt as TypedRepository, h as AuthHelpers, ht as TypedController, it as RequestWithExtras, j as GracefulShutdownOptions, k as FastifyWithDecorators, l as AdditionalRoute, lt as ResourceMetadata, m as ArcRequest, mt as TokenPair, nt as RequestContext, ot as ResourceConfig, p as ArcInternalMetadata, pt as ServiceContext, q as ParsedQuery, qt as QueryOptions, rt as RequestIdOptions, st as ResourceHookContext, tt as RegistryStats, u as AnyRecord, ut as ResourcePermissions, v as AuthenticatorContext, vt as UserLike, w as CrudSchemas, x as CrudController, xt as ValidationResult, y as ConfigError, yt as UserOrganization, z as JWTPayload } from "../interface-DYH8AXGe.mjs";
3
3
  import { i as UserBase, n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "../types-BNUccdcf.mjs";
4
4
  export { AUTHENTICATED_SCOPE, AdditionalRoute, AnyRecord, ApiResponse, ArcDecorator, ArcInternalMetadata, ArcRequest, AuthHelpers, AuthPluginOptions, Authenticator, AuthenticatorContext, BaseControllerOptions, ConfigError, ControllerHandler, ControllerLike, ControllerQueryOptions, CrudController, CrudRepository, CrudRouteKey, CrudRouterOptions, CrudSchemas, ElevationEvent, ElevationOptions, EventDefinition, EventsDecorator, FastifyHandler, FastifyRequestExtras, FastifyWithAuth, FastifyWithDecorators, FieldRule, GracefulShutdownOptions, HealthCheck, HealthOptions, IController, IControllerResponse, IRequestContext, InferAdapterDoc, InferDoc, InferDocType, InferResourceDoc, IntrospectionData, IntrospectionPluginOptions, JWTPayload, JwtContext, LookupOption, MiddlewareConfig, MiddlewareHandler, ObjectId, OpenApiSchemas, OwnershipCheck, PUBLIC_SCOPE, PaginatedResult, PaginationParams, ParsedQuery, PermissionCheck, PermissionContext, PermissionResult, PopulateOption, PresetFunction, PresetHook, PresetResult, QueryOptions, QueryParserInterface, RateLimitConfig, RegistryEntry, RegistryStats, RequestContext, RequestIdOptions, RequestScope, RequestWithExtras, ResourceCacheConfig, ResourceConfig, ResourceHookContext, ResourceHooks, ResourceMetadata, ResourcePermissions, RouteHandler, RouteHandlerMethod, RouteSchemaOptions, ServiceContext, TokenPair, TypedController, TypedRepository, TypedResourceConfig, UserBase, UserLike, UserOrganization, ValidateOptions, ValidationResult, envelope, getOrgId, getOrgRoles, getTeamId, getUserId, hasOrgAccess, isAuthenticated, isElevated, isMember };
@@ -1,4 +1,4 @@
1
- import { Bt as ResourceDefinition } from "./interface-CrN45qz1.mjs";
1
+ import { Vt as ResourceDefinition } from "./interface-DYH8AXGe.mjs";
2
2
  import { z } from "zod";
3
3
 
4
4
  //#region src/integrations/mcp/types.d.ts
@@ -1,5 +1,5 @@
1
1
  import { n as ElevationOptions } from "./elevation-C_taLQrM.mjs";
2
- import { g as Authenticator } from "./interface-CrN45qz1.mjs";
2
+ import { _ as Authenticator } from "./interface-DYH8AXGe.mjs";
3
3
  import { t as ExternalOpenApiPaths } from "./externalPaths-DpO-s7r8.mjs";
4
4
  import { i as CacheStore } from "./interface-D_BWALyZ.mjs";
5
5
  import { r as QueryCachePluginOptions } from "./queryCachePlugin-DcmETvcB.mjs";
@@ -1,4 +1,4 @@
1
- import { K as ParsedQuery, W as OpenApiSchemas, Z as QueryParserInterface, l as AnyRecord } from "../interface-CrN45qz1.mjs";
1
+ import { G as OpenApiSchemas, Q as QueryParserInterface, q as ParsedQuery, u as AnyRecord } from "../interface-DYH8AXGe.mjs";
2
2
  import { a as NotFoundError, c as RateLimitError, d as ValidationError, i as ForbiddenError, l as ServiceUnavailableError, m as isArcError, n as ConflictError, o as OrgAccessDeniedError, p as createError, r as ErrorDetails, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-CcVbl1-T.mjs";
3
3
  import { a as CircuitBreakerStats, c as createCircuitBreakerRegistry, i as CircuitBreakerRegistry, n as CircuitBreakerError, o as CircuitState, r as CircuitBreakerOptions, s as createCircuitBreaker, t as CircuitBreaker } from "../circuitBreaker-JP2GdJ4b.mjs";
4
4
  import { FastifyInstance } from "fastify";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc",
3
- "version": "2.6.2",
3
+ "version": "2.6.3",
4
4
  "description": "Resource-oriented backend framework for Fastify — clean, minimal, powerful, tree-shakable",
5
5
  "type": "module",
6
6
  "exports": {
@@ -222,31 +222,31 @@
222
222
  "peerDependencies": {
223
223
  "@classytic/mongokit": ">=3.5.0",
224
224
  "@classytic/streamline": ">=2.0.0",
225
- "@fastify/cors": "^11.0.0",
226
- "@fastify/helmet": "^13.0.0",
227
- "@fastify/jwt": "^10.0.0",
228
- "@fastify/multipart": "^9.0.0",
229
- "@fastify/rate-limit": "^10.0.0",
230
- "@fastify/sensible": "^6.0.0",
231
- "@fastify/type-provider-typebox": "^6.0.0",
232
- "@fastify/under-pressure": "^9.0.0",
233
- "@fastify/websocket": "^11.0.0",
225
+ "@fastify/cors": ">=11.0.0",
226
+ "@fastify/helmet": ">=13.0.0",
227
+ "@fastify/jwt": ">=10.0.0",
228
+ "@fastify/multipart": ">=9.0.0",
229
+ "@fastify/rate-limit": ">=10.0.0",
230
+ "@fastify/sensible": ">=6.0.0",
231
+ "@fastify/type-provider-typebox": ">=6.0.0",
232
+ "@fastify/under-pressure": ">=9.0.0",
233
+ "@fastify/websocket": ">=11.0.0",
234
234
  "@modelcontextprotocol/sdk": ">=1.28.0",
235
235
  "@opentelemetry/auto-instrumentations-node": ">=0.40.0",
236
236
  "@opentelemetry/exporter-trace-otlp-http": ">=0.50.0",
237
237
  "@opentelemetry/instrumentation-http": ">=0.50.0",
238
238
  "@opentelemetry/instrumentation-mongodb": ">=0.40.0",
239
239
  "@opentelemetry/sdk-node": ">=0.50.0",
240
- "@sinclair/typebox": "^0.34.0",
240
+ "@sinclair/typebox": ">=0.34.0",
241
241
  "better-auth": ">=1.5.5",
242
- "bullmq": "^5.0.0",
243
- "fastify": "^5.7.4",
244
- "fastify-raw-body": "^5.0.0",
245
- "ioredis": "^5.0.0",
246
- "mongodb": "^6.0.0 || ^7.0.0",
242
+ "bullmq": ">=5.0.0",
243
+ "fastify": ">=5.0.0",
244
+ "fastify-raw-body": ">=5.0.0",
245
+ "ioredis": ">=5.0.0",
246
+ "mongodb": ">=6.0.0",
247
247
  "mongoose": ">=9.0.0",
248
- "pino-pretty": "^13.0.0",
249
- "zod": "^4.0.0"
248
+ "pino-pretty": ">=13.0.0",
249
+ "zod": ">=4.0.0"
250
250
  },
251
251
  "peerDependenciesMeta": {
252
252
  "@classytic/mongokit": {
@@ -220,6 +220,31 @@ When to use `tenantField: false`:
220
220
  - Cross-org reports or analytics
221
221
  - Single-tenant apps where org scoping isn't needed
222
222
 
223
+ ### idField — Custom Primary Key
224
+
225
+ Default is `'_id'`. Override for resources keyed by a business identifier (UUID, slug, `ORD-2026-0001`, `job-5219f346-a4d`, etc.).
226
+
227
+ ```typescript
228
+ defineResource({
229
+ name: 'job',
230
+ adapter: createMongooseAdapter(JobModel, jobRepository),
231
+ idField: 'jobId', // ← one line
232
+ });
233
+
234
+ // GET /jobs/job-5219f346-a4d → controller runs { jobId: 'job-5219f346-a4d' }
235
+ // GET /jobs/<uuid> → accepted (no ObjectId pattern enforcement)
236
+ ```
237
+
238
+ Changes all three layers:
239
+ - **Fastify AJV** — strips any ObjectId pattern from `params.id` so custom formats aren't pre-rejected
240
+ - **BaseController** — `get`/`update`/`delete` query by `{ [idField]: id }` (merged with tenant + policy filters)
241
+ - **OpenAPI docs** — `spec.paths['/jobs/{id}']` emits a plain string `id` with description
242
+ - **MCP tools** — auto-generated CRUD tools use `idField` transparently
243
+
244
+ URL path segment stays `:id` (not `:jobId`) — clients send the ID value, Arc maps it server-side. User-provided `openApiSchemas.params` still overrides everything.
245
+
246
+ For custom adapters, honor the new `AdapterSchemaContext` passed to `generateSchemas(options, context?)` to emit the right `params.id` pattern from the start. Legacy adapters still work — Arc's safety net strips mismatched ObjectId patterns automatically.
247
+
223
248
  ## QueryCache
224
249
 
225
250
  TanStack Query-inspired server cache with stale-while-revalidate and auto-invalidation on mutations.