@classytic/arc 2.1.3 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -326,11 +326,12 @@ import { tracingPlugin } from '@classytic/arc/plugins/tracing';
326
326
  ## CLI
327
327
 
328
328
  ```bash
329
- arc init my-api --mongokit --better-auth --ts # Scaffold project
330
- arc generate resource product # Generate resource files
331
- arc docs ./openapi.json --entry ./dist/index.js # Export OpenAPI
332
- arc introspect --entry ./dist/index.js # Show resources
333
- arc doctor # Health check
329
+ npx @classytic/arc init my-api --mongokit --better-auth --ts # Scaffold project
330
+ npx @classytic/arc generate resource product # Generate resource files
331
+ npx @classytic/arc describe ./dist/index.js # Resource metadata (JSON)
332
+ npx @classytic/arc docs ./openapi.json --entry ./dist/index.js # Export OpenAPI
333
+ npx @classytic/arc introspect --entry ./dist/index.js # Show resources
334
+ npx @classytic/arc doctor # Health check
334
335
  ```
335
336
 
336
337
  ## Subpath Imports
package/bin/arc.js CHANGED
@@ -221,6 +221,7 @@ function parseInitOptions(rawArgs) {
221
221
  const opts = {
222
222
  name: undefined,
223
223
  adapter: undefined,
224
+ auth: undefined,
224
225
  tenant: undefined,
225
226
  typescript: undefined,
226
227
  edge: undefined,
@@ -1,5 +1,5 @@
1
1
  import "../elevation-DGo5shaX.mjs";
2
- import { a as RepositoryLike, i as RelationMetadata, n as DataAdapter, o as SchemaMetadata, r as FieldMetadata, s as ValidationResult, t as AdapterFactory } from "../interface-e9XfSsUV.mjs";
2
+ import { a as RepositoryLike, i as RelationMetadata, n as DataAdapter, o as SchemaMetadata, r as FieldMetadata, s as ValidationResult, t as AdapterFactory } from "../interface-Dm4-jnia.mjs";
3
3
  import "../types-RLkFVgaw.mjs";
4
- 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 "../prisma-C3iornoK.mjs";
4
+ 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 "../prisma-Bi9nxirN.mjs";
5
5
  export { type AdapterFactory, type DataAdapter, type FieldMetadata, MongooseAdapter, type MongooseAdapterOptions, PrismaAdapter, type PrismaAdapterOptions, type PrismaQueryOptions, PrismaQueryParser, type PrismaQueryParserOptions, type RelationMetadata, type RepositoryLike, type SchemaMetadata, type ValidationResult, createMongooseAdapter, createPrismaAdapter };
@@ -1,5 +1,5 @@
1
1
  import "../elevation-DGo5shaX.mjs";
2
- import "../interface-e9XfSsUV.mjs";
2
+ import "../interface-Dm4-jnia.mjs";
3
3
  import "../types-RLkFVgaw.mjs";
4
4
  import { a as AuditContext, c as AuditStore, i as AuditAction, l as AuditStoreOptions, n as MongoAuditStoreOptions, o as AuditEntry, r as MongoConnection, s as AuditQueryOptions, u as createAuditEntry } from "../mongodb-ClykrfGo.mjs";
5
5
  import { FastifyPluginAsync } from "fastify";
@@ -1,5 +1,5 @@
1
1
  import "../elevation-DGo5shaX.mjs";
2
- import "../interface-e9XfSsUV.mjs";
2
+ import "../interface-Dm4-jnia.mjs";
3
3
  import "../types-RLkFVgaw.mjs";
4
4
  import { n as MongoAuditStoreOptions, t as MongoAuditStore } from "../mongodb-ClykrfGo.mjs";
5
5
  export { MongoAuditStore, type MongoAuditStoreOptions };
@@ -1,5 +1,5 @@
1
1
  import "../elevation-DGo5shaX.mjs";
2
- import "../interface-e9XfSsUV.mjs";
2
+ import "../interface-Dm4-jnia.mjs";
3
3
  import { t as PermissionCheck } from "../types-RLkFVgaw.mjs";
4
4
  import { AuthHelpers, AuthPluginOptions } from "../types/index.mjs";
5
5
  import { t as ExternalOpenApiPaths } from "../externalPaths-SyPF2tgK.mjs";
@@ -8,6 +8,11 @@
8
8
  * - src/resources/product/product.resource.ts
9
9
  * - src/resources/product/product.controller.ts
10
10
  * - src/resources/product/product.schemas.ts
11
+ *
12
+ * Handles kebab-case names: `arc g r org-profile` generates:
13
+ * - Class names: OrgProfile, OrgProfileRepository
14
+ * - Variable names: orgProfileSchema, orgProfileRepository
15
+ * - File names: org-profile.model.ts, org-profile.repository.ts
11
16
  */
12
17
  /**
13
18
  * Generate command handler
@@ -12,6 +12,11 @@ import { join } from "node:path";
12
12
  * - src/resources/product/product.resource.ts
13
13
  * - src/resources/product/product.controller.ts
14
14
  * - src/resources/product/product.schemas.ts
15
+ *
16
+ * Handles kebab-case names: `arc g r org-profile` generates:
17
+ * - Class names: OrgProfile, OrgProfileRepository
18
+ * - Variable names: orgProfileSchema, orgProfileRepository
19
+ * - File names: org-profile.model.ts, org-profile.repository.ts
15
20
  */
16
21
  function readProjectConfig() {
17
22
  try {
@@ -24,10 +29,25 @@ function readProjectConfig() {
24
29
  function isTypeScriptProject() {
25
30
  return existsSync(join(process.cwd(), "tsconfig.json"));
26
31
  }
32
+ /** Convert kebab-case to PascalCase: org-profile → OrgProfile */
33
+ function toPascalCase(name) {
34
+ return name.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
35
+ }
36
+ /** Convert PascalCase to camelCase: OrgProfile → orgProfile */
37
+ function toCamelCase(pascalName) {
38
+ return pascalName.charAt(0).toLowerCase() + pascalName.slice(1);
39
+ }
40
+ /**
41
+ * Template functions accept:
42
+ * - name: PascalCase class name (e.g., OrgProfile)
43
+ * - fileName: kebab-case for file paths (e.g., org-profile)
44
+ */
27
45
  function getTemplates(ts, config = {}) {
28
46
  const isMultiTenant = config.tenant === "multi";
29
47
  return {
30
- model: (name) => `/**
48
+ model: (name, fileName) => {
49
+ const camel = toCamelCase(name);
50
+ return `/**
31
51
  * ${name} Model
32
52
  * Generated by Arc CLI
33
53
  */
@@ -44,7 +64,7 @@ export interface I${name} {
44
64
 
45
65
  export type ${name}Document = HydratedDocument<I${name}>;
46
66
  ` : ""}
47
- const ${name.toLowerCase()}Schema = new Schema${ts ? `<I${name}>` : ""}(
67
+ const ${camel}Schema = new Schema${ts ? `<I${name}>` : ""}(
48
68
  {
49
69
  name: { type: String, required: true, trim: true },
50
70
  description: { type: String, trim: true },
@@ -54,13 +74,16 @@ const ${name.toLowerCase()}Schema = new Schema${ts ? `<I${name}>` : ""}(
54
74
  );
55
75
 
56
76
  // Indexes
57
- ${name.toLowerCase()}Schema.index({ name: 1 });
58
- ${name.toLowerCase()}Schema.index({ isActive: 1 });
77
+ ${camel}Schema.index({ name: 1 });
78
+ ${camel}Schema.index({ isActive: 1 });
59
79
 
60
- const ${name} = mongoose.models.${name} || mongoose.model('${name}', ${name.toLowerCase()}Schema);
80
+ const ${name} = mongoose.models.${name} || mongoose.model('${name}', ${camel}Schema);
61
81
  export default ${name};
62
- `,
63
- repository: (name) => `/**
82
+ `;
83
+ },
84
+ repository: (name, fileName) => {
85
+ const camel = toCamelCase(name);
86
+ return `/**
64
87
  * ${name} Repository
65
88
  * Generated by Arc CLI
66
89
  */
@@ -71,7 +94,7 @@ import {
71
94
  softDeletePlugin,
72
95
  mongoOperationsPlugin,
73
96
  } from '@classytic/mongokit';
74
- import ${name} from './${name.toLowerCase()}.model.js';
97
+ import ${name} from './${fileName}.model.js';
75
98
 
76
99
  class ${name}Repository extends Repository {
77
100
  constructor() {
@@ -90,11 +113,14 @@ class ${name}Repository extends Repository {
90
113
  }
91
114
  }
92
115
 
93
- const ${name.toLowerCase()}Repository = new ${name}Repository();
94
- export default ${name.toLowerCase()}Repository;
116
+ const ${camel}Repository = new ${name}Repository();
117
+ export default ${camel}Repository;
95
118
  export { ${name}Repository };
96
- `,
97
- controller: (name) => `/**
119
+ `;
120
+ },
121
+ controller: (name, fileName) => {
122
+ const camel = toCamelCase(name);
123
+ return `/**
98
124
  * ${name} Controller
99
125
  * Generated by Arc CLI
100
126
  *
@@ -103,27 +129,28 @@ export { ${name}Repository };
103
129
  */
104
130
 
105
131
  import { BaseController } from '@classytic/arc';
106
- import ${name.toLowerCase()}Repository from './${name.toLowerCase()}.repository.js';
132
+ import ${camel}Repository from './${fileName}.repository.js';
107
133
 
108
134
  class ${name}Controller extends BaseController {
109
135
  constructor() {
110
- super(${name.toLowerCase()}Repository, {
111
- resourceName: '${name.toLowerCase()}',
136
+ super(${camel}Repository, {
137
+ resourceName: '${fileName}',
112
138
  });
113
139
  }
114
140
 
115
141
  // Add custom controller methods here
116
142
  }
117
143
 
118
- const ${name.toLowerCase()}Controller = new ${name}Controller();
119
- export default ${name.toLowerCase()}Controller;
120
- `,
121
- schemas: (name) => `/**
144
+ const ${camel}Controller = new ${name}Controller();
145
+ export default ${camel}Controller;
146
+ `;
147
+ },
148
+ schemas: (name, fileName) => `/**
122
149
  * ${name} Schemas
123
150
  * Generated by Arc CLI
124
151
  */
125
152
 
126
- import ${name} from './${name.toLowerCase()}.model.js';
153
+ import ${name} from './${fileName}.model.js';
127
154
  import { buildCrudSchemasFromModel } from '@classytic/mongokit/utils';
128
155
 
129
156
  /**
@@ -145,7 +172,8 @@ const crudSchemas = buildCrudSchemasFromModel(${name}, {
145
172
 
146
173
  export default crudSchemas;
147
174
  `,
148
- resource: (name) => {
175
+ resource: (name, fileName) => {
176
+ const camel = toCamelCase(name);
149
177
  const useMongoKit = config.adapter === "mongokit" || !config.adapter;
150
178
  const queryParserImport = useMongoKit ? `\nimport { QueryParser } from '@classytic/mongokit';\n\nconst queryParser = new QueryParser();\n` : "";
151
179
  const queryParserConfig = useMongoKit ? `\n queryParser,` : "";
@@ -155,24 +183,24 @@ export default crudSchemas;
155
183
  */
156
184
 
157
185
  import { defineResource, createMongooseAdapter } from '@classytic/arc';
158
- import { requireOrgMembership, requireOrgRole } from '@classytic/arc/permissions';
159
- import ${name}${ts ? `, { type I${name} }` : ""} from './${name.toLowerCase()}.model.js';
160
- import ${name.toLowerCase()}Repository from './${name.toLowerCase()}.repository.js';${queryParserImport}
186
+ import { requireAuth, requireRoles } from '@classytic/arc/permissions';
187
+ import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
188
+ import ${camel}Repository from './${fileName}.repository.js';${queryParserImport}
161
189
 
162
- const ${name.toLowerCase()}Resource = defineResource${ts ? `<I${name}>` : ""}({
163
- name: '${name.toLowerCase()}',
164
- adapter: createMongooseAdapter(${name}, ${name.toLowerCase()}Repository),${queryParserConfig}
190
+ const ${camel}Resource = defineResource${ts ? `<I${name}>` : ""}({
191
+ name: '${fileName}',
192
+ adapter: createMongooseAdapter(${name}, ${camel}Repository),${queryParserConfig}
165
193
  presets: ['softDelete'],
166
194
  permissions: {
167
- list: requireOrgMembership(),
168
- get: requireOrgMembership(),
169
- create: requireOrgRole('admin'),
170
- update: requireOrgRole('admin'),
171
- delete: requireOrgRole('admin'),
195
+ list: requireAuth(),
196
+ get: requireAuth(),
197
+ create: requireRoles(['admin']),
198
+ update: requireRoles(['admin']),
199
+ delete: requireRoles(['admin']),
172
200
  },
173
201
  });
174
202
 
175
- export default ${name.toLowerCase()}Resource;
203
+ export default ${camel}Resource;
176
204
  ` : `/**
177
205
  * ${name} Resource
178
206
  * Generated by Arc CLI
@@ -180,12 +208,12 @@ export default ${name.toLowerCase()}Resource;
180
208
 
181
209
  import { defineResource, createMongooseAdapter } from '@classytic/arc';
182
210
  import { requireAuth, requireRoles } from '@classytic/arc/permissions';
183
- import ${name}${ts ? `, { type I${name} }` : ""} from './${name.toLowerCase()}.model.js';
184
- import ${name.toLowerCase()}Repository from './${name.toLowerCase()}.repository.js';${queryParserImport}
211
+ import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
212
+ import ${camel}Repository from './${fileName}.repository.js';${queryParserImport}
185
213
 
186
- const ${name.toLowerCase()}Resource = defineResource${ts ? `<I${name}>` : ""}({
187
- name: '${name.toLowerCase()}',
188
- adapter: createMongooseAdapter(${name}, ${name.toLowerCase()}Repository),${queryParserConfig}
214
+ const ${camel}Resource = defineResource${ts ? `<I${name}>` : ""}({
215
+ name: '${fileName}',
216
+ adapter: createMongooseAdapter(${name}, ${camel}Repository),${queryParserConfig}
189
217
  presets: ['softDelete'],
190
218
  permissions: {
191
219
  list: requireAuth(),
@@ -196,10 +224,12 @@ const ${name.toLowerCase()}Resource = defineResource${ts ? `<I${name}>` : ""}({
196
224
  },
197
225
  });
198
226
 
199
- export default ${name.toLowerCase()}Resource;
227
+ export default ${camel}Resource;
200
228
  `;
201
229
  },
202
- test: (name) => `/**
230
+ test: (name, fileName) => {
231
+ const camel = toCamelCase(name);
232
+ return `/**
203
233
  * ${name} Tests
204
234
  * Generated by Arc CLI
205
235
  */
@@ -207,7 +237,7 @@ export default ${name.toLowerCase()}Resource;
207
237
  import { describe, it, expect, beforeAll, afterAll } from 'vitest';
208
238
  import mongoose from 'mongoose';
209
239
  import { createMinimalTestApp } from '@classytic/arc/testing';
210
- ${ts ? "import type { FastifyInstance } from 'fastify';\n" : ""}import ${name.toLowerCase()}Resource from '../src/resources/${name.toLowerCase()}/${name.toLowerCase()}.resource.js';
240
+ ${ts ? "import type { FastifyInstance } from 'fastify';\n" : ""}import ${camel}Resource from '../src/resources/${fileName}/${fileName}.resource.js';
211
241
 
212
242
  describe('${name} Resource', () => {
213
243
  let app${ts ? ": FastifyInstance" : ""};
@@ -217,7 +247,7 @@ describe('${name} Resource', () => {
217
247
  await mongoose.connect(testDbUri);
218
248
 
219
249
  app = createMinimalTestApp();
220
- await app.register(${name.toLowerCase()}Resource.toPlugin());
250
+ await app.register(${camel}Resource.toPlugin());
221
251
  await app.ready();
222
252
  });
223
253
 
@@ -226,11 +256,11 @@ describe('${name} Resource', () => {
226
256
  await mongoose.connection.close();
227
257
  });
228
258
 
229
- describe('GET /${pluralize(name.toLowerCase())}', () => {
259
+ describe('GET /${pluralize(fileName)}', () => {
230
260
  it('should return a list', async () => {
231
261
  const response = await app.inject({
232
262
  method: 'GET',
233
- url: '/${pluralize(name.toLowerCase())}',
263
+ url: '/${pluralize(fileName)}',
234
264
  });
235
265
 
236
266
  expect(response.statusCode).toBe(200);
@@ -239,7 +269,8 @@ describe('${name} Resource', () => {
239
269
  });
240
270
  });
241
271
  });
242
- `
272
+ `;
273
+ }
243
274
  };
244
275
  }
245
276
  /**
@@ -249,7 +280,7 @@ async function generate(type, args) {
249
280
  if (!type) throw new Error("Missing type argument\nUsage: arc generate <resource|controller|model|repository|schemas> <name>");
250
281
  const [name] = args;
251
282
  if (!name) throw new Error("Missing name argument\nUsage: arc generate <type> <name>\nExample: arc generate resource product");
252
- const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1);
283
+ const capitalizedName = toPascalCase(name);
253
284
  const lowerName = name.toLowerCase();
254
285
  const projectConfig = readProjectConfig();
255
286
  const ts = projectConfig.typescript ?? isTypeScriptProject();
@@ -290,9 +321,9 @@ async function generateResource(name, lowerName, resourcePath, templates, ext) {
290
321
  console.log(` + Created: src/resources/${lowerName}/`);
291
322
  }
292
323
  const files = {
293
- [`${lowerName}.model.${ext}`]: templates.model(name),
294
- [`${lowerName}.repository.${ext}`]: templates.repository(name),
295
- [`${lowerName}.resource.${ext}`]: templates.resource(name)
324
+ [`${lowerName}.model.${ext}`]: templates.model(name, lowerName),
325
+ [`${lowerName}.repository.${ext}`]: templates.repository(name, lowerName),
326
+ [`${lowerName}.resource.${ext}`]: templates.resource(name, lowerName)
296
327
  };
297
328
  for (const [filename, content] of Object.entries(files)) {
298
329
  const filepath = join(resourcePath, filename);
@@ -306,9 +337,10 @@ async function generateResource(name, lowerName, resourcePath, templates, ext) {
306
337
  if (!existsSync(testsDir)) mkdirSync(testsDir, { recursive: true });
307
338
  const testPath = join(testsDir, `${lowerName}.test.${ext}`);
308
339
  if (!existsSync(testPath)) {
309
- writeFileSync(testPath, templates.test(name));
340
+ writeFileSync(testPath, templates.test(name, lowerName));
310
341
  console.log(` + Created: tests/${lowerName}.test.${ext}`);
311
342
  }
343
+ const camel = toCamelCase(name);
312
344
  const isMultiTenant = readProjectConfig().tenant === "multi";
313
345
  console.log(`
314
346
  ╔═══════════════════════════════════════════════════════════════╗
@@ -318,19 +350,19 @@ async function generateResource(name, lowerName, resourcePath, templates, ext) {
318
350
  Next steps:
319
351
 
320
352
  1. Register in src/resources/index.${ext}:
321
- import ${lowerName}Resource from './${lowerName}/${lowerName}.resource.js';
353
+ import ${camel}Resource from './${lowerName}/${lowerName}.resource.js';
322
354
 
323
355
  export const resources = [
324
356
  // ... existing resources
325
- ${lowerName}Resource,
357
+ ${camel}Resource,
326
358
  ];
327
359
 
328
360
  2. Customize the model schema in:
329
361
  src/resources/${lowerName}/${lowerName}.model.${ext}
330
362
 
331
363
  3. Adjust permissions in ${lowerName}.resource.${ext}:
332
- ${isMultiTenant ? ` - requireOrgMembership() → any org member
333
- - requireOrgRole('admin') → specific org roles` : ` - requireAuth() → any authenticated user
364
+ ${isMultiTenant ? ` - requireAuth() → any authenticated user
365
+ - requireRoles(['admin']) → specific platform roles` : ` - requireAuth() → any authenticated user
334
366
  - requireRoles(['admin']) → specific platform roles`}
335
367
 
336
368
  4. Run tests:
@@ -349,7 +381,7 @@ async function generateFile(name, lowerName, resourcePath, fileType, template, e
349
381
  const filename = `${lowerName}.${fileType}.${ext}`;
350
382
  const filepath = join(resourcePath, filename);
351
383
  if (existsSync(filepath)) throw new Error(`${filename} already exists. Remove it first or use a different name.`);
352
- writeFileSync(filepath, template(name));
384
+ writeFileSync(filepath, template(name, lowerName));
353
385
  console.log(` + Created: ${filename}`);
354
386
  }
355
387
 
@@ -260,23 +260,20 @@ function packageJsonTemplate(config) {
260
260
  test: "vitest run",
261
261
  "test:watch": "vitest"
262
262
  };
263
- const imports = config.typescript ? {
264
- "#config/*": "./dist/config/*",
265
- "#shared/*": "./dist/shared/*",
266
- "#resources/*": "./dist/resources/*",
267
- "#plugins/*": "./dist/plugins/*"
268
- } : {
269
- "#config/*": "./src/config/*",
270
- "#shared/*": "./src/shared/*",
271
- "#resources/*": "./src/resources/*",
272
- "#plugins/*": "./src/plugins/*"
273
- };
274
263
  return JSON.stringify({
275
264
  name: config.name,
276
265
  version: "1.0.0",
277
266
  type: "module",
278
267
  main: config.typescript ? "dist/index.js" : "src/index.js",
279
- imports,
268
+ imports: {
269
+ "#config/*": "./src/config/*",
270
+ "#shared/*": "./src/shared/*",
271
+ "#resources/*": "./src/resources/*",
272
+ "#plugins/*": "./src/plugins/*",
273
+ "#services/*": "./src/services/*",
274
+ "#lib/*": "./src/lib/*",
275
+ "#utils/*": "./src/utils/*"
276
+ },
280
277
  scripts,
281
278
  engines: { node: ">=20" }
282
279
  }, null, 2);
@@ -2177,7 +2174,17 @@ ${orgPluginUsage}
2177
2174
  enabled: process.env.NODE_ENV === 'production',
2178
2175
  },
2179
2176
  });
2180
- }
2177
+ ${config.adapter === "mongokit" ? `
2178
+ // Register stub Mongoose models for Better Auth collections.
2179
+ // BA uses the raw MongoDB driver, so no Mongoose models exist by default.
2180
+ // These stubs (strict: false) enable populate() on refs like 'user', 'organization', etc.
2181
+ const baCollections = ['user', 'organization', 'member', 'invitation', 'session', 'account'];
2182
+ for (const name of baCollections) {
2183
+ if (!mongoose.models[name]) {
2184
+ mongoose.model(name, new mongoose.Schema({}, { strict: false, collection: name }));
2185
+ }
2186
+ }
2187
+ ` : ""} }
2181
2188
 
2182
2189
  return _auth;
2183
2190
  }
@@ -1,5 +1,5 @@
1
1
  import "../elevation-DGo5shaX.mjs";
2
- import { E as defineResource, T as ResourceDefinition, c as BaseController, d as QueryResolverConfig, f as BodySanitizer, h as AccessControlConfig, l as BaseControllerOptions, m as AccessControl, p as BodySanitizerConfig, u as QueryResolver } from "../interface-e9XfSsUV.mjs";
2
+ import { E as defineResource, T as ResourceDefinition, c as BaseController, d as QueryResolverConfig, f as BodySanitizer, h as AccessControlConfig, l as BaseControllerOptions, m as AccessControl, p as BodySanitizerConfig, u as QueryResolver } from "../interface-Dm4-jnia.mjs";
3
3
  import "../types-RLkFVgaw.mjs";
4
- import { A as createCrudRouter, C as MutationOperation, D as ActionRouterConfig, E as ActionHandler, O as IdempotencyService, S as MUTATION_OPERATIONS, T as SYSTEM_FIELDS, _ as HookOperation, a as getControllerScope, b as MAX_REGEX_LENGTH, c as CrudOperation, d as DEFAULT_MAX_LIMIT, f as DEFAULT_SORT, g as HOOK_PHASES, h as HOOK_OPERATIONS, i as getControllerContext, j as createPermissionMiddleware, k as createActionRouter, l as DEFAULT_ID_FIELD, m as DEFAULT_UPDATE_METHOD, n as createFastifyHandler, o as sendControllerResponse, p as DEFAULT_TENANT_FIELD, r as createRequestContext, s as CRUD_OPERATIONS, t as createCrudHandlers, u as DEFAULT_LIMIT, v as HookPhase, w as RESERVED_QUERY_PARAMS, x as MAX_SEARCH_LENGTH, y as MAX_FILTER_DEPTH } from "../fastifyAdapter-C8DlE0YH.mjs";
4
+ import { A as createCrudRouter, C as MutationOperation, D as ActionRouterConfig, E as ActionHandler, O as IdempotencyService, S as MUTATION_OPERATIONS, T as SYSTEM_FIELDS, _ as HookOperation, a as getControllerScope, b as MAX_REGEX_LENGTH, c as CrudOperation, d as DEFAULT_MAX_LIMIT, f as DEFAULT_SORT, g as HOOK_PHASES, h as HOOK_OPERATIONS, i as getControllerContext, j as createPermissionMiddleware, k as createActionRouter, l as DEFAULT_ID_FIELD, m as DEFAULT_UPDATE_METHOD, n as createFastifyHandler, o as sendControllerResponse, p as DEFAULT_TENANT_FIELD, r as createRequestContext, s as CRUD_OPERATIONS, t as createCrudHandlers, u as DEFAULT_LIMIT, v as HookPhase, w as RESERVED_QUERY_PARAMS, x as MAX_SEARCH_LENGTH, y as MAX_FILTER_DEPTH } from "../fastifyAdapter-DTOLNtjw.mjs";
5
5
  export { AccessControl, type AccessControlConfig, type ActionHandler, type ActionRouterConfig, BaseController, type BaseControllerOptions, BodySanitizer, type 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, type IdempotencyService, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MutationOperation, QueryResolver, type QueryResolverConfig, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, createActionRouter, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineResource, getControllerContext, getControllerScope, sendControllerResponse };
@@ -1,4 +1,4 @@
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-DdXFXQtN.mjs";
2
- import { _ as QueryResolver, c as createPermissionMiddleware, d as createFastifyHandler, f as createRequestContext, g as BaseController, h as sendControllerResponse, m as getControllerScope, n as defineResource, o as createActionRouter, p as getControllerContext, s as createCrudRouter, t as ResourceDefinition, u as createCrudHandlers, v as BodySanitizer, y as AccessControl } from "../defineResource-PXzSJ15_.mjs";
2
+ import { _ as QueryResolver, c as createPermissionMiddleware, d as createFastifyHandler, f as createRequestContext, g as BaseController, h as sendControllerResponse, m as getControllerScope, n as defineResource, o as createActionRouter, p as getControllerContext, s as createCrudRouter, t as ResourceDefinition, u as createCrudHandlers, v as BodySanitizer, y as AccessControl } from "../defineResource-Bq_fXZtm.mjs";
3
3
 
4
4
  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 };
@@ -50,7 +50,9 @@ const productionPreset = {
50
50
  allowedHeaders: [
51
51
  "Content-Type",
52
52
  "Authorization",
53
- "Accept"
53
+ "Accept",
54
+ "x-organization-id",
55
+ "x-request-id"
54
56
  ]
55
57
  },
56
58
  rateLimit: {
@@ -95,7 +97,9 @@ const developmentPreset = {
95
97
  allowedHeaders: [
96
98
  "Content-Type",
97
99
  "Authorization",
98
- "Accept"
100
+ "Accept",
101
+ "x-organization-id",
102
+ "x-request-id"
99
103
  ]
100
104
  },
101
105
  rateLimit: {
@@ -337,8 +341,9 @@ async function createApp(options) {
337
341
  } else fastify.log.warn("Helmet disabled - security headers not applied");
338
342
  if (config.cors !== false) {
339
343
  const cors = await loadPlugin("cors");
340
- const corsOptions = config.cors ?? {};
344
+ const corsOptions = { ...config.cors ?? {} };
341
345
  if (config.preset === "production" && (!corsOptions || !("origin" in corsOptions))) throw new Error("CORS origin must be explicitly configured in production.\nSet cors.origin to allowed domains or set cors: false to disable.\nExample: cors: { origin: ['https://yourdomain.com'] }\nDocs: https://github.com/classytic/arc#security");
346
+ if (corsOptions.credentials && corsOptions.origin === "*") corsOptions.origin = true;
342
347
  await fastify.register(cors, corsOptions);
343
348
  fastify.log.debug("CORS enabled");
344
349
  } else fastify.log.warn("CORS disabled");
@@ -41,7 +41,7 @@ var AccessControl = class AccessControl {
41
41
  if (policyFilters) Object.assign(filter, policyFilters);
42
42
  const scope = arcContext?._scope;
43
43
  const orgId = scope ? getOrgId(scope) : void 0;
44
- if (orgId && !policyFilters?.[this.tenantField]) filter[this.tenantField] = orgId;
44
+ if (this.tenantField && orgId && !policyFilters?.[this.tenantField]) filter[this.tenantField] = orgId;
45
45
  return filter;
46
46
  }
47
47
  /**
@@ -65,6 +65,7 @@ var AccessControl = class AccessControl {
65
65
  * unscoped records from leaking across tenants.
66
66
  */
67
67
  checkOrgScope(item, arcContext) {
68
+ if (!this.tenantField) return true;
68
69
  const scope = arcContext?._scope;
69
70
  const orgId = scope ? getOrgId(scope) : void 0;
70
71
  if (!item || !orgId) return true;
@@ -259,7 +260,7 @@ var QueryResolver = class {
259
260
  this.defaultLimit = config.defaultLimit ?? DEFAULT_LIMIT;
260
261
  this.defaultSort = config.defaultSort ?? DEFAULT_SORT;
261
262
  this.schemaOptions = config.schemaOptions ?? {};
262
- this.tenantField = config.tenantField ?? DEFAULT_TENANT_FIELD;
263
+ this.tenantField = config.tenantField !== void 0 ? config.tenantField : DEFAULT_TENANT_FIELD;
263
264
  }
264
265
  /**
265
266
  * Resolve a request into parsed query options -- ONE parse per request.
@@ -278,7 +279,7 @@ var QueryResolver = class {
278
279
  if (policyFilters) Object.assign(filters, policyFilters);
279
280
  const scope = arcContext?._scope;
280
281
  const orgId = scope ? getOrgId(scope) : void 0;
281
- if (orgId && !policyFilters?.[this.tenantField]) filters[this.tenantField] = orgId;
282
+ if (this.tenantField && orgId && !policyFilters?.[this.tenantField]) filters[this.tenantField] = orgId;
282
283
  return {
283
284
  page,
284
285
  limit,
@@ -371,7 +372,7 @@ var BaseController = class {
371
372
  this.defaultLimit = options.defaultLimit ?? DEFAULT_LIMIT;
372
373
  this.defaultSort = options.defaultSort ?? DEFAULT_SORT;
373
374
  this.resourceName = options.resourceName;
374
- this.tenantField = options.tenantField ?? DEFAULT_TENANT_FIELD;
375
+ this.tenantField = options.tenantField !== void 0 ? options.tenantField : DEFAULT_TENANT_FIELD;
375
376
  this.idField = options.idField ?? DEFAULT_ID_FIELD;
376
377
  this._matchesFilter = options.matchesFilter;
377
378
  if (options.cache) this._cacheConfig = options.cache;
@@ -396,6 +397,16 @@ var BaseController = class {
396
397
  this.update = this.update.bind(this);
397
398
  this.delete = this.delete.bind(this);
398
399
  }
400
+ /**
401
+ * Get the tenant field name if multi-tenant scoping is enabled.
402
+ * Returns `undefined` when `tenantField` is `false` (platform-universal mode).
403
+ *
404
+ * Use this in subclass overrides instead of accessing `this.tenantField` directly
405
+ * to avoid TypeScript indexing errors with `string | false`.
406
+ */
407
+ getTenantField() {
408
+ return this.tenantField || void 0;
409
+ }
399
410
  /** Extract typed Arc internal metadata from request */
400
411
  meta(req) {
401
412
  return req.metadata;
@@ -570,7 +581,7 @@ var BaseController = class {
570
581
  const data = this.bodySanitizer.sanitize(req.body ?? {}, "create", req, arcContext);
571
582
  const scope = arcContext?._scope;
572
583
  const createOrgId = scope ? getOrgId(scope) : void 0;
573
- if (createOrgId) data[this.tenantField] = createOrgId;
584
+ if (this.tenantField && createOrgId) data[this.tenantField] = createOrgId;
574
585
  const userId = getUserId(req.user);
575
586
  if (userId) data.createdBy = userId;
576
587
  const hooks = this.getHooks(req);
@@ -1571,29 +1582,6 @@ function createActionRouter(fastify, config) {
1571
1582
  type: "object",
1572
1583
  properties: bodyProperties,
1573
1584
  required: ["action"]
1574
- },
1575
- response: {
1576
- 200: {
1577
- type: "object",
1578
- properties: {
1579
- success: { type: "boolean" },
1580
- data: { type: "object" }
1581
- }
1582
- },
1583
- 400: {
1584
- type: "object",
1585
- properties: {
1586
- success: { type: "boolean" },
1587
- error: { type: "string" }
1588
- }
1589
- },
1590
- 403: {
1591
- type: "object",
1592
- properties: {
1593
- success: { type: "boolean" },
1594
- error: { type: "string" }
1595
- }
1596
- }
1597
1585
  }
1598
1586
  };
1599
1587
  const preHandler = [];
@@ -1,5 +1,5 @@
1
1
  import "../elevation-DGo5shaX.mjs";
2
- import "../interface-e9XfSsUV.mjs";
2
+ import "../interface-Dm4-jnia.mjs";
3
3
  import "../types-RLkFVgaw.mjs";
4
4
  import { RegistryEntry } from "../types/index.mjs";
5
5
  import { t as ExternalOpenApiPaths } from "../externalPaths-SyPF2tgK.mjs";
@@ -1,5 +1,5 @@
1
1
  import "../elevation-DGo5shaX.mjs";
2
- import "../interface-e9XfSsUV.mjs";
2
+ import "../interface-Dm4-jnia.mjs";
3
3
  import "../types-RLkFVgaw.mjs";
4
4
  import "../queryCachePlugin-Q6SYuHZ6.mjs";
5
5
  import "../eventPlugin-H6wDDjGO.mjs";
@@ -1,3 +1,3 @@
1
- import { a as getPreset, i as developmentPreset, n as createApp, o as productionPreset, s as testingPreset, t as ArcFactory } from "../createApp-D2D5XXaV.mjs";
1
+ import { a as getPreset, i as developmentPreset, n as createApp, o as productionPreset, s as testingPreset, t as ArcFactory } from "../createApp-BTHYuHNU.mjs";
2
2
 
3
3
  export { ArcFactory, createApp, developmentPreset, getPreset, productionPreset, testingPreset };
@@ -1,5 +1,5 @@
1
1
  import { s as RequestScope } from "./elevation-DGo5shaX.mjs";
2
- import { b as IControllerResponse, x as IRequestContext, y as IController } from "./interface-e9XfSsUV.mjs";
2
+ import { b as IControllerResponse, x as IRequestContext, y as IController } from "./interface-Dm4-jnia.mjs";
3
3
  import { t as PermissionCheck } from "./types-RLkFVgaw.mjs";
4
4
  import { CrudController, CrudRouterOptions, FastifyWithDecorators, RequestContext, RequestWithExtras } from "./types/index.mjs";
5
5
  import { FastifyInstance, FastifyReply, FastifyRequest, RouteHandlerMethod } from "fastify";
@@ -1,4 +1,4 @@
1
1
  import "../elevation-DGo5shaX.mjs";
2
- import { $ as beforeUpdate, B as DefineHookOptions, G as HookRegistration, H as HookHandler, J as afterCreate, K as HookSystem, Q as beforeDelete, U as HookOperation, V as HookContext, W as HookPhase, X as afterUpdate, Y as afterDelete, Z as beforeCreate, et as createHookSystem, q as HookSystemOptions, tt as defineHook } from "../interface-e9XfSsUV.mjs";
2
+ import { $ as beforeUpdate, B as DefineHookOptions, G as HookRegistration, H as HookHandler, J as afterCreate, K as HookSystem, Q as beforeDelete, U as HookOperation, V as HookContext, W as HookPhase, X as afterUpdate, Y as afterDelete, Z as beforeCreate, et as createHookSystem, q as HookSystemOptions, tt as defineHook } from "../interface-Dm4-jnia.mjs";
3
3
  import "../types-RLkFVgaw.mjs";
4
4
  export { type DefineHookOptions, type HookContext, type HookHandler, type HookOperation, type HookPhase, type HookRegistration, HookSystem, type HookSystemOptions, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
package/dist/index.d.mts CHANGED
@@ -1,11 +1,11 @@
1
1
  import "./elevation-DGo5shaX.mjs";
2
- import { D as CrudRepository, E as defineResource, F as OperationFilter, I as PipelineConfig, L as PipelineContext, M as Guard, N as Interceptor, P as NextFunction, R as PipelineStep, S as RouteHandler, T as ResourceDefinition, _ as ControllerLike, a as RepositoryLike, b as IControllerResponse, c as BaseController, i as RelationMetadata, j as QueryOptions, k as PaginatedResult, l as BaseControllerOptions, n as DataAdapter, o as SchemaMetadata, r as FieldMetadata, s as ValidationResult, x as IRequestContext, y as IController, z as Transform } from "./interface-e9XfSsUV.mjs";
2
+ import { D as CrudRepository, E as defineResource, F as OperationFilter, I as PipelineConfig, L as PipelineContext, M as Guard, N as Interceptor, P as NextFunction, R as PipelineStep, S as RouteHandler, T as ResourceDefinition, _ as ControllerLike, a as RepositoryLike, b as IControllerResponse, c as BaseController, i as RelationMetadata, j as QueryOptions, k as PaginatedResult, l as BaseControllerOptions, n as DataAdapter, o as SchemaMetadata, r as FieldMetadata, s as ValidationResult, x as IRequestContext, y as IController, z as Transform } from "./interface-Dm4-jnia.mjs";
3
3
  import { a as applyFieldWritePermissions, i as applyFieldReadPermissions, n as FieldPermissionMap, o as fields, t as FieldPermission } from "./fields-Bi_AVKSo.mjs";
4
4
  import { i as UserBase, n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "./types-RLkFVgaw.mjs";
5
5
  import { AdditionalRoute, AnyRecord, ApiResponse, ArcInternalMetadata, AuthPluginOptions, ConfigError, CrudController, CrudRouteKey, CrudRouterOptions, CrudSchemas, EventDefinition, FastifyRequestExtras, FastifyWithAuth, FastifyWithDecorators, FieldRule, GracefulShutdownOptions, HealthCheck, HealthOptions, InferAdapterDoc, InferDocType, InferResourceDoc, IntrospectionData, IntrospectionPluginOptions, JWTPayload, MiddlewareConfig, MiddlewareHandler, OwnershipCheck, PresetFunction, PresetResult, RateLimitConfig, RegistryEntry, RegistryStats, RequestContext, RequestIdOptions, RequestWithExtras, ResourceConfig, ResourceMetadata, RouteHandlerMethod, RouteSchemaOptions, ServiceContext, TypedController, TypedRepository, TypedResourceConfig, UserOrganization, ValidateOptions, ValidationResult as ValidationResult$1 } from "./types/index.mjs";
6
- import { c as MongooseAdapterOptions, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, s as MongooseAdapter, t as PrismaAdapter } from "./prisma-C3iornoK.mjs";
6
+ import { c as MongooseAdapterOptions, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, s as MongooseAdapter, t as PrismaAdapter } from "./prisma-Bi9nxirN.mjs";
7
7
  import "./adapters/index.mjs";
8
- import { C as MutationOperation, S as MUTATION_OPERATIONS, T as SYSTEM_FIELDS, _ as HookOperation, a as getControllerScope, b as MAX_REGEX_LENGTH, c as CrudOperation, d as DEFAULT_MAX_LIMIT, f as DEFAULT_SORT, g as HOOK_PHASES, h as HOOK_OPERATIONS, l as DEFAULT_ID_FIELD, m as DEFAULT_UPDATE_METHOD, p as DEFAULT_TENANT_FIELD, s as CRUD_OPERATIONS, u as DEFAULT_LIMIT, v as HookPhase, w as RESERVED_QUERY_PARAMS, x as MAX_SEARCH_LENGTH, y as MAX_FILTER_DEPTH } from "./fastifyAdapter-C8DlE0YH.mjs";
8
+ import { C as MutationOperation, S as MUTATION_OPERATIONS, T as SYSTEM_FIELDS, _ as HookOperation, a as getControllerScope, b as MAX_REGEX_LENGTH, c as CrudOperation, d as DEFAULT_MAX_LIMIT, f as DEFAULT_SORT, g as HOOK_PHASES, h as HOOK_OPERATIONS, l as DEFAULT_ID_FIELD, m as DEFAULT_UPDATE_METHOD, p as DEFAULT_TENANT_FIELD, s as CRUD_OPERATIONS, u as DEFAULT_LIMIT, v as HookPhase, w as RESERVED_QUERY_PARAMS, x as MAX_SEARCH_LENGTH, y as MAX_FILTER_DEPTH } from "./fastifyAdapter-DTOLNtjw.mjs";
9
9
  import "./core/index.mjs";
10
10
  import { a as NotFoundError, d as ValidationError, i as ForbiddenError, t as ArcError, u as UnauthorizedError } from "./errors-DAWRdiYP.mjs";
11
11
  import { a as presets_d_exports, c as readOnly, i as ownerWithAdminBypass, n as authenticated, o as publicRead, r as fullPublic, s as publicReadAdminWrite, t as adminOnly } from "./presets-BTeYbw7h.mjs";
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { a as DEFAULT_SORT, c as HOOK_OPERATIONS, d as MAX_REGEX_LENGTH, f as MAX_SEARCH_LENGTH, h as SYSTEM_FIELDS, i as DEFAULT_MAX_LIMIT, l as HOOK_PHASES, m as RESERVED_QUERY_PARAMS, n as DEFAULT_ID_FIELD, o as DEFAULT_TENANT_FIELD, p as MUTATION_OPERATIONS, r as DEFAULT_LIMIT, s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS, u as MAX_FILTER_DEPTH } from "./constants-DdXFXQtN.mjs";
2
2
  import { a as createMongooseAdapter, i as MongooseAdapter, r as createPrismaAdapter, t as PrismaAdapter } from "./prisma-DJbMt3yf.mjs";
3
- import { a as validateResourceConfig, g as BaseController, i as formatValidationErrors, l as pipe, m as getControllerScope, n as defineResource, r as assertValidConfig, t as ResourceDefinition } from "./defineResource-PXzSJ15_.mjs";
3
+ import { a as validateResourceConfig, g as BaseController, i as formatValidationErrors, l as pipe, m as getControllerScope, n as defineResource, r as assertValidConfig, t as ResourceDefinition } from "./defineResource-Bq_fXZtm.mjs";
4
4
  import { n as applyFieldWritePermissions, r as fields, t as applyFieldReadPermissions } from "./fields-CTd_CrKr.mjs";
5
5
  import { i as NotFoundError, l as UnauthorizedError, r as ForbiddenError, t as ArcError, u as ValidationError } from "./errors-DBANPbGr.mjs";
6
6
  import { t as requestContext } from "./requestContext-xi6OKBL-.mjs";
@@ -759,8 +759,8 @@ interface ControllerLike {
759
759
  //#endregion
760
760
  //#region src/core/AccessControl.d.ts
761
761
  interface AccessControlConfig {
762
- /** Field name used for multi-tenant scoping (default: 'organizationId') */
763
- tenantField: string;
762
+ /** Field name used for multi-tenant scoping (default: 'organizationId'). Set to `false` to disable org filtering. */
763
+ tenantField: string | false;
764
764
  /** Primary key field name (default: '_id') */
765
765
  idField: string;
766
766
  /**
@@ -876,8 +876,8 @@ interface QueryResolverConfig {
876
876
  defaultSort?: string;
877
877
  /** Schema options for field sanitization */
878
878
  schemaOptions?: RouteSchemaOptions;
879
- /** Field name used for multi-tenant scoping (default: 'organizationId') */
880
- tenantField?: string;
879
+ /** Field name used for multi-tenant scoping (default: 'organizationId'). Set to `false` to disable. */
880
+ tenantField?: string | false;
881
881
  }
882
882
  declare class QueryResolver {
883
883
  private queryParser;
@@ -926,8 +926,9 @@ interface BaseControllerOptions {
926
926
  /**
927
927
  * Field name used for multi-tenant scoping (default: 'organizationId').
928
928
  * Override to match your schema: 'workspaceId', 'tenantId', 'teamId', etc.
929
+ * Set to `false` to disable org filtering for platform-universal resources.
929
930
  */
930
- tenantField?: string;
931
+ tenantField?: string | false;
931
932
  /**
932
933
  * Primary key field name (default: '_id').
933
934
  * Override for non-MongoDB adapters (e.g., 'id' for SQL databases).
@@ -965,7 +966,7 @@ declare class BaseController<TDoc = AnyRecord, TRepository extends RepositoryLik
965
966
  protected defaultLimit: number;
966
967
  protected defaultSort: string;
967
968
  protected resourceName?: string;
968
- protected tenantField: string;
969
+ protected tenantField: string | false;
969
970
  protected idField: string;
970
971
  /** Composable access control (ID filtering, policy checks, org scope, ownership) */
971
972
  readonly accessControl: AccessControl;
@@ -977,6 +978,14 @@ declare class BaseController<TDoc = AnyRecord, TRepository extends RepositoryLik
977
978
  private _presetFields;
978
979
  private _cacheConfig?;
979
980
  constructor(repository: TRepository, options?: BaseControllerOptions);
981
+ /**
982
+ * Get the tenant field name if multi-tenant scoping is enabled.
983
+ * Returns `undefined` when `tenantField` is `false` (platform-universal mode).
984
+ *
985
+ * Use this in subclass overrides instead of accessing `this.tenantField` directly
986
+ * to avoid TypeScript indexing errors with `string | false`.
987
+ */
988
+ protected getTenantField(): string | undefined;
980
989
  /** Extract typed Arc internal metadata from request */
981
990
  private meta;
982
991
  /** Get hook system from request context (instance-scoped) */
@@ -1,5 +1,5 @@
1
1
  import "../elevation-DGo5shaX.mjs";
2
- import { S as RouteHandler } from "../interface-e9XfSsUV.mjs";
2
+ import { S as RouteHandler } from "../interface-Dm4-jnia.mjs";
3
3
  import { i as UserBase } from "../types-RLkFVgaw.mjs";
4
4
  import "../types/index.mjs";
5
5
  import { InvitationAdapter, InvitationDoc, MemberDoc, OrgAdapter, OrgDoc, OrgPermissionStatement, OrgRole, OrganizationPluginOptions } from "./types.mjs";
@@ -1,5 +1,5 @@
1
1
  import "../elevation-DGo5shaX.mjs";
2
- import { K as HookSystem, w as ResourceRegistry } from "../interface-e9XfSsUV.mjs";
2
+ import { K as HookSystem, w as ResourceRegistry } from "../interface-Dm4-jnia.mjs";
3
3
  import "../types-RLkFVgaw.mjs";
4
4
  import { AdditionalRoute, AnyRecord, MiddlewareConfig, PresetHook, RouteSchemaOptions } from "../types/index.mjs";
5
5
  import { t as ExternalOpenApiPaths } from "../externalPaths-SyPF2tgK.mjs";
@@ -1,5 +1,5 @@
1
1
  import "../elevation-DGo5shaX.mjs";
2
- import { b as IControllerResponse, k as PaginatedResult, x as IRequestContext } from "../interface-e9XfSsUV.mjs";
2
+ import { b as IControllerResponse, k as PaginatedResult, x as IRequestContext } from "../interface-Dm4-jnia.mjs";
3
3
  import "../types-RLkFVgaw.mjs";
4
4
  import { AnyRecord, PresetResult, ResourceConfig } from "../types/index.mjs";
5
5
  import multiTenantPreset, { MultiTenantOptions } from "./multiTenant.mjs";
@@ -1,5 +1,5 @@
1
1
  import "../elevation-DGo5shaX.mjs";
2
- import "../interface-e9XfSsUV.mjs";
2
+ import "../interface-Dm4-jnia.mjs";
3
3
  import "../types-RLkFVgaw.mjs";
4
4
  import { CrudRouteKey, PresetResult } from "../types/index.mjs";
5
5
 
@@ -1,4 +1,4 @@
1
- import { D as CrudRepository, a as RepositoryLike, n as DataAdapter, o as SchemaMetadata, s as ValidationResult } from "./interface-e9XfSsUV.mjs";
1
+ import { D as CrudRepository, a as RepositoryLike, n as DataAdapter, o as SchemaMetadata, s as ValidationResult } from "./interface-Dm4-jnia.mjs";
2
2
  import { OpenApiSchemas, ParsedQuery, QueryParserInterface, RouteSchemaOptions } from "./types/index.mjs";
3
3
  import { Model } from "mongoose";
4
4
 
@@ -1,5 +1,5 @@
1
1
  import "../elevation-DGo5shaX.mjs";
2
- import { C as RegisterOptions, w as ResourceRegistry } from "../interface-e9XfSsUV.mjs";
2
+ import { C as RegisterOptions, w as ResourceRegistry } from "../interface-Dm4-jnia.mjs";
3
3
  import "../types-RLkFVgaw.mjs";
4
4
  import { IntrospectionPluginOptions } from "../types/index.mjs";
5
5
  import { FastifyPluginAsync } from "fastify";
@@ -1,5 +1,5 @@
1
1
  import "../elevation-DGo5shaX.mjs";
2
- import { D as CrudRepository, T as ResourceDefinition } from "../interface-e9XfSsUV.mjs";
2
+ import { D as CrudRepository, T as ResourceDefinition } from "../interface-Dm4-jnia.mjs";
3
3
  import "../types-RLkFVgaw.mjs";
4
4
  import { AnyRecord } from "../types/index.mjs";
5
5
  import "../queryCachePlugin-Q6SYuHZ6.mjs";
@@ -649,7 +649,7 @@ interface HttpTestHarnessOptions<T = unknown> {
649
649
  };
650
650
  /** Auth provider for generating request headers */
651
651
  auth: AuthProvider;
652
- /** API path prefix (default: '/api') */
652
+ /** API path prefix (default: '/api' for eager, '' for deferred) */
653
653
  apiPrefix?: string;
654
654
  }
655
655
  /** Options can be passed directly or as a getter for deferred resolution */
@@ -666,12 +666,21 @@ type OptionsOrGetter<T> = HttpTestHarnessOptions<T> | (() => HttpTestHarnessOpti
666
666
  declare class HttpTestHarness<T = unknown> {
667
667
  private resource;
668
668
  private optionsOrGetter;
669
- private baseUrl;
669
+ private eagerBaseUrl;
670
670
  private enabledRoutes;
671
671
  private updateMethod;
672
672
  constructor(resource: ResourceDefinition<unknown>, optionsOrGetter: OptionsOrGetter<T>);
673
673
  /** Resolve options (supports both direct and deferred) */
674
674
  private getOptions;
675
+ /**
676
+ * Resolve the base URL for requests.
677
+ *
678
+ * - Eager mode: uses pre-computed baseUrl from constructor
679
+ * - Deferred mode: reads apiPrefix from the getter options at runtime
680
+ *
681
+ * Must only be called inside it()/afterAll() callbacks (after beforeAll has run).
682
+ */
683
+ private getBaseUrl;
675
684
  /**
676
685
  * Run all test suites: CRUD + permissions + validation
677
686
  */
@@ -715,6 +724,7 @@ declare class HttpTestHarness<T = unknown> {
715
724
  *
716
725
  * createHttpTestHarness(jobResource, () => ({
717
726
  * app: ctx.app,
727
+ * apiPrefix: '',
718
728
  * fixtures: { valid: { title: 'Test' } },
719
729
  * auth: createBetterAuthProvider({ ... }),
720
730
  * })).runAll();
@@ -1000,7 +1000,7 @@ var DatabaseSnapshot = class {
1000
1000
  * ```
1001
1001
  */
1002
1002
  async function createTestApp(options = {}) {
1003
- const { createApp } = await import("../createApp-D2D5XXaV.mjs").then((n) => n.r);
1003
+ const { createApp } = await import("../createApp-BTHYuHNU.mjs").then((n) => n.r);
1004
1004
  const { useInMemoryDb = true, mongoUri: providedMongoUri, ...appOptions } = options;
1005
1005
  const defaultAuth = {
1006
1006
  type: "jwt",
@@ -1606,6 +1606,7 @@ async function setupBetterAuthOrg(options) {
1606
1606
  *
1607
1607
  * const harness = createHttpTestHarness(jobResource, () => ({
1608
1608
  * app: ctx.app,
1609
+ * apiPrefix: '',
1609
1610
  * fixtures: { valid: { title: 'Test' } },
1610
1611
  * auth: createBetterAuthProvider({ tokens: { admin: ctx.users.admin.token }, orgId: ctx.orgId, adminRole: 'admin' }),
1611
1612
  * }));
@@ -1687,13 +1688,14 @@ function createBetterAuthProvider(options) {
1687
1688
  var HttpTestHarness = class {
1688
1689
  resource;
1689
1690
  optionsOrGetter;
1690
- baseUrl;
1691
+ eagerBaseUrl;
1691
1692
  enabledRoutes;
1692
1693
  updateMethod;
1693
1694
  constructor(resource, optionsOrGetter) {
1694
1695
  this.resource = resource;
1695
1696
  this.optionsOrGetter = optionsOrGetter;
1696
- this.baseUrl = `${typeof optionsOrGetter === "function" ? "/api" : optionsOrGetter.apiPrefix ?? "/api"}${resource.prefix}`;
1697
+ if (typeof optionsOrGetter === "function") this.eagerBaseUrl = null;
1698
+ else this.eagerBaseUrl = `${optionsOrGetter.apiPrefix ?? "/api"}${resource.prefix}`;
1697
1699
  const disabled = new Set(resource.disabledRoutes ?? []);
1698
1700
  this.enabledRoutes = new Set(resource.disableDefaultRoutes ? [] : CRUD_OPERATIONS.filter((op) => !disabled.has(op)));
1699
1701
  this.updateMethod = resource.updateMethod === "PUT" ? "PUT" : "PATCH";
@@ -1703,6 +1705,18 @@ var HttpTestHarness = class {
1703
1705
  return typeof this.optionsOrGetter === "function" ? this.optionsOrGetter() : this.optionsOrGetter;
1704
1706
  }
1705
1707
  /**
1708
+ * Resolve the base URL for requests.
1709
+ *
1710
+ * - Eager mode: uses pre-computed baseUrl from constructor
1711
+ * - Deferred mode: reads apiPrefix from the getter options at runtime
1712
+ *
1713
+ * Must only be called inside it()/afterAll() callbacks (after beforeAll has run).
1714
+ */
1715
+ getBaseUrl() {
1716
+ if (this.eagerBaseUrl !== null) return this.eagerBaseUrl;
1717
+ return `${this.getOptions().apiPrefix ?? ""}${this.resource.prefix}`;
1718
+ }
1719
+ /**
1706
1720
  * Run all test suites: CRUD + permissions + validation
1707
1721
  */
1708
1722
  runAll() {
@@ -1722,12 +1736,13 @@ var HttpTestHarness = class {
1722
1736
  * - GET /:id with non-existent ID → 404
1723
1737
  */
1724
1738
  runCrud() {
1725
- const { resource, baseUrl, enabledRoutes, updateMethod } = this;
1739
+ const { resource, enabledRoutes, updateMethod } = this;
1726
1740
  let createdId = null;
1727
1741
  describe(`${resource.displayName} HTTP CRUD`, () => {
1728
1742
  afterAll(async () => {
1729
1743
  if (createdId && enabledRoutes.has("delete")) {
1730
1744
  const { app, auth } = this.getOptions();
1745
+ const baseUrl = this.getBaseUrl();
1731
1746
  await app.inject({
1732
1747
  method: "DELETE",
1733
1748
  url: `${baseUrl}/${createdId}`,
@@ -1737,6 +1752,7 @@ var HttpTestHarness = class {
1737
1752
  });
1738
1753
  if (enabledRoutes.has("create")) it("POST should create a resource", async () => {
1739
1754
  const { app, auth, fixtures } = this.getOptions();
1755
+ const baseUrl = this.getBaseUrl();
1740
1756
  const adminHeaders = auth.getHeaders(auth.adminRole);
1741
1757
  const res = await app.inject({
1742
1758
  method: "POST",
@@ -1753,6 +1769,7 @@ var HttpTestHarness = class {
1753
1769
  });
1754
1770
  if (enabledRoutes.has("list")) it("GET should list resources", async () => {
1755
1771
  const { app, auth } = this.getOptions();
1772
+ const baseUrl = this.getBaseUrl();
1756
1773
  const res = await app.inject({
1757
1774
  method: "GET",
1758
1775
  url: baseUrl,
@@ -1769,6 +1786,7 @@ var HttpTestHarness = class {
1769
1786
  it("GET /:id should return the resource", async () => {
1770
1787
  if (!createdId) return;
1771
1788
  const { app, auth } = this.getOptions();
1789
+ const baseUrl = this.getBaseUrl();
1772
1790
  const res = await app.inject({
1773
1791
  method: "GET",
1774
1792
  url: `${baseUrl}/${createdId}`,
@@ -1782,6 +1800,7 @@ var HttpTestHarness = class {
1782
1800
  });
1783
1801
  it("GET /:id with non-existent ID should return 404", async () => {
1784
1802
  const { app, auth } = this.getOptions();
1803
+ const baseUrl = this.getBaseUrl();
1785
1804
  const res = await app.inject({
1786
1805
  method: "GET",
1787
1806
  url: `${baseUrl}/000000000000000000000000`,
@@ -1795,6 +1814,7 @@ var HttpTestHarness = class {
1795
1814
  it(`${updateMethod} /:id should update the resource`, async () => {
1796
1815
  if (!createdId) return;
1797
1816
  const { app, auth, fixtures } = this.getOptions();
1817
+ const baseUrl = this.getBaseUrl();
1798
1818
  const updatePayload = fixtures.update || fixtures.valid;
1799
1819
  const res = await app.inject({
1800
1820
  method: updateMethod,
@@ -1809,6 +1829,7 @@ var HttpTestHarness = class {
1809
1829
  });
1810
1830
  it(`${updateMethod} /:id with non-existent ID should return 404`, async () => {
1811
1831
  const { app, auth, fixtures } = this.getOptions();
1832
+ const baseUrl = this.getBaseUrl();
1812
1833
  expect((await app.inject({
1813
1834
  method: updateMethod,
1814
1835
  url: `${baseUrl}/000000000000000000000000`,
@@ -1820,6 +1841,7 @@ var HttpTestHarness = class {
1820
1841
  if (enabledRoutes.has("delete")) {
1821
1842
  it("DELETE /:id should delete the resource", async () => {
1822
1843
  const { app, auth, fixtures } = this.getOptions();
1844
+ const baseUrl = this.getBaseUrl();
1823
1845
  const adminHeaders = auth.getHeaders(auth.adminRole);
1824
1846
  let deleteId;
1825
1847
  if (enabledRoutes.has("create")) {
@@ -1845,6 +1867,7 @@ var HttpTestHarness = class {
1845
1867
  });
1846
1868
  it("DELETE /:id with non-existent ID should return 404", async () => {
1847
1869
  const { app, auth } = this.getOptions();
1870
+ const baseUrl = this.getBaseUrl();
1848
1871
  expect((await app.inject({
1849
1872
  method: "DELETE",
1850
1873
  url: `${baseUrl}/000000000000000000000000`,
@@ -1862,10 +1885,11 @@ var HttpTestHarness = class {
1862
1885
  * - Admin role gets 2xx for all operations
1863
1886
  */
1864
1887
  runPermissions() {
1865
- const { resource, baseUrl, enabledRoutes, updateMethod } = this;
1888
+ const { resource, enabledRoutes, updateMethod } = this;
1866
1889
  describe(`${resource.displayName} HTTP Permissions`, () => {
1867
1890
  if (enabledRoutes.has("list")) it("GET list without auth should return 401", async () => {
1868
1891
  const { app } = this.getOptions();
1892
+ const baseUrl = this.getBaseUrl();
1869
1893
  expect((await app.inject({
1870
1894
  method: "GET",
1871
1895
  url: baseUrl
@@ -1873,6 +1897,7 @@ var HttpTestHarness = class {
1873
1897
  });
1874
1898
  if (enabledRoutes.has("get")) it("GET get without auth should return 401", async () => {
1875
1899
  const { app } = this.getOptions();
1900
+ const baseUrl = this.getBaseUrl();
1876
1901
  expect((await app.inject({
1877
1902
  method: "GET",
1878
1903
  url: `${baseUrl}/000000000000000000000000`
@@ -1880,6 +1905,7 @@ var HttpTestHarness = class {
1880
1905
  });
1881
1906
  if (enabledRoutes.has("create")) it("POST create without auth should return 401", async () => {
1882
1907
  const { app, fixtures } = this.getOptions();
1908
+ const baseUrl = this.getBaseUrl();
1883
1909
  expect((await app.inject({
1884
1910
  method: "POST",
1885
1911
  url: baseUrl,
@@ -1888,6 +1914,7 @@ var HttpTestHarness = class {
1888
1914
  });
1889
1915
  if (enabledRoutes.has("update")) it(`${updateMethod} update without auth should return 401`, async () => {
1890
1916
  const { app, fixtures } = this.getOptions();
1917
+ const baseUrl = this.getBaseUrl();
1891
1918
  expect((await app.inject({
1892
1919
  method: updateMethod,
1893
1920
  url: `${baseUrl}/000000000000000000000000`,
@@ -1896,6 +1923,7 @@ var HttpTestHarness = class {
1896
1923
  });
1897
1924
  if (enabledRoutes.has("delete")) it("DELETE delete without auth should return 401", async () => {
1898
1925
  const { app } = this.getOptions();
1926
+ const baseUrl = this.getBaseUrl();
1899
1927
  expect((await app.inject({
1900
1928
  method: "DELETE",
1901
1929
  url: `${baseUrl}/000000000000000000000000`
@@ -1903,6 +1931,7 @@ var HttpTestHarness = class {
1903
1931
  });
1904
1932
  if (enabledRoutes.has("list")) it("admin should access list endpoint", async () => {
1905
1933
  const { app, auth } = this.getOptions();
1934
+ const baseUrl = this.getBaseUrl();
1906
1935
  expect((await app.inject({
1907
1936
  method: "GET",
1908
1937
  url: baseUrl,
@@ -1911,6 +1940,7 @@ var HttpTestHarness = class {
1911
1940
  });
1912
1941
  if (enabledRoutes.has("create")) it("admin should access create endpoint", async () => {
1913
1942
  const { app, auth, fixtures } = this.getOptions();
1943
+ const baseUrl = this.getBaseUrl();
1914
1944
  const res = await app.inject({
1915
1945
  method: "POST",
1916
1946
  url: baseUrl,
@@ -1933,11 +1963,12 @@ var HttpTestHarness = class {
1933
1963
  * Tests that invalid payloads return 400.
1934
1964
  */
1935
1965
  runValidation() {
1936
- const { resource, baseUrl, enabledRoutes } = this;
1966
+ const { resource, enabledRoutes } = this;
1937
1967
  if (!enabledRoutes.has("create")) return;
1938
1968
  describe(`${resource.displayName} HTTP Validation`, () => {
1939
1969
  it("POST with invalid payload should not return 2xx", async () => {
1940
1970
  const { app, auth, fixtures } = this.getOptions();
1971
+ const baseUrl = this.getBaseUrl();
1941
1972
  if (!fixtures.invalid) return;
1942
1973
  const res = await app.inject({
1943
1974
  method: "POST",
@@ -1963,6 +1994,7 @@ var HttpTestHarness = class {
1963
1994
  *
1964
1995
  * createHttpTestHarness(jobResource, () => ({
1965
1996
  * app: ctx.app,
1997
+ * apiPrefix: '',
1966
1998
  * fixtures: { valid: { title: 'Test' } },
1967
1999
  * auth: createBetterAuthProvider({ ... }),
1968
2000
  * })).runAll();
@@ -1,5 +1,5 @@
1
1
  import { a as AUTHENTICATED_SCOPE, c as getOrgId, d as hasOrgAccess, f as isAuthenticated, l as getOrgRoles, m as isMember, n as ElevationOptions, o as PUBLIC_SCOPE, p as isElevated, s as RequestScope, t as ElevationEvent, u as getTeamId } from "../elevation-DGo5shaX.mjs";
2
- import { A as PaginationParams, D as CrudRepository, I as PipelineConfig, K as HookSystem, O as InferDoc, S as RouteHandler, _ as ControllerLike, b as IControllerResponse, g as ControllerHandler, j as QueryOptions, k as PaginatedResult, l as BaseControllerOptions, n as DataAdapter, v as FastifyHandler, w as ResourceRegistry, x as IRequestContext, y as IController } from "../interface-e9XfSsUV.mjs";
2
+ import { A as PaginationParams, D as CrudRepository, I as PipelineConfig, K as HookSystem, O as InferDoc, S as RouteHandler, _ as ControllerLike, b as IControllerResponse, g as ControllerHandler, j as QueryOptions, k as PaginatedResult, l as BaseControllerOptions, n as DataAdapter, v as FastifyHandler, w as ResourceRegistry, x as IRequestContext, y as IController } from "../interface-Dm4-jnia.mjs";
3
3
  import { n as FieldPermissionMap } from "../fields-Bi_AVKSo.mjs";
4
4
  import { i as UserBase, n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "../types-RLkFVgaw.mjs";
5
5
  import { FastifyInstance, FastifyReply, FastifyRequest, RouteHandlerMethod, RouteHandlerMethod as RouteHandlerMethod$1 } from "fastify";
@@ -363,7 +363,7 @@ interface ResourceConfig<TDoc = AnyRecord> {
363
363
  * Override to match your schema: 'workspaceId', 'tenantId', 'teamId', etc.
364
364
  * Takes effect when org context is present (via multiTenant preset).
365
365
  */
366
- tenantField?: string;
366
+ tenantField?: string | false;
367
367
  /**
368
368
  * Primary key field name (default: '_id').
369
369
  * Override for non-MongoDB adapters (e.g., 'id' for SQL databases).
@@ -1,5 +1,5 @@
1
1
  import "../elevation-DGo5shaX.mjs";
2
- import "../interface-e9XfSsUV.mjs";
2
+ import "../interface-Dm4-jnia.mjs";
3
3
  import "../types-RLkFVgaw.mjs";
4
4
  import { AnyRecord, OpenApiSchemas, ParsedQuery, QueryParserInterface } from "../types/index.mjs";
5
5
  import { a as NotFoundError, c as RateLimitError, d as ValidationError, f as createError, i as ForbiddenError, l as ServiceUnavailableError, n as ConflictError, o as OrgAccessDeniedError, p as isArcError, r as ErrorDetails, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-DAWRdiYP.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc",
3
- "version": "2.1.3",
3
+ "version": "2.2.0",
4
4
  "description": "Resource-oriented backend framework for Fastify — clean, minimal, powerful, tree-shakable",
5
5
  "type": "module",
6
6
  "exports": {
@@ -191,7 +191,7 @@
191
191
  "node": ">=22"
192
192
  },
193
193
  "peerDependencies": {
194
- "@classytic/mongokit": "^3.2.3",
194
+ "@classytic/mongokit": "^3.2.4",
195
195
  "@classytic/streamline": ">=1.0.0",
196
196
  "@fastify/cors": "^11.0.0",
197
197
  "@fastify/helmet": "^13.0.0",