@classytic/arc 2.6.1 → 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 (48) hide show
  1. package/README.md +48 -2
  2. package/dist/{BaseController-AbbRx3e0.mjs → BaseController-DzRtluEF.mjs} +88 -8
  3. package/dist/{ResourceRegistry-DeCIFlix.mjs → ResourceRegistry-C6ngvOnn.mjs} +1 -0
  4. package/dist/adapters/index.d.mts +2 -2
  5. package/dist/adapters/index.mjs +1 -1
  6. package/dist/{adapters-CTn28N4y.mjs → adapters-gM-WYjNe.mjs} +6 -4
  7. package/dist/audit/index.d.mts +31 -5
  8. package/dist/audit/index.mjs +21 -3
  9. package/dist/auth/index.d.mts +1 -1
  10. package/dist/cli/commands/docs.mjs +1 -1
  11. package/dist/cli/commands/introspect.mjs +1 -1
  12. package/dist/core/index.d.mts +2 -2
  13. package/dist/core/index.mjs +2 -2
  14. package/dist/{createApp-Bol7DLUf.mjs → createApp-D2w0LdYJ.mjs} +27 -11
  15. package/dist/{defineResource-bVKHjQzE.mjs → defineResource-wWMBB4GP.mjs} +48 -30
  16. package/dist/docs/index.d.mts +1 -1
  17. package/dist/dynamic/index.d.mts +1 -1
  18. package/dist/dynamic/index.mjs +1 -1
  19. package/dist/factory/index.d.mts +1 -1
  20. package/dist/factory/index.mjs +31 -15
  21. package/dist/hooks/index.d.mts +1 -1
  22. package/dist/{index-BIsZ_su5.d.mts → index-CHeJa4Zd.d.mts} +3 -3
  23. package/dist/{index-Cb3gtbg7.d.mts → index-gz6iuzCp.d.mts} +1 -1
  24. package/dist/index.d.mts +3 -3
  25. package/dist/index.mjs +4 -4
  26. package/dist/integrations/index.d.mts +1 -1
  27. package/dist/integrations/mcp/index.d.mts +2 -2
  28. package/dist/integrations/mcp/index.mjs +1 -1
  29. package/dist/integrations/mcp/testing.d.mts +1 -1
  30. package/dist/integrations/mcp/testing.mjs +1 -1
  31. package/dist/{interface-DDW43OmS.d.mts → interface-DYH8AXGe.d.mts} +89 -4
  32. package/dist/org/index.d.mts +1 -1
  33. package/dist/plugins/index.d.mts +1 -1
  34. package/dist/plugins/index.mjs +1 -1
  35. package/dist/plugins/tracing-entry.mjs +1 -1
  36. package/dist/presets/index.d.mts +1 -1
  37. package/dist/presets/multiTenant.d.mts +1 -1
  38. package/dist/registry/index.d.mts +1 -1
  39. package/dist/registry/index.mjs +1 -1
  40. package/dist/{resourceToTools-DH3c3e-T.mjs → resourceToTools-nCJWnG1r.mjs} +250 -13
  41. package/dist/testing/index.d.mts +26 -3
  42. package/dist/testing/index.mjs +46 -2
  43. package/dist/types/index.d.mts +1 -1
  44. package/dist/{types-D5rjsS_i.d.mts → types-B4_TDdPe.d.mts} +1 -1
  45. package/dist/{types-D5hJ-k_3.d.mts → types-By-5mIfn.d.mts} +7 -1
  46. package/dist/utils/index.d.mts +1 -1
  47. package/package.json +18 -18
  48. package/skills/arc/SKILL.md +80 -8
package/README.md CHANGED
@@ -34,7 +34,7 @@ Three ways to register resources:
34
34
 
35
35
  ```typescript
36
36
  // Auto-discover from directory (recommended)
37
- resources: await loadResources('./src/resources'),
37
+ resources: await loadResources(import.meta.url), // dev/prod parity
38
38
 
39
39
  // Explicit array
40
40
  resources: [productResource, orderResource],
@@ -43,7 +43,42 @@ resources: [productResource, orderResource],
43
43
  plugins: async (f) => { await f.register(productResource.toPlugin()); },
44
44
  ```
45
45
 
46
- > **Note:** `loadResources()` works with standard relative imports and Node.js `#` subpath imports (`package.json` `imports` field). It does **not** support tsconfig path aliases (`@/*`, `~/`) — use explicit `resources: [...]` instead.
46
+ `loadResources()` discovers `default` exports, `export const resource`, OR any named export with `toPlugin()` (e.g. `export const userResource`). Per-resource opt-out of `resourcePrefix` via `skipGlobalPrefix: true` for webhooks/admin routes.
47
+
48
+ > **Import compatibility:** Works with relative imports and Node.js `#` subpath imports. Does **not** support tsconfig path aliases (`@/*`, `~/`) — use explicit `resources: [...]` instead.
49
+
50
+ ## Boot Sequence
51
+
52
+ ```typescript
53
+ const app = await createApp({
54
+ resourcePrefix: '/api/v1',
55
+ plugins: async (f) => { await connectDB(); }, // 1. infra (DB, docs)
56
+ bootstrap: [inventoryInit, accountingInit], // 2. domain init
57
+ resources: await loadResources(import.meta.url), // 3. routes
58
+ afterResources: async (f) => { subscribeEvents(f); }, // 4. post-wiring
59
+ onReady: async (f) => { logger.info('ready'); },
60
+ });
61
+ ```
62
+
63
+ ## Audit (per-resource opt-in)
64
+
65
+ Clean DX without growing exclude lists:
66
+
67
+ ```typescript
68
+ // app.ts — register once with perResource mode
69
+ await fastify.register(auditPlugin, { autoAudit: { perResource: true } });
70
+
71
+ // order.resource.ts — opt in
72
+ defineResource({ name: 'order', audit: true });
73
+
74
+ // payment.resource.ts — only audit deletes
75
+ defineResource({ name: 'payment', audit: { operations: ['delete'] } });
76
+
77
+ // Manual logging from MCP tools or custom routes
78
+ app.post('/orders/:id/refund', async (req) => {
79
+ await app.audit.custom('order', req.params.id, 'refund', { reason }, { user });
80
+ });
81
+ ```
47
82
 
48
83
  ## defineResource
49
84
 
@@ -73,6 +108,17 @@ const productResource = defineResource({
73
108
  // Plus preset routes: GET /deleted, POST /:id/restore, GET /slug/:slug
74
109
  ```
75
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
+
76
122
  ## Authentication
77
123
 
78
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
  }
@@ -51,6 +51,7 @@ var ResourceRegistry = class {
51
51
  fieldPermissions: extractFieldPermissions(resource.fields),
52
52
  pipelineSteps: extractPipelineSteps(resource.pipe),
53
53
  rateLimit: resource.rateLimit,
54
+ audit: resource.audit,
54
55
  plugin: resource.toPlugin()
55
56
  };
56
57
  this._resources.set(resource.name, entry);
@@ -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-DDW43OmS.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-BIsZ_su5.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",
@@ -19,14 +19,40 @@ interface AuditPluginOptions {
19
19
  * Automatically audit CRUD operations via the hook system (default: true when enabled).
20
20
  * When enabled, create/update/delete operations are auto-logged without manual calls.
21
21
  *
22
- * - `true`: Auto-audit all CRUD operations on all resources
23
- * - `{ operations: ['create', 'delete'] }`: Only auto-audit specific operations
24
- * - `{ exclude: ['health', 'metrics'] }`: Skip specific resources
25
- * - `false`: Disable auto-audit (manual calls only)
22
+ * **Three opt-in patterns** pick the one that matches your app:
23
+ *
24
+ * 1. **Per-resource opt-in (recommended for most apps)** set `audit: true` on each
25
+ * resource. Audit only fires for those resources. No global `include`/`exclude` needed.
26
+ * ```ts
27
+ * defineResource({ name: 'order', audit: true });
28
+ * // auditPlugin auto-detects which resources opted in
29
+ * ```
30
+ *
31
+ * 2. **Allowlist mode** — set `include: ['order', 'invoice']` for centralized config.
32
+ * Only listed resources are audited.
33
+ *
34
+ * 3. **Denylist mode** — set `exclude: ['health', 'metrics']` to audit everything except
35
+ * listed resources. Use sparingly — leads to growing exclude lists.
36
+ *
37
+ * Default behavior (`autoAudit: true`): denylist mode with no exclusions (audit everything).
38
+ * For most apps, switching to per-resource opt-in is cleaner.
39
+ *
40
+ * - `true`: Audit all CRUD operations on all resources (legacy default)
41
+ * - `{ operations: ['create', 'delete'] }`: Only specific operations
42
+ * - `{ include: ['order'] }`: Allowlist — only listed resources
43
+ * - `{ exclude: ['health'] }`: Denylist — all except listed
44
+ * - `{ perResource: true }`: Only resources with `audit: true` in their definition
45
+ * - `false`: Disable auto-audit (manual `fastify.audit.*()` calls only)
26
46
  */
27
47
  autoAudit?: boolean | {
28
- operations?: ("create" | "update" | "delete")[];
48
+ operations?: ("create" | "update" | "delete")[]; /** Allowlist — only listed resources are audited (mutually exclusive with exclude) */
49
+ include?: string[]; /** Denylist — audit everything except listed resources */
29
50
  exclude?: string[];
51
+ /**
52
+ * Per-resource opt-in mode: only audit resources with `audit: true` in their
53
+ * `defineResource()` config. The cleanest pattern for most apps.
54
+ */
55
+ perResource?: boolean;
30
56
  };
31
57
  }
32
58
  declare module "fastify" {
@@ -180,16 +180,34 @@ const auditPlugin = async (fastify, opts = {}) => {
180
180
  "update",
181
181
  "delete"
182
182
  ];
183
- const ops = typeof autoAuditConfig === "object" ? autoAuditConfig.operations ?? defaultOps : defaultOps;
184
- const excludeResources = new Set(typeof autoAuditConfig === "object" ? autoAuditConfig.exclude ?? [] : []);
183
+ const isObj = typeof autoAuditConfig === "object";
184
+ const ops = isObj ? autoAuditConfig.operations ?? defaultOps : defaultOps;
185
+ const includeResources = isObj && autoAuditConfig.include ? new Set(autoAuditConfig.include) : null;
186
+ const excludeResources = new Set(isObj ? autoAuditConfig.exclude ?? [] : []);
187
+ const perResourceMode = isObj ? autoAuditConfig.perResource === true : false;
188
+ if (includeResources && excludeResources.size > 0) fastify.log?.warn?.("Audit autoAudit: both 'include' and 'exclude' specified. Using 'include' (allowlist wins).");
185
189
  fastify.addHook("onReady", async () => {
186
190
  const arc = "arc" in fastify ? fastify.arc : void 0;
187
191
  if (!arc?.hooks) {
188
192
  fastify.log?.debug?.("Auto-audit skipped: arc-core plugin not registered");
189
193
  return;
190
194
  }
195
+ const optedInResources = /* @__PURE__ */ new Set();
196
+ const operationsByResource = /* @__PURE__ */ new Map();
197
+ if (perResourceMode && arc.registry) for (const entry of arc.registry.getAll()) {
198
+ const auditFlag = entry.audit;
199
+ if (!auditFlag) continue;
200
+ optedInResources.add(entry.name);
201
+ if (typeof auditFlag === "object" && auditFlag.operations) operationsByResource.set(entry.name, auditFlag.operations);
202
+ }
191
203
  for (const op of ops) arc.hooks.after("*", op, async (ctx) => {
192
- if (excludeResources.has(ctx.resource)) return;
204
+ if (perResourceMode) {
205
+ if (!optedInResources.has(ctx.resource)) return;
206
+ const allowedOps = operationsByResource.get(ctx.resource);
207
+ if (allowedOps && !allowedOps.includes(op)) return;
208
+ } else if (includeResources) {
209
+ if (!includeResources.has(ctx.resource)) return;
210
+ } else if (excludeResources.has(ctx.resource)) return;
193
211
  const docId = autoAuditExtractId(ctx.result);
194
212
  const scope = ctx.context?._scope;
195
213
  const auditCtx = {
@@ -1,4 +1,4 @@
1
- import { h as AuthPluginOptions, m as AuthHelpers } from "../interface-DDW43OmS.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,4 +1,4 @@
1
- import { t as ResourceRegistry } from "../../ResourceRegistry-DeCIFlix.mjs";
1
+ import { t as ResourceRegistry } from "../../ResourceRegistry-C6ngvOnn.mjs";
2
2
  import { t as buildOpenApiSpec } from "../../openapi-CBmZ6EQN.mjs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
@@ -1,4 +1,4 @@
1
- import { t as ResourceRegistry } from "../../ResourceRegistry-DeCIFlix.mjs";
1
+ import { t as ResourceRegistry } from "../../ResourceRegistry-C6ngvOnn.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 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-DDW43OmS.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-Cb3gtbg7.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-bVKHjQzE.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 };
@@ -66,20 +66,36 @@ const productionPreset = {
66
66
  }
67
67
  };
68
68
  /**
69
+ * Try to detect if `pino-pretty` is installed (devDep). Returns the transport
70
+ * config if available, or falls back to plain JSON logging. This prevents the
71
+ * common "pino-pretty not found" crash in production when someone uses the
72
+ * development preset by mistake (or via NODE_ENV-based preset selection).
73
+ */
74
+ function devLoggerConfig() {
75
+ try {
76
+ const req = eval("require") ?? null;
77
+ if (req?.resolve) {
78
+ req.resolve("pino-pretty");
79
+ return {
80
+ level: "debug",
81
+ transport: {
82
+ target: "pino-pretty",
83
+ options: {
84
+ colorize: true,
85
+ translateTime: "SYS:HH:MM:ss",
86
+ ignore: "pid,hostname"
87
+ }
88
+ }
89
+ };
90
+ }
91
+ } catch {}
92
+ return { level: "debug" };
93
+ }
94
+ /**
69
95
  * Development preset - relaxed security, verbose logging
70
96
  */
71
97
  const developmentPreset = {
72
- logger: {
73
- level: "debug",
74
- transport: {
75
- target: "pino-pretty",
76
- options: {
77
- colorize: true,
78
- translateTime: "SYS:HH:MM:ss",
79
- ignore: "pid,hostname"
80
- }
81
- }
82
- },
98
+ logger: devLoggerConfig(),
83
99
  trustProxy: true,
84
100
  helmet: { contentSecurityPolicy: false },
85
101
  cors: {
@@ -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";
@@ -1013,19 +1013,52 @@ function defineResource(config) {
1013
1013
  resource._pendingHooks.push(...inlineHooks);
1014
1014
  }
1015
1015
  if (!config.skipRegistry) try {
1016
- let openApiSchemas = config.openApiSchemas;
1017
- if (!openApiSchemas && config.adapter?.generateSchemas) {
1018
- const generated = config.adapter.generateSchemas(config.schemaOptions);
1016
+ let openApiSchemas;
1017
+ if (config.adapter?.generateSchemas) {
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
- if (!openApiSchemas?.listQuery && queryParser?.getQuerySchema) {
1051
+ if (queryParser?.getQuerySchema) {
1023
1052
  const querySchema = queryParser.getQuerySchema();
1024
1053
  if (querySchema) openApiSchemas = {
1025
1054
  ...openApiSchemas,
1026
1055
  listQuery: querySchema
1027
1056
  };
1028
1057
  }
1058
+ if (config.openApiSchemas) openApiSchemas = {
1059
+ ...openApiSchemas,
1060
+ ...config.openApiSchemas
1061
+ };
1029
1062
  if (openApiSchemas) openApiSchemas = convertOpenApiSchemas(openApiSchemas);
1030
1063
  resource._registryMeta = {
1031
1064
  module: config.module,
@@ -1050,6 +1083,7 @@ var ResourceDefinition = class {
1050
1083
  disabledRoutes;
1051
1084
  events;
1052
1085
  rateLimit;
1086
+ audit;
1053
1087
  updateMethod;
1054
1088
  pipe;
1055
1089
  fields;
@@ -1078,6 +1112,7 @@ var ResourceDefinition = class {
1078
1112
  this.disabledRoutes = config.disabledRoutes ?? [];
1079
1113
  this.events = config.events ?? {};
1080
1114
  this.rateLimit = config.rateLimit;
1115
+ this.audit = config.audit;
1081
1116
  this.updateMethod = config.updateMethod;
1082
1117
  this.pipe = config.pipe;
1083
1118
  this.fields = config.fields;
@@ -1150,7 +1185,7 @@ var ResourceDefinition = class {
1150
1185
  const openApi = self._registryMeta?.openApiSchemas;
1151
1186
  if (openApi && (!self.customSchemas || Object.keys(self.customSchemas).length === 0)) {
1152
1187
  const generated = {};
1153
- const { createBody, updateBody, params, response } = openApi;
1188
+ const { createBody, updateBody, params } = openApi;
1154
1189
  const safeBody = (schema) => {
1155
1190
  if (schema && typeof schema === "object" && schema.type === "object") return {
1156
1191
  additionalProperties: true,
@@ -1183,39 +1218,22 @@ var ResourceDefinition = class {
1183
1218
  }
1184
1219
  const listQuerySchema = self._registryMeta?.openApiSchemas?.listQuery;
1185
1220
  if (listQuerySchema) {
1186
- const KEEP_TYPE = new Set([
1221
+ const KEEP_AS_IS = new Set([
1187
1222
  "page",
1188
1223
  "limit",
1189
1224
  "sort",
1190
1225
  "search",
1191
1226
  "select",
1192
- "after"
1193
- ]);
1194
- const TYPE_DEPENDENT = new Set([
1195
- "type",
1196
- "minimum",
1197
- "maximum",
1198
- "minLength",
1199
- "maxLength",
1200
- "pattern",
1201
- "format",
1202
- "exclusiveMinimum",
1203
- "exclusiveMaximum",
1204
- "multipleOf",
1205
- "minItems",
1206
- "maxItems",
1207
- "uniqueItems"
1227
+ "after",
1228
+ "populate",
1229
+ "lookup",
1230
+ "aggregate"
1208
1231
  ]);
1209
1232
  const props = listQuerySchema.properties;
1210
1233
  const normalizedProps = props ? { ...props } : void 0;
1211
1234
  if (normalizedProps) for (const key of Object.keys(normalizedProps)) {
1212
- if (KEEP_TYPE.has(key)) continue;
1213
- const prop = normalizedProps[key];
1214
- if (prop && typeof prop === "object" && "type" in prop) {
1215
- const cleaned = {};
1216
- for (const [k, v] of Object.entries(prop)) if (!TYPE_DEPENDENT.has(k)) cleaned[k] = v;
1217
- normalizedProps[key] = Object.keys(cleaned).length > 0 ? cleaned : {};
1218
- }
1235
+ if (KEEP_AS_IS.has(key)) continue;
1236
+ normalizedProps[key] = {};
1219
1237
  }
1220
1238
  const normalizedSchema = {
1221
1239
  ...listQuerySchema,
@@ -1,4 +1,4 @@
1
- import { $ as RegistryEntry } from "../interface-DDW43OmS.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-DDW43OmS.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-bVKHjQzE.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-D5hJ-k_3.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