@classytic/arc 2.4.3 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -6
- package/dist/{BaseController-CkM5dUh_.mjs → BaseController-AbbRx3e0.mjs} +5 -2
- package/dist/adapters/index.d.mts +2 -2
- package/dist/adapters/index.mjs +1 -1
- package/dist/{adapters-DTC4Ug66.mjs → adapters-CTn28N4y.mjs} +72 -11
- package/dist/audit/index.d.mts +1 -1
- package/dist/audit/index.mjs +11 -1
- package/dist/audit/mongodb.d.mts +1 -1
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.mjs +2 -2
- package/dist/cli/commands/init.mjs +12 -9
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +2 -2
- package/dist/{createApp-CBgVaFyh.mjs → createApp-Bol7DLUf.mjs} +404 -279
- package/dist/{defineResource-B22gcNvn.mjs → defineResource-bVKHjQzE.mjs} +116 -19
- package/dist/discovery/index.mjs +1 -1
- package/dist/docs/index.d.mts +1 -1
- package/dist/dynamic/index.d.mts +1 -1
- package/dist/dynamic/index.mjs +2 -2
- package/dist/{elevation-Ca_yveIO.d.mts → elevation-C_taLQrM.d.mts} +27 -1
- package/dist/{errorHandler-DMbGdzBG.mjs → errorHandler-r2595m8T.mjs} +1 -1
- package/dist/{errors-CPpvPHT0.d.mts → errors-CcVbl1-T.d.mts} +17 -1
- package/dist/{errors-rxhfP7Hf.mjs → errors-NoQKsbAT.mjs} +23 -1
- package/dist/{eventPlugin-iGrSEmwJ.d.mts → eventPlugin-DW45v4V5.d.mts} +30 -2
- package/dist/events/index.d.mts +2 -2
- package/dist/events/index.mjs +40 -10
- package/dist/factory/index.d.mts +44 -23
- package/dist/factory/index.mjs +136 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +3 -3
- package/dist/idempotency/mongodb.d.mts +1 -1
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-yhxyjqNb.d.mts → index-BIsZ_su5.d.mts} +4 -8
- package/dist/{index-BL8CaQih.d.mts → index-Cb3gtbg7.d.mts} +2 -2
- package/dist/{index-Diqcm14c.d.mts → index-NGZksqM5.d.mts} +30 -1
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +8 -7
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +4 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/{interface-DGmPxakH.d.mts → interface-DDW43OmS.d.mts} +194 -13
- package/dist/{mongodb-CUpYfxfD.d.mts → mongodb-kltrBPa1.d.mts} +10 -0
- package/dist/{mongodb-bga9AbkD.d.mts → mongodb-pMvOlR5_.d.mts} +1 -1
- package/dist/org/index.d.mts +1 -1
- package/dist/org/index.mjs +1 -1
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +2 -2
- package/dist/{permissions-Jk5x3sxz.mjs → permissions-C8ImI8gC.mjs} +44 -2
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +3 -3
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/{presets-OMPaHMTY.mjs → presets-BMfdy34e.mjs} +2 -2
- package/dist/{redis-CQ5YxMC5.d.mts → redis-D0Qc-9EW.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/{resourceToTools-PMFE8HIv.mjs → resourceToTools-DH3c3e-T.mjs} +81 -7
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +2 -2
- package/dist/{sse-BkViJPlT.mjs → sse-BF7GR7IB.mjs} +1 -1
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +1 -1
- package/dist/types/index.d.mts +3 -3
- package/dist/types/index.mjs +23 -2
- package/dist/{types-C6TQjtdi.mjs → types-BhtYdxZU.mjs} +26 -1
- package/dist/{types-Dt0-AI6E.d.mts → types-D5hJ-k_3.d.mts} +189 -6
- package/dist/{types-BJmgxNbF.d.mts → types-D5rjsS_i.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +1 -1
- package/package.json +6 -5
- package/skills/arc/SKILL.md +104 -4
- package/skills/arc/references/mcp.md +160 -2
- /package/dist/{interface-B4awm1RJ.d.mts → interface-gr-7qo9j.d.mts} +0 -0
package/dist/scope/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as AUTHENTICATED_SCOPE, c as
|
|
1
|
+
import { _ as isMember, a as AUTHENTICATED_SCOPE, c as getOrgContext, d as getTeamId, f as getUserId, g as isElevated, h as isAuthenticated, i as elevationPlugin, l as getOrgId, m as hasOrgAccess, n as ElevationOptions, o as PUBLIC_SCOPE, p as getUserRoles, r as _default, s as RequestScope, t as ElevationEvent, u as getOrgRoles } from "../elevation-C_taLQrM.mjs";
|
|
2
2
|
import { FastifyReply, FastifyRequest } from "fastify";
|
|
3
3
|
|
|
4
4
|
//#region src/scope/rateLimitKey.d.ts
|
|
@@ -29,4 +29,4 @@ interface ResolveOrgFromHeaderOptions {
|
|
|
29
29
|
*/
|
|
30
30
|
declare function resolveOrgFromHeader(options: ResolveOrgFromHeaderOptions): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
31
31
|
//#endregion
|
|
32
|
-
export { AUTHENTICATED_SCOPE, type ElevationEvent, type ElevationOptions, PUBLIC_SCOPE, type RateLimitKeyContext, type RequestScope, type ResolveOrgFromHeaderOptions, type TenantKeyGeneratorOptions, createTenantKeyGenerator, _default as elevationPlugin, elevationPlugin as elevationPluginFn, getOrgId, getOrgRoles, getTeamId, getUserId, getUserRoles, hasOrgAccess, isAuthenticated, isElevated, isMember, resolveOrgFromHeader };
|
|
32
|
+
export { AUTHENTICATED_SCOPE, type ElevationEvent, type ElevationOptions, PUBLIC_SCOPE, type RateLimitKeyContext, type RequestScope, type ResolveOrgFromHeaderOptions, type TenantKeyGeneratorOptions, createTenantKeyGenerator, _default as elevationPlugin, elevationPlugin as elevationPluginFn, getOrgContext, getOrgId, getOrgRoles, getTeamId, getUserId, getUserRoles, hasOrgAccess, isAuthenticated, isElevated, isMember, resolveOrgFromHeader };
|
package/dist/scope/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as getOrgRoles, c as getUserRoles, d as isElevated, f as isMember, i as getOrgId, l as hasOrgAccess, n as PUBLIC_SCOPE, o as getTeamId, r as getOrgContext, s as getUserId, t as AUTHENTICATED_SCOPE, u as isAuthenticated } from "../types-BhtYdxZU.mjs";
|
|
2
2
|
import { n as normalizeRoles } from "../types-ZUu_h0jp.mjs";
|
|
3
3
|
import { n as elevation_default, t as elevationPlugin } from "../elevation-BEdACOLB.mjs";
|
|
4
4
|
//#region src/scope/rateLimitKey.ts
|
|
@@ -75,4 +75,4 @@ function resolveOrgFromHeader(options) {
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
//#endregion
|
|
78
|
-
export { AUTHENTICATED_SCOPE, PUBLIC_SCOPE, createTenantKeyGenerator, elevation_default as elevationPlugin, elevationPlugin as elevationPluginFn, getOrgId, getOrgRoles, getTeamId, getUserId, getUserRoles, hasOrgAccess, isAuthenticated, isElevated, isMember, resolveOrgFromHeader };
|
|
78
|
+
export { AUTHENTICATED_SCOPE, PUBLIC_SCOPE, createTenantKeyGenerator, elevation_default as elevationPlugin, elevationPlugin as elevationPluginFn, getOrgContext, getOrgId, getOrgRoles, getTeamId, getUserId, getUserRoles, hasOrgAccess, isAuthenticated, isElevated, isMember, resolveOrgFromHeader };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { i as getOrgId, n as PUBLIC_SCOPE } from "./types-BhtYdxZU.mjs";
|
|
3
3
|
import { t as arcLog } from "./logger-Dz3j1ItV.mjs";
|
|
4
4
|
import fp from "fastify-plugin";
|
|
5
5
|
//#region src/plugins/sse.ts
|
package/dist/testing/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { r as CreateAppOptions } from "../types-
|
|
1
|
+
import { Bt as ResourceDefinition, Ht as CrudRepository, l as AnyRecord } from "../interface-DDW43OmS.mjs";
|
|
2
|
+
import { r as CreateAppOptions } from "../types-D5hJ-k_3.mjs";
|
|
3
3
|
import Fastify, { FastifyInstance, FastifyServerOptions } from "fastify";
|
|
4
4
|
import { Connection } from "mongoose";
|
|
5
5
|
import { Mock } from "vitest";
|
package/dist/testing/index.mjs
CHANGED
|
@@ -1752,7 +1752,7 @@ function runEventTests(resourceName, displayName, events) {
|
|
|
1752
1752
|
* ```
|
|
1753
1753
|
*/
|
|
1754
1754
|
async function createTestApp(options = {}) {
|
|
1755
|
-
const { createApp } = await import("../createApp-
|
|
1755
|
+
const { createApp } = await import("../createApp-Bol7DLUf.mjs").then((n) => n.r);
|
|
1756
1756
|
const { useInMemoryDb = true, mongoUri: providedMongoUri, ...appOptions } = options;
|
|
1757
1757
|
const defaultAuth = {
|
|
1758
1758
|
type: "jwt",
|
package/dist/types/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as AUTHENTICATED_SCOPE,
|
|
2
|
-
import { $ as
|
|
1
|
+
import { _ as isMember, a as AUTHENTICATED_SCOPE, d as getTeamId, g as isElevated, h as isAuthenticated, l as getOrgId, m as hasOrgAccess, n as ElevationOptions, o as PUBLIC_SCOPE, s as RequestScope, t as ElevationEvent, u as getOrgRoles } from "../elevation-C_taLQrM.mjs";
|
|
2
|
+
import { $ as RegistryEntry, A as GracefulShutdownOptions, B as LookupOption, C as CrudSchemas, D as FastifyWithAuth, E as FastifyRequestExtras, F as InferResourceDoc, Ft as IControllerResponse, G as OwnershipCheck, Gt as PaginationParams, H as MiddlewareHandler, Ht as CrudRepository, I as IntrospectionData, It as IRequestContext, J as PresetFunction, K as ParsedQuery, Kt as QueryOptions, L as IntrospectionPluginOptions, Lt as RouteHandler, M as HealthOptions, Mt as ControllerLike, N as InferAdapterDoc, Nt as FastifyHandler, O as FastifyWithDecorators, P as InferDocType, Pt as IController, Q as RateLimitConfig, R as JWTPayload, S as CrudRouterOptions, St as getUserId, T as EventsDecorator, U as ObjectId, Ut as InferDoc, V as MiddlewareConfig, W as OpenApiSchemas, Wt as PaginatedResult, X as PresetResult, Y as PresetHook, Z as QueryParserInterface, _ as AuthenticatorContext, _t as UserLike, at as ResourceConfig, b as CrudController, bt as ValidationResult, c as AdditionalRoute, ct as ResourceMetadata, d as ArcDecorator, dt as RouteSchemaOptions, et as RegistryStats, f as ArcInternalMetadata, ft as ServiceContext, g as Authenticator, gt as TypedResourceConfig, h as AuthPluginOptions, ht as TypedRepository, it as ResourceCacheConfig, j as HealthCheck, jt as ControllerHandler, k as FieldRule, l as AnyRecord, lt as ResourcePermissions, m as AuthHelpers, mt as TypedController, nt as RequestIdOptions, ot as ResourceHookContext, p as ArcRequest, pt as TokenPair, q as PopulateOption, rt as RequestWithExtras, st as ResourceHooks, tt as RequestContext, u as ApiResponse, ut as RouteHandlerMethod, v as ConfigError, vt as UserOrganization, w as EventDefinition, wt as BaseControllerOptions, x as CrudRouteKey, xt as envelope, y as ControllerQueryOptions, yt as ValidateOptions, z as JwtContext } from "../interface-DDW43OmS.mjs";
|
|
3
3
|
import { i as UserBase, n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "../types-BNUccdcf.mjs";
|
|
4
|
-
export { AUTHENTICATED_SCOPE, AdditionalRoute, AnyRecord, ApiResponse, ArcDecorator, ArcInternalMetadata, AuthHelpers, AuthPluginOptions, Authenticator, AuthenticatorContext, BaseControllerOptions, ConfigError, ControllerHandler, ControllerLike, ControllerQueryOptions, CrudController, CrudRepository, CrudRouteKey, CrudRouterOptions, CrudSchemas, ElevationEvent, ElevationOptions, EventDefinition, EventsDecorator, FastifyHandler, FastifyRequestExtras, FastifyWithAuth, FastifyWithDecorators, FieldRule, GracefulShutdownOptions, HealthCheck, HealthOptions, IController, IControllerResponse, IRequestContext, InferAdapterDoc, InferDoc, InferDocType, InferResourceDoc, IntrospectionData, IntrospectionPluginOptions, JWTPayload, JwtContext, LookupOption, MiddlewareConfig, MiddlewareHandler, ObjectId, OpenApiSchemas, OwnershipCheck, PUBLIC_SCOPE, PaginatedResult, PaginationParams, ParsedQuery, PermissionCheck, PermissionContext, PermissionResult, PopulateOption, PresetFunction, PresetHook, PresetResult, QueryOptions, QueryParserInterface, RateLimitConfig, RegistryEntry, RegistryStats, RequestContext, RequestIdOptions, RequestScope, RequestWithExtras, ResourceCacheConfig, ResourceConfig, ResourceHooks, ResourceMetadata, ResourcePermissions, RouteHandler, RouteHandlerMethod, RouteSchemaOptions, ServiceContext, TokenPair, TypedController, TypedRepository, TypedResourceConfig, UserBase, UserLike, UserOrganization, ValidateOptions, ValidationResult, getOrgId, getOrgRoles, getTeamId, getUserId, hasOrgAccess, isAuthenticated, isElevated, isMember };
|
|
4
|
+
export { AUTHENTICATED_SCOPE, AdditionalRoute, AnyRecord, ApiResponse, ArcDecorator, ArcInternalMetadata, ArcRequest, AuthHelpers, AuthPluginOptions, Authenticator, AuthenticatorContext, BaseControllerOptions, ConfigError, ControllerHandler, ControllerLike, ControllerQueryOptions, CrudController, CrudRepository, CrudRouteKey, CrudRouterOptions, CrudSchemas, ElevationEvent, ElevationOptions, EventDefinition, EventsDecorator, FastifyHandler, FastifyRequestExtras, FastifyWithAuth, FastifyWithDecorators, FieldRule, GracefulShutdownOptions, HealthCheck, HealthOptions, IController, IControllerResponse, IRequestContext, InferAdapterDoc, InferDoc, InferDocType, InferResourceDoc, IntrospectionData, IntrospectionPluginOptions, JWTPayload, JwtContext, LookupOption, MiddlewareConfig, MiddlewareHandler, ObjectId, OpenApiSchemas, OwnershipCheck, PUBLIC_SCOPE, PaginatedResult, PaginationParams, ParsedQuery, PermissionCheck, PermissionContext, PermissionResult, PopulateOption, PresetFunction, PresetHook, PresetResult, QueryOptions, QueryParserInterface, RateLimitConfig, RegistryEntry, RegistryStats, RequestContext, RequestIdOptions, RequestScope, RequestWithExtras, ResourceCacheConfig, ResourceConfig, ResourceHookContext, ResourceHooks, ResourceMetadata, ResourcePermissions, RouteHandler, RouteHandlerMethod, RouteSchemaOptions, ServiceContext, TokenPair, TypedController, TypedRepository, TypedResourceConfig, UserBase, UserLike, UserOrganization, ValidateOptions, ValidationResult, envelope, getOrgId, getOrgRoles, getTeamId, getUserId, hasOrgAccess, isAuthenticated, isElevated, isMember };
|
package/dist/types/index.mjs
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as getOrgRoles, d as isElevated, f as isMember, i as getOrgId, l as hasOrgAccess, n as PUBLIC_SCOPE, o as getTeamId, t as AUTHENTICATED_SCOPE, u as isAuthenticated } from "../types-BhtYdxZU.mjs";
|
|
2
2
|
//#region src/types/index.ts
|
|
3
3
|
/**
|
|
4
|
+
* Response envelope helper — wraps data in Arc's standard `{ success, data }` format.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { envelope } from '@classytic/arc';
|
|
9
|
+
*
|
|
10
|
+
* handler: async (req, reply) => {
|
|
11
|
+
* const data = await getResults();
|
|
12
|
+
* return envelope(data);
|
|
13
|
+
* // → { success: true, data }
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
function envelope(data, meta) {
|
|
18
|
+
return {
|
|
19
|
+
success: true,
|
|
20
|
+
data,
|
|
21
|
+
...meta
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
4
25
|
* Extract user ID from a user object (supports both id and _id)
|
|
5
26
|
*/
|
|
6
27
|
function getUserId(user) {
|
|
@@ -9,4 +30,4 @@ function getUserId(user) {
|
|
|
9
30
|
return id ? String(id) : void 0;
|
|
10
31
|
}
|
|
11
32
|
//#endregion
|
|
12
|
-
export { AUTHENTICATED_SCOPE, PUBLIC_SCOPE, getOrgId, getOrgRoles, getTeamId, getUserId, hasOrgAccess, isAuthenticated, isElevated, isMember };
|
|
33
|
+
export { AUTHENTICATED_SCOPE, PUBLIC_SCOPE, envelope, getOrgId, getOrgRoles, getTeamId, getUserId, hasOrgAccess, isAuthenticated, isElevated, isMember };
|
|
@@ -58,9 +58,34 @@ function getUserRoles(scope) {
|
|
|
58
58
|
if (scope.kind === "member") return scope.userRoles;
|
|
59
59
|
return [];
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Org context — canonical extraction from a Fastify request.
|
|
63
|
+
*
|
|
64
|
+
* Works regardless of auth type (JWT, Better Auth, custom) by reading
|
|
65
|
+
* `request.scope` and `request.user`. Eliminates the need for each resource
|
|
66
|
+
* to re-invent org extraction from headers/user/scope.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* import { getOrgContext } from '@classytic/arc/scope';
|
|
71
|
+
*
|
|
72
|
+
* handler: async (request, reply) => {
|
|
73
|
+
* const { userId, organizationId, roles, orgRoles } = getOrgContext(request);
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
function getOrgContext(request) {
|
|
78
|
+
const scope = request.scope ?? { kind: "public" };
|
|
79
|
+
return {
|
|
80
|
+
userId: getUserId(scope) ?? request.user?.id ?? request.user?._id,
|
|
81
|
+
organizationId: getOrgId(scope) ?? request.user?.organizationId ?? request.headers?.["x-organization-id"],
|
|
82
|
+
roles: getUserRoles(scope),
|
|
83
|
+
orgRoles: getOrgRoles(scope)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
61
86
|
/** Default public scope — used as initial decoration value */
|
|
62
87
|
const PUBLIC_SCOPE = Object.freeze({ kind: "public" });
|
|
63
88
|
/** Default authenticated scope — used when user is logged in but no org */
|
|
64
89
|
const AUTHENTICATED_SCOPE = Object.freeze({ kind: "authenticated" });
|
|
65
90
|
//#endregion
|
|
66
|
-
export {
|
|
91
|
+
export { getOrgRoles as a, getUserRoles as c, isElevated as d, isMember as f, getOrgId as i, hasOrgAccess as l, PUBLIC_SCOPE as n, getTeamId as o, getOrgContext as r, getUserId as s, AUTHENTICATED_SCOPE as t, isAuthenticated as u };
|
|
@@ -1,14 +1,133 @@
|
|
|
1
|
-
import { n as ElevationOptions } from "./elevation-
|
|
2
|
-
import {
|
|
1
|
+
import { n as ElevationOptions } from "./elevation-C_taLQrM.mjs";
|
|
2
|
+
import { g as Authenticator } from "./interface-DDW43OmS.mjs";
|
|
3
3
|
import { t as ExternalOpenApiPaths } from "./externalPaths-DpO-s7r8.mjs";
|
|
4
4
|
import { i as CacheStore } from "./interface-D_BWALyZ.mjs";
|
|
5
5
|
import { r as QueryCachePluginOptions } from "./queryCachePlugin-DcmETvcB.mjs";
|
|
6
6
|
import { i as EventTransport } from "./EventTransport-wc5hSLik.mjs";
|
|
7
|
-
import { t as EventPluginOptions } from "./eventPlugin-
|
|
7
|
+
import { t as EventPluginOptions } from "./eventPlugin-DW45v4V5.mjs";
|
|
8
8
|
import { c as MetricsOptions, d as SSEOptions, m as CachingOptions, r as VersioningOptions, t as ErrorHandlerOptions } from "./errorHandler-Do4vVQ1f.mjs";
|
|
9
|
-
import { r as IdempotencyStore } from "./interface-
|
|
9
|
+
import { r as IdempotencyStore } from "./interface-gr-7qo9j.mjs";
|
|
10
10
|
import { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest, FastifyServerOptions } from "fastify";
|
|
11
11
|
|
|
12
|
+
//#region src/factory/loadResources.d.ts
|
|
13
|
+
/**
|
|
14
|
+
* loadResources — Auto-discover resource files from a directory.
|
|
15
|
+
*
|
|
16
|
+
* Scans for `*.resource.{ts,js,mts,mjs}` files, imports each,
|
|
17
|
+
* and collects their default exports. No barrel file needed.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { createApp, loadResources } from '@classytic/arc/factory';
|
|
22
|
+
*
|
|
23
|
+
* // Recommended: import.meta.url — works in both src/ (dev) and dist/ (prod)
|
|
24
|
+
* const app = await createApp({
|
|
25
|
+
* resources: await loadResources(import.meta.url),
|
|
26
|
+
* auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // Or explicit path (must match runtime layout)
|
|
30
|
+
* const app2 = await createApp({
|
|
31
|
+
* resources: await loadResources('./src/resources'),
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* File convention:
|
|
36
|
+
* ```
|
|
37
|
+
* src/resources/
|
|
38
|
+
* product/product.resource.ts → export default defineResource({ name: 'product', ... })
|
|
39
|
+
* order/order.resource.ts → export default defineResource({ name: 'order', ... })
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Resource interface — the contract between `loadResources`/`createApp` and resource definitions.
|
|
44
|
+
*
|
|
45
|
+
* Matches the shape of `ResourceDefinition` from `defineResource()` without requiring
|
|
46
|
+
* the import. All properties except `toPlugin` are optional so plain objects work too:
|
|
47
|
+
*
|
|
48
|
+
* ```typescript
|
|
49
|
+
* // Full resource (from defineResource)
|
|
50
|
+
* const product = defineResource({ name: 'product', ... }); // satisfies ResourceLike
|
|
51
|
+
*
|
|
52
|
+
* // Minimal resource (plain object)
|
|
53
|
+
* const simple: ResourceLike = { name: 'ping', toPlugin: () => () => {} };
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
interface ResourceLike {
|
|
57
|
+
/** Plugin factory — called by createApp to register routes */
|
|
58
|
+
toPlugin: () => unknown;
|
|
59
|
+
/** Resource name (used for route generation, logging, duplicate detection) */
|
|
60
|
+
name?: string;
|
|
61
|
+
/** Route prefix (default: `/${name}s`) */
|
|
62
|
+
prefix?: string;
|
|
63
|
+
/** Skip the global `resourcePrefix` from createApp — register at root */
|
|
64
|
+
skipGlobalPrefix?: boolean;
|
|
65
|
+
/** Display name for docs/OpenAPI */
|
|
66
|
+
displayName?: string;
|
|
67
|
+
/** Applied preset names */
|
|
68
|
+
_appliedPresets?: string[];
|
|
69
|
+
}
|
|
70
|
+
interface LoadResourcesOptions {
|
|
71
|
+
/** File pattern suffix (default: '.resource'). Matches `*.resource.{ts,js,mts,mjs}`. */
|
|
72
|
+
suffix?: string;
|
|
73
|
+
/** Recurse into subdirectories (default: true) */
|
|
74
|
+
recursive?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Resource names to exclude. Matched against the resource's `.name` property
|
|
77
|
+
* after import, so you use the resource name (not the filename).
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* await loadResources('./src/resources', { exclude: ['debug', 'legacy-report'] })
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
exclude?: string[];
|
|
85
|
+
/**
|
|
86
|
+
* Resource names to include. When set, only matching resources are returned.
|
|
87
|
+
* Takes priority over `exclude`.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* // Only load these two resources (useful for testing or microservice splits)
|
|
92
|
+
* await loadResources('./src/resources', { include: ['product', 'order'] })
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
include?: string[];
|
|
96
|
+
/**
|
|
97
|
+
* Suppress warning logs for skipped/failed files.
|
|
98
|
+
* Useful when your resources directory contains factory files or helpers
|
|
99
|
+
* that don't export a resource (e.g., `account.resource.ts` exporting a factory).
|
|
100
|
+
*
|
|
101
|
+
* @default false
|
|
102
|
+
*/
|
|
103
|
+
silent?: boolean;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Scan a directory for resource files and import their default exports.
|
|
107
|
+
*
|
|
108
|
+
* Accepts a directory path OR `import.meta.url` (file:// URL).
|
|
109
|
+
* When given a URL, resolves to the directory containing that file —
|
|
110
|
+
* so `loadResources(import.meta.url)` works in both dev (`src/`) and
|
|
111
|
+
* production (`dist/`) without path gymnastics.
|
|
112
|
+
*
|
|
113
|
+
* @param dir - Directory path, or `import.meta.url` (file:// URL resolved to its dirname)
|
|
114
|
+
* @param options - Pattern and recursion options
|
|
115
|
+
* @returns Array of resource definitions (anything with `.toPlugin()`)
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* // Works from both src/ and dist/ — resolves relative to the calling file
|
|
120
|
+
* await loadResources(import.meta.url);
|
|
121
|
+
*
|
|
122
|
+
* // Subdirectory relative to the calling file
|
|
123
|
+
* await loadResources(import.meta.url, { suffix: '.resource' });
|
|
124
|
+
*
|
|
125
|
+
* // Explicit path (must match runtime layout)
|
|
126
|
+
* await loadResources('./src/resources');
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
declare function loadResources(dir: string, options?: LoadResourcesOptions): Promise<ResourceLike[]>;
|
|
130
|
+
//#endregion
|
|
12
131
|
//#region src/factory/types.d.ts
|
|
13
132
|
type CorsOptions = Record<string, unknown> & {
|
|
14
133
|
origin?: unknown;
|
|
@@ -473,8 +592,72 @@ interface CreateAppOptions {
|
|
|
473
592
|
ajv?: {
|
|
474
593
|
keywords?: string[];
|
|
475
594
|
};
|
|
476
|
-
/**
|
|
595
|
+
/**
|
|
596
|
+
* Resources to register automatically.
|
|
597
|
+
* Each resource's `.toPlugin()` is called and registered for you.
|
|
598
|
+
*
|
|
599
|
+
* @example
|
|
600
|
+
* ```ts
|
|
601
|
+
* const app = await createApp({
|
|
602
|
+
* resources: [productResource, orderResource, userResource],
|
|
603
|
+
* auth: { type: 'jwt', jwt: { secret: 'xxx' } },
|
|
604
|
+
* });
|
|
605
|
+
* ```
|
|
606
|
+
*/
|
|
607
|
+
resources?: Array<ResourceLike>;
|
|
608
|
+
/**
|
|
609
|
+
* URL prefix for all auto-registered resources.
|
|
610
|
+
* Applied only to resources in the `resources` array — not to `plugins()`.
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* ```ts
|
|
614
|
+
* const app = await createApp({
|
|
615
|
+
* resourcePrefix: '/api/v1',
|
|
616
|
+
* resources: await loadResources(import.meta.url),
|
|
617
|
+
* });
|
|
618
|
+
* // product → /api/v1/products, order → /api/v1/orders
|
|
619
|
+
* ```
|
|
620
|
+
*/
|
|
621
|
+
resourcePrefix?: string;
|
|
622
|
+
/**
|
|
623
|
+
* Custom plugin registration — runs after Arc core (security, auth, events)
|
|
624
|
+
* but before `bootstrap` and `resources`.
|
|
625
|
+
*
|
|
626
|
+
* Use this for infrastructure setup: database connections, OpenAPI docs,
|
|
627
|
+
* webhook plugins, SSE wiring, etc.
|
|
628
|
+
*/
|
|
477
629
|
plugins?: (fastify: FastifyInstance) => Promise<void>;
|
|
630
|
+
/**
|
|
631
|
+
* Bootstrap functions — run after `plugins()` but before `resources`.
|
|
632
|
+
*
|
|
633
|
+
* Use this for domain initialization that needs infrastructure ready
|
|
634
|
+
* (DB connected, events wired, Redis available) but must complete
|
|
635
|
+
* before resources register (e.g., engine singletons, event handlers,
|
|
636
|
+
* seed data, connection verification).
|
|
637
|
+
*
|
|
638
|
+
* Boot order:
|
|
639
|
+
* ```
|
|
640
|
+
* 1. Arc core (security, auth, events)
|
|
641
|
+
* 2. plugins() ← infra (DB, SSE, docs)
|
|
642
|
+
* 3. bootstrap[] ← domain init (singletons, event handlers)
|
|
643
|
+
* 4. resources[] ← auto-discovered routes
|
|
644
|
+
* ```
|
|
645
|
+
*
|
|
646
|
+
* @example
|
|
647
|
+
* ```ts
|
|
648
|
+
* const app = await createApp({
|
|
649
|
+
* plugins: async (f) => { await connectDB(); await f.register(docsPlugin); },
|
|
650
|
+
* bootstrap: [inventoryInit, accountingInit, loyaltyInit],
|
|
651
|
+
* resources: await loadResources(import.meta.url),
|
|
652
|
+
* });
|
|
653
|
+
* ```
|
|
654
|
+
*/
|
|
655
|
+
bootstrap?: Array<(fastify: FastifyInstance) => void | Promise<void>>;
|
|
656
|
+
/**
|
|
657
|
+
* Hook called after resources are registered but before the app is ready.
|
|
658
|
+
* Use for post-registration wiring (e.g., cross-resource event subscriptions).
|
|
659
|
+
*/
|
|
660
|
+
afterResources?: (fastify: FastifyInstance) => void | Promise<void>;
|
|
478
661
|
/** Hook called after all plugins are loaded and the app is ready */
|
|
479
662
|
onReady?: (fastify: FastifyInstance) => void | Promise<void>;
|
|
480
663
|
/** Hook called when the app is shutting down */
|
|
@@ -507,4 +690,4 @@ interface RawBodyOptions {
|
|
|
507
690
|
runFirst?: boolean;
|
|
508
691
|
}
|
|
509
692
|
//#endregion
|
|
510
|
-
export { CustomPluginAuthOption as a, RawBodyOptions as c, CustomAuthenticatorOption as i, UnderPressureOptions as l, BetterAuthOption as n, JwtAuthOption as o, CreateAppOptions as r, MultipartOptions as s, AuthOption as t };
|
|
693
|
+
export { CustomPluginAuthOption as a, RawBodyOptions as c, ResourceLike as d, loadResources as f, CustomAuthenticatorOption as i, UnderPressureOptions as l, BetterAuthOption as n, JwtAuthOption as o, CreateAppOptions as r, MultipartOptions as s, AuthOption as t, LoadResourcesOptions as u };
|
package/dist/utils/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as NotFoundError, c as RateLimitError, d as ValidationError,
|
|
1
|
+
import { K as ParsedQuery, W as OpenApiSchemas, Z as QueryParserInterface, l as AnyRecord } from "../interface-DDW43OmS.mjs";
|
|
2
|
+
import { a as NotFoundError, c as RateLimitError, d as ValidationError, i as ForbiddenError, l as ServiceUnavailableError, m as isArcError, n as ConflictError, o as OrgAccessDeniedError, p as createError, r as ErrorDetails, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-CcVbl1-T.mjs";
|
|
3
3
|
import { a as CircuitBreakerStats, c as createCircuitBreakerRegistry, i as CircuitBreakerRegistry, n as CircuitBreakerError, o as CircuitState, r as CircuitBreakerOptions, s as createCircuitBreaker, t as CircuitBreaker } from "../circuitBreaker-JP2GdJ4b.mjs";
|
|
4
4
|
import { FastifyInstance } from "fastify";
|
|
5
5
|
|
package/dist/utils/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as createQueryParser, t as ArcQueryParser } from "../queryParser-CgCtsjti.mjs";
|
|
2
2
|
import { a as createCircuitBreaker, i as CircuitState, n as CircuitBreakerError, o as createCircuitBreakerRegistry, r as CircuitBreakerRegistry, t as CircuitBreaker } from "../circuitBreaker-BOBOpN2w.mjs";
|
|
3
3
|
import { _ as defineCompensation, a as getListQueryParams, c as listResponse, d as paginateWrapper, f as paginationSchema, g as wrapResponse, h as successResponseSchema, i as getDefaultCrudSchemas, l as messageWrapper, m as responses, n as deleteResponse, o as itemResponse, p as queryParams, r as errorResponseSchema, s as itemWrapper, t as createStateMachine, u as mutationResponse, v as withCompensation } from "../utils-Dc0WhlIl.mjs";
|
|
4
|
-
import { a as OrgAccessDeniedError, c as ServiceUnavailableError,
|
|
4
|
+
import { a as OrgAccessDeniedError, c as ServiceUnavailableError, f as createError, i as NotFoundError, l as UnauthorizedError, n as ConflictError, o as OrgRequiredError, p as isArcError, r as ForbiddenError, s as RateLimitError, t as ArcError, u as ValidationError } from "../errors-NoQKsbAT.mjs";
|
|
5
5
|
import { a as toJsonSchema, i as isZodSchema, n as convertRouteSchema, r as isJsonSchema, t as convertOpenApiSchemas } from "../schemaConverter-DjzHpFam.mjs";
|
|
6
6
|
import { t as hasEvents } from "../typeGuards-Cj5Rgvlg.mjs";
|
|
7
7
|
export { ArcError, ArcQueryParser, CircuitBreaker, CircuitBreakerError, CircuitBreakerRegistry, CircuitState, ConflictError, ForbiddenError, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, ServiceUnavailableError, UnauthorizedError, ValidationError, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createError, createQueryParser, createStateMachine, defineCompensation, deleteResponse, errorResponseSchema, getDefaultCrudSchemas, getListQueryParams, hasEvents, isArcError, isJsonSchema, isZodSchema, itemResponse, itemWrapper, listResponse, messageWrapper, mutationResponse, paginateWrapper, paginationSchema, queryParams, responses, successResponseSchema, toJsonSchema, withCompensation, wrapResponse };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/arc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "Resource-oriented backend framework for Fastify — clean, minimal, powerful, tree-shakable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
"node": ">=22"
|
|
221
221
|
},
|
|
222
222
|
"peerDependencies": {
|
|
223
|
-
"@classytic/mongokit": ">=3.
|
|
223
|
+
"@classytic/mongokit": ">=3.5.0",
|
|
224
224
|
"@classytic/streamline": ">=2.0.0",
|
|
225
225
|
"@fastify/cors": "^11.0.0",
|
|
226
226
|
"@fastify/helmet": "^13.0.0",
|
|
@@ -244,7 +244,7 @@
|
|
|
244
244
|
"fastify-raw-body": "^5.0.0",
|
|
245
245
|
"ioredis": "^5.0.0",
|
|
246
246
|
"mongodb": "^6.0.0 || ^7.0.0",
|
|
247
|
-
"mongoose": "
|
|
247
|
+
"mongoose": ">=9.0.0",
|
|
248
248
|
"pino-pretty": "^13.0.0",
|
|
249
249
|
"zod": "^4.0.0"
|
|
250
250
|
},
|
|
@@ -333,11 +333,12 @@
|
|
|
333
333
|
},
|
|
334
334
|
"dependencies": {
|
|
335
335
|
"fastify-plugin": "^5.0.1",
|
|
336
|
-
"qs": "^6.14.1"
|
|
336
|
+
"qs": "^6.14.1",
|
|
337
|
+
"secure-json-parse": "^4.1.0"
|
|
337
338
|
},
|
|
338
339
|
"devDependencies": {
|
|
339
340
|
"@biomejs/biome": "^2.4.10",
|
|
340
|
-
"@classytic/mongokit": "^3.
|
|
341
|
+
"@classytic/mongokit": "^3.5.2",
|
|
341
342
|
"@fastify/jwt": "^10.0.0",
|
|
342
343
|
"@fastify/multipart": "^9.0.0",
|
|
343
344
|
"@fastify/type-provider-typebox": "^6.0.0",
|
package/skills/arc/SKILL.md
CHANGED
|
@@ -8,11 +8,11 @@ description: |
|
|
|
8
8
|
Triggers: arc, fastify resource, defineResource, createApp, BaseController, arc preset,
|
|
9
9
|
arc auth, arc events, arc jobs, arc websocket, arc mcp, arc plugin, arc testing, arc cli,
|
|
10
10
|
arc permissions, arc hooks, arc pipeline, arc factory, arc cache, arc QueryCache.
|
|
11
|
-
version: 2.
|
|
11
|
+
version: 2.6.0
|
|
12
12
|
license: MIT
|
|
13
13
|
metadata:
|
|
14
14
|
author: Classytic
|
|
15
|
-
version: "2.
|
|
15
|
+
version: "2.6.0"
|
|
16
16
|
tags:
|
|
17
17
|
- fastify
|
|
18
18
|
- rest-api
|
|
@@ -196,6 +196,30 @@ presets: ['softDelete', { name: 'multiTenant', tenantField: 'organizationId' }]
|
|
|
196
196
|
// Bulk: presets: ['bulk'] or bulkPreset({ operations: ['createMany', 'updateMany'] })
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
+
### tenantField — When to Use and When to Disable
|
|
200
|
+
|
|
201
|
+
Arc defaults `tenantField` to `'organizationId'` on BaseController. This silently adds `{ organizationId: scope.organizationId }` to every query when the user has an org context. Correct for per-org resources, wrong for company-wide resources.
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// Per-org resource (default) — each org sees only its own data
|
|
205
|
+
defineResource({ name: 'invoice', ... });
|
|
206
|
+
// → queries auto-scoped: { organizationId: 'org-123' }
|
|
207
|
+
|
|
208
|
+
// Company-wide resource — ALL orgs share the same data
|
|
209
|
+
defineResource({ name: 'account-type', tenantField: false, ... });
|
|
210
|
+
// → no org filter applied, all users see all records
|
|
211
|
+
|
|
212
|
+
// Custom tenant field — your schema uses a different name
|
|
213
|
+
defineResource({ name: 'workspace-item', tenantField: 'workspaceId', ... });
|
|
214
|
+
// → queries scoped by workspaceId instead of organizationId
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
When to use `tenantField: false`:
|
|
218
|
+
- Lookup tables (account types, categories, currencies)
|
|
219
|
+
- Platform-wide settings or config
|
|
220
|
+
- Cross-org reports or analytics
|
|
221
|
+
- Single-tenant apps where org scoping isn't needed
|
|
222
|
+
|
|
199
223
|
## QueryCache
|
|
200
224
|
|
|
201
225
|
TanStack Query-inspired server cache with stale-while-revalidate and auto-invalidation on mutations.
|
|
@@ -314,6 +338,26 @@ const app = await createApp({
|
|
|
314
338
|
|
|
315
339
|
## Hooks
|
|
316
340
|
|
|
341
|
+
**Inline on resource (recommended):**
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
defineResource({
|
|
345
|
+
name: 'chat',
|
|
346
|
+
hooks: {
|
|
347
|
+
beforeCreate: async (ctx) => { ctx.data.slug = slugify(ctx.data.name); },
|
|
348
|
+
afterCreate: async (ctx) => { analytics.track('created', { id: ctx.data._id, user: ctx.user?.id }); },
|
|
349
|
+
beforeUpdate: async (ctx) => { console.log('Updating', ctx.meta?.id, 'existing:', ctx.meta?.existing); },
|
|
350
|
+
afterUpdate: async (ctx) => { await invalidateCache(ctx.data._id); },
|
|
351
|
+
beforeDelete: async (ctx) => { if (ctx.data.isProtected) throw new Error('Cannot delete'); },
|
|
352
|
+
afterDelete: async (ctx) => { await cleanupFiles(ctx.meta?.id); },
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
`ResourceHookContext`: `{ data, user?, meta? }` — `data` is the document, `meta` has `id` and `existing` (for update/delete).
|
|
358
|
+
|
|
359
|
+
**App-level (cross-resource):**
|
|
360
|
+
|
|
317
361
|
```typescript
|
|
318
362
|
import { createHookSystem, beforeCreate, afterUpdate } from '@classytic/arc/hooks';
|
|
319
363
|
|
|
@@ -386,8 +430,10 @@ defineResource({
|
|
|
386
430
|
## Error Classes
|
|
387
431
|
|
|
388
432
|
```typescript
|
|
389
|
-
import { ArcError, NotFoundError, ValidationError,
|
|
390
|
-
throw new NotFoundError('Product not found');
|
|
433
|
+
import { ArcError, NotFoundError, ValidationError, createDomainError } from '@classytic/arc';
|
|
434
|
+
throw new NotFoundError('Product not found'); // 404
|
|
435
|
+
throw createDomainError('MEMBER_NOT_FOUND', 'Not found', 404); // domain error with code
|
|
436
|
+
throw createDomainError('SELF_REFERRAL', 'Cannot self-refer', 422, { field: 'referralCode' });
|
|
391
437
|
```
|
|
392
438
|
|
|
393
439
|
Error handler catches: `ArcError` → `.statusCode` (Fastify) → `.status` (MongoKit, http-errors) → `errorMap` → Mongoose/MongoDB → fallback 500. DB-agnostic — any error with `.status` or `.statusCode` gets the correct HTTP response.
|
|
@@ -481,6 +527,60 @@ src/resources/order/
|
|
|
481
527
|
|
|
482
528
|
Generate: `arc generate resource order --mcp` | Wire: `extraTools: [fulfillOrderTool]`
|
|
483
529
|
|
|
530
|
+
**Auto-load resources** (v2.6.0) — no barrel files, no manual `toPlugin()`:
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
import { createApp, loadResources } from '@classytic/arc/factory';
|
|
534
|
+
|
|
535
|
+
const app = await createApp({
|
|
536
|
+
resources: await loadResources('./src/resources'), // discovers *.resource.ts
|
|
537
|
+
auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
|
|
538
|
+
});
|
|
539
|
+
// loadResources options: exclude, include, suffix, recursive
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
**Import compatibility:** `loadResources()` uses runtime `import()`. Works with relative imports (`./foo.js`) and Node.js `#` subpath imports (`#shared/utils.js` via `package.json` `imports` — both `.js` and `.ts` extensions). Does **NOT** work with tsconfig path aliases (`@/*`, `~/`) — those are compile-time only, Node.js ignores them. Projects using tsconfig aliases should use explicit `resources: [r1, r2]` instead.
|
|
543
|
+
|
|
544
|
+
**Unified role check** (v2.6.0) — checks both platform AND org roles:
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
import { roles } from '@classytic/arc/permissions';
|
|
548
|
+
permissions: {
|
|
549
|
+
create: roles('admin', 'editor'), // works with BA org roles + platform roles
|
|
550
|
+
delete: roles('admin'),
|
|
551
|
+
}
|
|
552
|
+
// Also: requireRoles(['admin'], { includeOrgRoles: true }) for backward compat
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**DX helpers** (v2.4.4):
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
// Typed request for wrapHandler: false routes — no more (req as any).user
|
|
559
|
+
import type { ArcRequest } from '@classytic/arc';
|
|
560
|
+
handler: async (req: ArcRequest, reply) => { req.user?.id; req.scope; req.signal; }
|
|
561
|
+
|
|
562
|
+
// Response envelope — no manual { success, data } wrapping
|
|
563
|
+
import { envelope } from '@classytic/arc';
|
|
564
|
+
reply.send(envelope(data, { total: 100 }));
|
|
565
|
+
|
|
566
|
+
// Canonical org extraction — replaces 19 duplicated patterns
|
|
567
|
+
import { getOrgContext } from '@classytic/arc/scope';
|
|
568
|
+
const { userId, organizationId, roles, orgRoles } = getOrgContext(request);
|
|
569
|
+
|
|
570
|
+
// Domain errors with auto HTTP status mapping
|
|
571
|
+
import { createDomainError } from '@classytic/arc';
|
|
572
|
+
throw createDomainError('SELF_REFERRAL', 'Cannot refer yourself', 422);
|
|
573
|
+
|
|
574
|
+
// Resource lifecycle hook — wire singletons during registration
|
|
575
|
+
defineResource({ name: 'notification', onRegister: (f) => setSseManager(f.sseManager) });
|
|
576
|
+
|
|
577
|
+
// SSE auth — preAuth runs BEFORE auth middleware (EventSource can't set headers)
|
|
578
|
+
additionalRoutes: [{ preAuth: [(req) => { req.headers.authorization = `Bearer ${req.query.token}`; }] }]
|
|
579
|
+
|
|
580
|
+
// SSE streaming — auto headers + bypasses response wrapper
|
|
581
|
+
additionalRoutes: [{ streamResponse: true, handler: async (req, reply) => reply.send(stream) }]
|
|
582
|
+
```
|
|
583
|
+
|
|
484
584
|
## Subpath Imports
|
|
485
585
|
|
|
486
586
|
```typescript
|