@classytic/arc 2.11.3 → 2.13.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 +27 -18
- package/dist/{BaseController-swXruJ2_.mjs → BaseController-DX_T-bDB.mjs} +388 -423
- package/dist/EventTransport-CT_52aWU.d.mts +34 -0
- package/dist/EventTransport-DLWoUMHy.mjs +103 -0
- package/dist/{QueryCache-DOBNHBE0.d.mts → QueryCache-D41bfdBB.d.mts} +1 -1
- package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/audit.d.mts +199 -0
- package/dist/auth/audit.mjs +288 -0
- package/dist/auth/index.d.mts +5 -5
- package/dist/auth/index.mjs +117 -191
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
- package/dist/buildHandler-olo-gt94.mjs +610 -0
- package/dist/cache/index.d.mts +3 -3
- package/dist/cache/index.mjs +3 -3
- package/dist/cli/commands/describe.d.mts +89 -13
- package/dist/cli/commands/describe.mjs +56 -2
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +147 -48
- package/dist/cli/commands/init.d.mts +13 -0
- package/dist/cli/commands/init.mjs +237 -112
- package/dist/cli/commands/introspect.mjs +8 -1
- package/dist/context/index.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -5
- package/dist/core-D72ia0EH.mjs +1399 -0
- package/dist/{createActionRouter-u3ql2EDo.mjs → createActionRouter-CEvzKcy8.mjs} +7 -20
- package/dist/createAggregationRouter-CyecOxnO.mjs +114 -0
- package/dist/{createApp-BFxtdKy6.mjs → createApp-XX2-N0Yd.mjs} +31 -27
- package/dist/defineEvent-D5h7EvAx.mjs +188 -0
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
- package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
- package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
- package/dist/errors-j4aJm1Wg.mjs +184 -0
- package/dist/{eventPlugin-KrFIQ097.mjs → eventPlugin-CaKTYkYM.mjs} +35 -137
- package/dist/{eventPlugin-CUNjYYRY.d.mts → eventPlugin-qXpqTebY.d.mts} +57 -7
- package/dist/events/index.d.mts +164 -5
- package/dist/events/index.mjs +133 -209
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +204 -31
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +2 -2
- package/dist/factory/index.mjs +2 -2
- package/dist/{fields-C8Y0XLAu.d.mts → fields-COhcH3fk.d.mts} +23 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +3 -3
- package/dist/idempotency/index.mjs +1 -20
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +1 -1
- package/dist/{index-BYCqHCVu.d.mts → index-BTqLEvhu.d.mts} +164 -4
- package/dist/{index-6u4_Gg6G.d.mts → index-BtW7qYwa.d.mts} +661 -281
- package/dist/{index-BdXnTPRj.d.mts → index-Ds61mrJE.d.mts} +50 -4
- package/dist/{index-DdQ3O9Pg.d.mts → index-Dz5IKsrE.d.mts} +360 -219
- package/dist/index.d.mts +6 -7
- package/dist/index.mjs +9 -10
- package/dist/integrations/event-gateway.d.mts +2 -2
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/mcp/index.d.mts +2 -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/integrations/streamline.d.mts +60 -11
- package/dist/integrations/streamline.mjs +75 -85
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +2 -8
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +2 -2
- package/dist/migrations/index.d.mts +23 -3
- package/dist/migrations/index.mjs +0 -7
- package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
- package/dist/{openapi-BGUn7Ki1.mjs → openapi-CiOMVW1p.mjs} +143 -13
- package/dist/org/index.d.mts +2 -2
- package/dist/org/index.mjs +1 -1
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +3 -3
- package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
- package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +18 -33
- package/dist/plugins/index.mjs +33 -13
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +5 -5
- package/dist/presets/filesUpload.mjs +6 -9
- 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 +2 -2
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +6 -8
- package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
- package/dist/{queryCachePlugin-BUXBSm4F.d.mts → queryCachePlugin-CqMdLI2-.d.mts} +2 -2
- package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
- package/dist/{redis-Cm1gnRDf.d.mts → redis-DiMkdHEl.d.mts} +1 -1
- package/dist/redis-stream-D6HzR1Z_.d.mts +232 -0
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
- package/dist/{resourceToTools-ByZpgjeH.mjs → resourceToTools-C5coh64w.mjs} +224 -71
- package/dist/{routerShared-BqLRb5l7.mjs → routerShared-D6_fEGHh.mjs} +40 -36
- package/dist/{schemaIR-BlG9bY7v.mjs → schemaIR-7Vl611Qs.mjs} +1 -1
- package/dist/schemas/index.d.mts +100 -30
- package/dist/schemas/index.mjs +86 -29
- package/dist/scim/index.d.mts +264 -0
- package/dist/scim/index.mjs +963 -0
- package/dist/scope/index.d.mts +3 -3
- package/dist/scope/index.mjs +4 -4
- package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
- package/dist/{store-helpers-BhrzxvyQ.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
- package/dist/testing/index.d.mts +2 -8
- package/dist/testing/index.mjs +16 -24
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-BH7dEGvU.d.mts → types-BvqwCCSx.d.mts} +77 -29
- package/dist/{types-tgR4Pt8F.d.mts → types-CTYvcwHe.d.mts} +195 -1
- package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
- package/dist/{types-9beEMe25.d.mts → types-DQHFc8PM.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +5 -5
- package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
- package/dist/{versioning-M9lNLhO8.d.mts → versioning-DTTvc80y.d.mts} +1 -1
- package/package.json +24 -34
- package/skills/arc/SKILL.md +521 -785
- package/skills/arc/references/agent-auth.md +238 -0
- package/skills/arc/references/api-reference.md +187 -0
- package/skills/arc/references/auth.md +354 -7
- package/skills/arc/references/enterprise-auth.md +94 -0
- package/skills/arc/references/events.md +8 -6
- package/skills/arc/references/mcp.md +2 -2
- package/skills/arc/references/multi-tenancy.md +11 -2
- package/skills/arc/references/production.md +10 -9
- package/skills/arc/references/scim.md +247 -0
- package/skills/arc/references/testing.md +1 -1
- package/skills/arc-code-review/SKILL.md +141 -0
- package/skills/arc-code-review/references/anti-patterns.md +911 -0
- package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
- package/skills/arc-code-review/references/migration-recipes.md +700 -0
- package/skills/arc-code-review/references/mongokit-migration.md +386 -0
- package/skills/arc-code-review/references/scaffolding.md +230 -0
- package/skills/arc-code-review/references/severity.md +127 -0
- package/dist/EventTransport-CfVEGaEl.d.mts +0 -293
- package/dist/adapters/index.d.mts +0 -3
- package/dist/adapters/index.mjs +0 -2
- package/dist/adapters-D0tT2Tyo.mjs +0 -949
- package/dist/auth/mongoose.d.mts +0 -191
- package/dist/auth/mongoose.mjs +0 -73
- package/dist/core-DnUsRpuX.mjs +0 -1049
- package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
- package/dist/errorHandler-Co3lnVmJ.d.mts +0 -114
- package/dist/errors-D5c-5BJL.mjs +0 -232
- package/dist/index-BbMrcvGp.d.mts +0 -362
- package/dist/redis-stream-CM8TXTix.d.mts +0 -110
- /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
- /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
- /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
- /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
- /package/dist/{elevation-s5ykdNHr.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
- /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BD5nw6St.d.mts} +0 -0
- /package/dist/{interface-CkkWm5uR.d.mts → interface-DfLGcus7.d.mts} +0 -0
- /package/dist/{interface-Da0r7Lna.d.mts → interface-beEtJyWM.d.mts} +0 -0
- /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
- /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
- /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
- /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
- /package/dist/{pluralize-BneOJkpi.mjs → pluralize-DQgqgifU.mjs} +0 -0
- /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
- /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
- /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
- /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-C4Le_UB3.d.mts} +0 -0
- /package/dist/{storage-BwGQXUpd.d.mts → storage-Dfzt4VTl.d.mts} +0 -0
- /package/dist/{tracing-DokiEsuz.d.mts → tracing-QJVprktp.d.mts} +0 -0
- /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
- /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
- /package/dist/{versioning-CGPjkqAg.mjs → versioning-BUrT5aP4.mjs} +0 -0
- /package/dist/{websocket-CyJ1VIFI.d.mts → websocket-ChC2rqe1.d.mts} +0 -0
|
@@ -1,18 +1,94 @@
|
|
|
1
|
+
import { V as ResourceDefinition, ft as RouteSchemaOptions, rt as RateLimitConfig } from "../../index-BtW7qYwa.mjs";
|
|
2
|
+
|
|
1
3
|
//#region src/cli/commands/describe.d.ts
|
|
4
|
+
interface DescribedResource {
|
|
5
|
+
name: string;
|
|
6
|
+
displayName: string;
|
|
7
|
+
prefix: string;
|
|
8
|
+
tag: string;
|
|
9
|
+
module?: string;
|
|
10
|
+
adapter: {
|
|
11
|
+
type: string;
|
|
12
|
+
name: string;
|
|
13
|
+
} | null;
|
|
14
|
+
permissions: Record<string, PermissionDescription>;
|
|
15
|
+
presets: string[];
|
|
16
|
+
fields?: Record<string, FieldDescription>;
|
|
17
|
+
pipeline?: PipelineDescription;
|
|
18
|
+
routes: RouteDescription[];
|
|
19
|
+
events: EventDescription[];
|
|
20
|
+
/**
|
|
21
|
+
* Declarative `POST /:id/action` entries surfaced for tooling parity
|
|
22
|
+
* with OpenAPI/MCP. Empty when the resource declares no actions.
|
|
23
|
+
*/
|
|
24
|
+
actions: ActionDescription[];
|
|
25
|
+
/** Per-resource fallback for actions without their own permission. */
|
|
26
|
+
actionPermissions?: PermissionDescription;
|
|
27
|
+
/**
|
|
28
|
+
* Declarative `GET /aggregations/:name` entries (v2.13). Empty when
|
|
29
|
+
* the resource declares no aggregations.
|
|
30
|
+
*/
|
|
31
|
+
aggregations: AggregationDescription[];
|
|
32
|
+
schemaOptions?: RouteSchemaOptions;
|
|
33
|
+
rateLimit?: RateLimitConfig | false;
|
|
34
|
+
middlewares: string[];
|
|
35
|
+
}
|
|
36
|
+
interface ActionDescription {
|
|
37
|
+
name: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
hasSchema: boolean;
|
|
40
|
+
permission: PermissionDescription;
|
|
41
|
+
}
|
|
42
|
+
interface AggregationDescription {
|
|
43
|
+
name: string;
|
|
44
|
+
summary?: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
groupBy: unknown;
|
|
47
|
+
measures: string[];
|
|
48
|
+
permission: PermissionDescription;
|
|
49
|
+
requiresDateRange: boolean;
|
|
50
|
+
hasRequiredFilters: boolean;
|
|
51
|
+
}
|
|
52
|
+
interface PermissionDescription {
|
|
53
|
+
type: "public" | "requireAuth" | "requireRoles" | "custom";
|
|
54
|
+
roles?: readonly string[];
|
|
55
|
+
}
|
|
56
|
+
interface FieldDescription {
|
|
57
|
+
type: string;
|
|
58
|
+
roles?: readonly string[];
|
|
59
|
+
redactValue?: unknown;
|
|
60
|
+
}
|
|
61
|
+
interface PipelineDescription {
|
|
62
|
+
guards: PipelineStepDescription[];
|
|
63
|
+
transforms: PipelineStepDescription[];
|
|
64
|
+
interceptors: PipelineStepDescription[];
|
|
65
|
+
}
|
|
66
|
+
interface PipelineStepDescription {
|
|
67
|
+
name: string;
|
|
68
|
+
operations?: string[];
|
|
69
|
+
}
|
|
70
|
+
interface RouteDescription {
|
|
71
|
+
method: string;
|
|
72
|
+
path: string;
|
|
73
|
+
operation: string;
|
|
74
|
+
summary?: string;
|
|
75
|
+
description?: string;
|
|
76
|
+
permission?: PermissionDescription;
|
|
77
|
+
}
|
|
78
|
+
interface EventDescription {
|
|
79
|
+
name: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
hasSchema: boolean;
|
|
82
|
+
hasHandler: boolean;
|
|
83
|
+
}
|
|
2
84
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```bash
|
|
11
|
-
* arc describe ./src/resources.js --json
|
|
12
|
-
* arc describe ./src/resources.js --resource product
|
|
13
|
-
* arc describe ./src/resources.js --pretty
|
|
14
|
-
* ```
|
|
85
|
+
* Programmatic entry for the same surface the `arc describe` CLI emits per
|
|
86
|
+
* resource. Exported so cross-surface contract tests can verify that the
|
|
87
|
+
* CLI describe output stays in lockstep with registry / OpenAPI / MCP.
|
|
88
|
+
* Hosts that want a JSON dump of one resource without spawning the CLI
|
|
89
|
+
* can call this directly.
|
|
15
90
|
*/
|
|
91
|
+
declare function describeResource(resource: ResourceDefinition<unknown>, module?: string): DescribedResource;
|
|
16
92
|
declare function describe(args: string[]): Promise<void>;
|
|
17
93
|
//#endregion
|
|
18
|
-
export { describe };
|
|
94
|
+
export { describe, describeResource };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { t as CRUD_OPERATIONS } from "../../constants-
|
|
1
|
+
import { t as CRUD_OPERATIONS } from "../../constants-Cxde4rpC.mjs";
|
|
2
|
+
import { n as stringifyMeasureMap } from "../../ResourceRegistry-CTERg_2x.mjs";
|
|
2
3
|
import { resolve } from "node:path";
|
|
3
4
|
import { pathToFileURL } from "node:url";
|
|
4
5
|
//#region src/cli/commands/describe.ts
|
|
@@ -147,12 +148,60 @@ function describeEvents(resourceName, events) {
|
|
|
147
148
|
hasHandler: !!def.handler
|
|
148
149
|
}));
|
|
149
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Surface declarative `actions:` so the CLI matches what OpenAPI / MCP /
|
|
153
|
+
* the registry already emit for the same definition. Resolves each
|
|
154
|
+
* action's permission via the same fallback chain runtime uses:
|
|
155
|
+
* per-action `permissions` → resource-level `actionPermissions` → custom.
|
|
156
|
+
*/
|
|
157
|
+
function describeActions(actions, fallback) {
|
|
158
|
+
if (!actions) return [];
|
|
159
|
+
return Object.entries(actions).map(([name, entry]) => {
|
|
160
|
+
if (typeof entry === "function") return {
|
|
161
|
+
name,
|
|
162
|
+
hasSchema: false,
|
|
163
|
+
permission: describePermission(fallback)
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
name,
|
|
167
|
+
description: entry.description,
|
|
168
|
+
hasSchema: !!entry.schema,
|
|
169
|
+
permission: describePermission(entry.permissions ?? fallback)
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Surface declarative `aggregations:` (v2.13). Reuses
|
|
175
|
+
* `stringifyMeasureMap` from the registry so the measure render matches
|
|
176
|
+
* OpenAPI/MCP byte-for-byte — single source of truth for `'sum:price'`
|
|
177
|
+
* vs `'count'` vs `'percentile:latency:0.95'`.
|
|
178
|
+
*/
|
|
179
|
+
function describeAggregations(aggregations) {
|
|
180
|
+
if (!aggregations) return [];
|
|
181
|
+
return Object.entries(aggregations).map(([name, entry]) => ({
|
|
182
|
+
name,
|
|
183
|
+
summary: entry.summary,
|
|
184
|
+
description: entry.description,
|
|
185
|
+
groupBy: entry.groupBy,
|
|
186
|
+
measures: Object.values(stringifyMeasureMap(entry.measures)),
|
|
187
|
+
permission: describePermission(entry.permissions),
|
|
188
|
+
requiresDateRange: !!entry.requireDateRange,
|
|
189
|
+
hasRequiredFilters: !!entry.requireFilters && entry.requireFilters.length > 0
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
150
192
|
function describeMiddlewares(middlewares) {
|
|
151
193
|
if (!middlewares) return [];
|
|
152
194
|
const ops = [];
|
|
153
195
|
for (const [op, handlers] of Object.entries(middlewares)) if (handlers?.length) ops.push(`${op}(${handlers.length})`);
|
|
154
196
|
return ops;
|
|
155
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Programmatic entry for the same surface the `arc describe` CLI emits per
|
|
200
|
+
* resource. Exported so cross-surface contract tests can verify that the
|
|
201
|
+
* CLI describe output stays in lockstep with registry / OpenAPI / MCP.
|
|
202
|
+
* Hosts that want a JSON dump of one resource without spawning the CLI
|
|
203
|
+
* can call this directly.
|
|
204
|
+
*/
|
|
156
205
|
function describeResource(resource, module) {
|
|
157
206
|
return {
|
|
158
207
|
name: resource.name,
|
|
@@ -170,6 +219,9 @@ function describeResource(resource, module) {
|
|
|
170
219
|
pipeline: describePipeline(resource.pipe),
|
|
171
220
|
routes: describeRoutes(resource),
|
|
172
221
|
events: describeEvents(resource.name, resource.events),
|
|
222
|
+
actions: describeActions(resource.actions, resource.actionPermissions),
|
|
223
|
+
actionPermissions: resource.actionPermissions ? describePermission(resource.actionPermissions) : void 0,
|
|
224
|
+
aggregations: describeAggregations(resource.aggregations),
|
|
173
225
|
schemaOptions: Object.keys(resource.schemaOptions ?? {}).length > 0 ? resource.schemaOptions : void 0,
|
|
174
226
|
rateLimit: resource.rateLimit,
|
|
175
227
|
middlewares: describeMiddlewares(resource.middlewares)
|
|
@@ -240,6 +292,8 @@ async function describe(args) {
|
|
|
240
292
|
totalEvents: described.reduce((sum, r) => sum + r.events.length, 0),
|
|
241
293
|
totalCatalogedEvents: eventCatalog?.length ?? 0,
|
|
242
294
|
totalFields,
|
|
295
|
+
totalActions: described.reduce((sum, r) => sum + r.actions.length, 0),
|
|
296
|
+
totalAggregations: described.reduce((sum, r) => sum + r.aggregations.length, 0),
|
|
243
297
|
presetUsage: presetCounts,
|
|
244
298
|
pipelineSteps: totalPipelineSteps
|
|
245
299
|
}
|
|
@@ -252,4 +306,4 @@ async function describe(args) {
|
|
|
252
306
|
}
|
|
253
307
|
}
|
|
254
308
|
//#endregion
|
|
255
|
-
export { describe };
|
|
309
|
+
export { describe, describeResource };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as ResourceRegistry } from "../../ResourceRegistry-
|
|
2
|
-
import { t as buildOpenApiSpec } from "../../openapi-
|
|
1
|
+
import { t as ResourceRegistry } from "../../ResourceRegistry-CTERg_2x.mjs";
|
|
2
|
+
import { t as buildOpenApiSpec } from "../../openapi-CiOMVW1p.mjs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as pluralize } from "../../pluralize-
|
|
1
|
+
import { t as pluralize } from "../../pluralize-DQgqgifU.mjs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
//#region src/cli/commands/generate.ts
|
|
@@ -49,37 +49,73 @@ function getTemplates(ts, config = {}) {
|
|
|
49
49
|
* Generated by Arc CLI
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
${ts ? "import mongoose, { type HydratedDocument, type Model, type Types } from 'mongoose';" : "import mongoose from 'mongoose';"}
|
|
53
53
|
|
|
54
54
|
const { Schema } = mongoose;
|
|
55
|
-
${ts ?
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
${ts ? [
|
|
56
|
+
"",
|
|
57
|
+
"/**",
|
|
58
|
+
" * Persisted shape — what `.lean()` and `.toObject()` return. Carrying this",
|
|
59
|
+
` * through \`Model<I${name}>\` lets \`.select(...)\` / \`.find(...)\` / \`.lean()\``,
|
|
60
|
+
" * infer correctly so domain methods don't need `as` casts.",
|
|
61
|
+
" *",
|
|
62
|
+
" * Replace the placeholder fields with your real domain shape.",
|
|
63
|
+
" */",
|
|
64
|
+
`export interface I${name} {`,
|
|
65
|
+
" _id: Types.ObjectId;",
|
|
66
|
+
...isMultiTenant ? [" organizationId: Types.ObjectId;"] : [],
|
|
67
|
+
" // TODO: define your fields here",
|
|
68
|
+
" createdAt: Date;",
|
|
69
|
+
" updatedAt: Date;",
|
|
70
|
+
"}",
|
|
71
|
+
"",
|
|
72
|
+
`export type ${name}Document = HydratedDocument<I${name}>;`,
|
|
73
|
+
""
|
|
74
|
+
].join("\n") : ""}
|
|
64
75
|
const ${camel}Schema = new Schema${ts ? `<I${name}>` : ""}(
|
|
65
76
|
{
|
|
66
|
-
name: { type: String, required: true, trim: true },
|
|
67
|
-
description: { type: String, trim: true },
|
|
68
|
-
${isMultiTenant ? "organizationId: { type: String, required: true, index: true },\n " : ""}isActive: { type: Boolean, default: true },
|
|
77
|
+
${isMultiTenant ? " organizationId: { type: Schema.Types.ObjectId, required: true, index: true },\n" : ""} // TODO: declare your fields here, e.g. name: { type: String, required: true, trim: true },
|
|
69
78
|
},
|
|
70
79
|
{ timestamps: true }
|
|
71
80
|
);
|
|
72
81
|
|
|
73
|
-
|
|
74
|
-
${
|
|
75
|
-
|
|
82
|
+
${isMultiTenant ? `${camel}Schema.index({ organizationId: 1, createdAt: -1 });\n` : ""}${ts ? [
|
|
83
|
+
`const ${name}: Model<I${name}> =`,
|
|
84
|
+
` (mongoose.models.${name} as Model<I${name}> | undefined) ??`,
|
|
85
|
+
` mongoose.model<I${name}>('${name}', ${camel}Schema);`
|
|
86
|
+
].join("\n") : `const ${name} = mongoose.models.${name} || mongoose.model('${name}', ${camel}Schema);`}
|
|
76
87
|
|
|
77
|
-
const ${name} = mongoose.models.${name} || mongoose.model('${name}', ${camel}Schema);
|
|
78
88
|
export default ${name};
|
|
79
89
|
`;
|
|
80
90
|
},
|
|
81
91
|
repository: (name, fileName) => {
|
|
82
92
|
const camel = toCamelCase(name);
|
|
93
|
+
if (!(config.adapter === "mongokit" || !config.adapter)) {
|
|
94
|
+
const generic = ts ? `<I${name}>` : "";
|
|
95
|
+
return `/**
|
|
96
|
+
* ${name} Repository
|
|
97
|
+
* Generated by Arc CLI
|
|
98
|
+
*
|
|
99
|
+
* This project uses a custom adapter — wire your repository to whichever
|
|
100
|
+
* kit you're using (sqlitekit, prismakit, custom). Replace the body below
|
|
101
|
+
* with your kit's Repository constructor. Arc only requires the
|
|
102
|
+
* \`MinimalRepo\` floor (getAll/getById/create/update/delete) declared in
|
|
103
|
+
* \`@classytic/repo-core/repository\`.
|
|
104
|
+
*/
|
|
105
|
+
${ts ? `\nimport type { I${name} } from './${fileName}.model.js';` : ""}
|
|
106
|
+
|
|
107
|
+
// Replace with your kit's repository instance:
|
|
108
|
+
// import { Repository } from '@classytic/sqlitekit';
|
|
109
|
+
// const ${camel}Repository = new Repository${generic}(${name}Table);
|
|
110
|
+
// export default ${camel}Repository;
|
|
111
|
+
|
|
112
|
+
const ${camel}Repository = {
|
|
113
|
+
// TODO: implement MinimalRepo<${ts ? `I${name}` : "any"}>
|
|
114
|
+
} as never;
|
|
115
|
+
|
|
116
|
+
export default ${camel}Repository;
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
83
119
|
const generic = ts ? `<I${name}>` : "";
|
|
84
120
|
return `/**
|
|
85
121
|
* ${name} Repository
|
|
@@ -90,25 +126,17 @@ import {
|
|
|
90
126
|
Repository,
|
|
91
127
|
methodRegistryPlugin,
|
|
92
128
|
softDeletePlugin,
|
|
93
|
-
mongoOperationsPlugin,
|
|
94
129
|
} from '@classytic/mongokit';
|
|
95
130
|
import ${name} from './${fileName}.model.js';${ts ? `\nimport type { I${name} } from './${fileName}.model.js';` : ""}
|
|
96
131
|
|
|
97
132
|
class ${name}Repository extends Repository${generic} {
|
|
98
133
|
constructor() {
|
|
99
|
-
super(${name}, [
|
|
100
|
-
methodRegistryPlugin(),
|
|
101
|
-
softDeletePlugin(),
|
|
102
|
-
mongoOperationsPlugin(),
|
|
103
|
-
]);
|
|
134
|
+
super(${name}, [methodRegistryPlugin(), softDeletePlugin()]);
|
|
104
135
|
}
|
|
105
136
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
async findActive() {
|
|
110
|
-
return this.Model.find({ isActive: true, deletedAt: null }).lean();
|
|
111
|
-
}
|
|
137
|
+
// Add domain methods here. arc + mongokit ship the standard CRUD
|
|
138
|
+
// (getAll/getById/create/update/delete) on the base class, so only
|
|
139
|
+
// write a method when there's real domain logic to capture.
|
|
112
140
|
}
|
|
113
141
|
|
|
114
142
|
const ${camel}Repository = new ${name}Repository();
|
|
@@ -149,7 +177,7 @@ export default ${camel}Controller;
|
|
|
149
177
|
*/
|
|
150
178
|
|
|
151
179
|
import ${name} from './${fileName}.model.js';
|
|
152
|
-
import { buildCrudSchemasFromModel } from '@classytic/mongokit
|
|
180
|
+
import { buildCrudSchemasFromModel } from '@classytic/mongokit';
|
|
153
181
|
|
|
154
182
|
/**
|
|
155
183
|
* CRUD Schemas with Field Rules
|
|
@@ -171,8 +199,10 @@ const crudSchemas = buildCrudSchemasFromModel(${name}, {
|
|
|
171
199
|
// organizationId: { systemManaged: true, preserveForElevated: true },
|
|
172
200
|
},
|
|
173
201
|
query: {
|
|
202
|
+
// Add your filterable fields here. createdAt is the default so the
|
|
203
|
+
// generated routes accept ?createdAt[gte]=2026-01-01 with no extra
|
|
204
|
+
// wiring. Add domain fields below as your model grows.
|
|
174
205
|
filterableFields: {
|
|
175
|
-
isActive: 'boolean',
|
|
176
206
|
createdAt: 'date',
|
|
177
207
|
},
|
|
178
208
|
},
|
|
@@ -183,23 +213,38 @@ export default crudSchemas;
|
|
|
183
213
|
resource: (name, fileName) => {
|
|
184
214
|
const camel = toCamelCase(name);
|
|
185
215
|
const useMongoKit = config.adapter === "mongokit" || !config.adapter;
|
|
186
|
-
const
|
|
216
|
+
const schemaGeneratorImport = useMongoKit ? `import { buildCrudSchemasFromModel } from '@classytic/mongokit';\n` : "";
|
|
217
|
+
const queryParserImport = useMongoKit ? `\nimport { QueryParser } from '@classytic/mongokit';\n\nconst queryParser = new QueryParser({\n // Whitelist the fields this resource accepts in URL filters.\n // Empty by default — only \`createdAt\` is implicit; add yours.\n allowedFilterFields: [],\n});\n` : "";
|
|
218
|
+
const adapterCall = useMongoKit ? `createMongooseAdapter({ model: ${name}, repository: ${camel}Repository, schemaGenerator: buildCrudSchemasFromModel })` : `createMongooseAdapter({ model: ${name}, repository: ${camel}Repository })`;
|
|
187
219
|
const queryParserConfig = useMongoKit ? `\n queryParser,` : "";
|
|
188
220
|
return isMultiTenant ? `/**
|
|
189
221
|
* ${name} Resource
|
|
190
222
|
* Generated by Arc CLI
|
|
191
223
|
*/
|
|
192
224
|
|
|
193
|
-
import { defineResource
|
|
225
|
+
import { defineResource } from '@classytic/arc';
|
|
226
|
+
import { createMongooseAdapter } from '@classytic/mongokit/adapter';
|
|
194
227
|
import { allOf, requireOrgMembership, requireRoles } from '@classytic/arc/permissions';
|
|
195
228
|
import { multiTenantPreset } from '@classytic/arc/presets';
|
|
196
|
-
import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
|
|
229
|
+
${schemaGeneratorImport}import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
|
|
197
230
|
import ${camel}Repository from './${fileName}.repository.js';${queryParserImport}
|
|
198
231
|
|
|
199
232
|
const ${camel}Resource = defineResource${ts ? `<I${name}>` : ""}({
|
|
200
233
|
name: '${fileName}',
|
|
201
|
-
adapter:
|
|
234
|
+
adapter: ${adapterCall},${queryParserConfig}
|
|
235
|
+
|
|
236
|
+
// Multi-tenant default: scope reads/writes by \`organizationId\`. For
|
|
237
|
+
// company-wide tables (lookup data, platform settings) set
|
|
238
|
+
// \`tenantField: false\` instead — otherwise queries silently return
|
|
239
|
+
// nothing because the column doesn't exist.
|
|
240
|
+
//
|
|
241
|
+
// Multi-level tenancy (org + branch + project, etc.) — replace with:
|
|
242
|
+
// multiTenantPreset({ tenantFields: [
|
|
243
|
+
// { field: 'organizationId', type: 'org' },
|
|
244
|
+
// { field: 'branchId', contextKey: 'branchId' },
|
|
245
|
+
// ] })
|
|
202
246
|
presets: ['softDelete', multiTenantPreset({ tenantField: 'organizationId' })],
|
|
247
|
+
|
|
203
248
|
permissions: {
|
|
204
249
|
list: requireOrgMembership(),
|
|
205
250
|
get: requireOrgMembership(),
|
|
@@ -215,15 +260,22 @@ export default ${camel}Resource;
|
|
|
215
260
|
* Generated by Arc CLI
|
|
216
261
|
*/
|
|
217
262
|
|
|
218
|
-
import { defineResource
|
|
263
|
+
import { defineResource } from '@classytic/arc';
|
|
264
|
+
import { createMongooseAdapter } from '@classytic/mongokit/adapter';
|
|
219
265
|
import { requireAuth, requireRoles } from '@classytic/arc/permissions';
|
|
220
|
-
import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
|
|
266
|
+
${schemaGeneratorImport}import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
|
|
221
267
|
import ${camel}Repository from './${fileName}.repository.js';${queryParserImport}
|
|
222
268
|
|
|
223
269
|
const ${camel}Resource = defineResource${ts ? `<I${name}>` : ""}({
|
|
224
270
|
name: '${fileName}',
|
|
225
|
-
adapter:
|
|
271
|
+
adapter: ${adapterCall},${queryParserConfig}
|
|
272
|
+
|
|
273
|
+
// Single-tenant default: arc auto-infers \`tenantField: false\` when
|
|
274
|
+
// the model has no \`organizationId\` path (silent-zero-results
|
|
275
|
+
// footgun closed in 2.12). Set \`tenantField: '<field>'\` to opt INTO
|
|
276
|
+
// tenant scoping, or \`tenantField: false\` to make it explicit.
|
|
226
277
|
presets: ['softDelete'],
|
|
278
|
+
|
|
227
279
|
permissions: {
|
|
228
280
|
list: requireAuth(),
|
|
229
281
|
get: requireAuth(),
|
|
@@ -278,13 +330,20 @@ ${ts ? "import { z } from 'zod';\n" : "const { z } = require('zod');\n"}
|
|
|
278
330
|
* ${name} Tests
|
|
279
331
|
* Generated by Arc CLI
|
|
280
332
|
*
|
|
281
|
-
* 2.
|
|
333
|
+
* Testing surface (arc 2.12+):
|
|
282
334
|
* - createTestApp turnkey Fastify + in-memory Mongo + auth + fixtures
|
|
283
|
-
* - expectArc fluent
|
|
335
|
+
* - expectArc fluent matchers — .ok / .failed / .unauthorized /
|
|
336
|
+
* .forbidden / .notFound / .conflict / .validationError /
|
|
337
|
+
* .paginated / .hidesField / .hasData / .hasStatus
|
|
284
338
|
* - ctx.auth unified TestAuthProvider — register a role, reuse .headers
|
|
339
|
+
*
|
|
340
|
+
* Wire shape (post-2.12): single-doc responses are flat (\`{_id, name, ...}\`),
|
|
341
|
+
* paginated responses are \`{ method: 'offset', data: [...], page, ... }\`.
|
|
342
|
+
* No \`success\` envelope — HTTP status discriminates success vs error;
|
|
343
|
+
* errors carry the canonical ErrorContract \`{ code, message, status }\`.
|
|
285
344
|
*/
|
|
286
345
|
|
|
287
|
-
import { describe, it, beforeAll, afterAll } from 'vitest';
|
|
346
|
+
import { describe, it, beforeAll, afterAll, expect } from 'vitest';
|
|
288
347
|
import { createTestApp, expectArc } from '@classytic/arc/testing';
|
|
289
348
|
import type { TestAppContext } from '@classytic/arc/testing';
|
|
290
349
|
import ${camel}Resource from '../src/resources/${fileName}/${fileName}.resource.js';
|
|
@@ -308,17 +367,42 @@ describe('${name} Resource', () => {
|
|
|
308
367
|
afterAll(() => ctx.close());
|
|
309
368
|
|
|
310
369
|
describe('GET /${pluralize(fileName)}', () => {
|
|
311
|
-
it('
|
|
312
|
-
const res = await ctx.app.inject({
|
|
370
|
+
it('returns a paginated list', async () => {
|
|
371
|
+
const res = await ctx.app.inject({
|
|
372
|
+
method: 'GET',
|
|
373
|
+
url: '/${pluralize(fileName)}',
|
|
374
|
+
headers: ctx.auth${ts ? "!" : ""}.as('admin').headers,
|
|
375
|
+
});
|
|
313
376
|
expectArc(res).ok().paginated();
|
|
314
377
|
});
|
|
378
|
+
|
|
379
|
+
it('rejects unauthenticated requests', async () => {
|
|
380
|
+
const res = await ctx.app.inject({ method: 'GET', url: '/${pluralize(fileName)}' });
|
|
381
|
+
expectArc(res).unauthorized();
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe('POST /${pluralize(fileName)}', () => {
|
|
386
|
+
it('creates a record (flat wire shape — no envelope)', async () => {
|
|
387
|
+
const res = await ctx.app.inject({
|
|
388
|
+
method: 'POST',
|
|
389
|
+
url: '/${pluralize(fileName)}',
|
|
390
|
+
headers: ctx.auth${ts ? "!" : ""}.as('admin').headers,
|
|
391
|
+
payload: { name: 'Example' },
|
|
392
|
+
});
|
|
393
|
+
expectArc(res).ok(201);
|
|
394
|
+
// Single-doc response is flat: \`body._id\` (not \`body.data._id\`).
|
|
395
|
+
const body = res.json()${ts ? " as { _id: string; name: string }" : ""};
|
|
396
|
+
expect(body._id).toBeDefined();
|
|
397
|
+
expect(body.name).toBe('Example');
|
|
398
|
+
});
|
|
315
399
|
});
|
|
316
400
|
|
|
317
|
-
// More
|
|
318
|
-
// - expectArc(res).
|
|
319
|
-
// - expectArc(res).
|
|
320
|
-
// - ctx.auth.as('admin').headers for authenticated requests
|
|
401
|
+
// More patterns to extend:
|
|
402
|
+
// - expectArc(res).forbidden() / .notFound() / .validationError()
|
|
403
|
+
// - expectArc(res).hidesField('password') for field-level perms
|
|
321
404
|
// - ctx.fixtures.create('${fileName}', {...}) for seeded data
|
|
405
|
+
// - error wire shape: body.code === 'arc.not_found' (ErrorContract)
|
|
322
406
|
});
|
|
323
407
|
`;
|
|
324
408
|
}
|
|
@@ -333,6 +417,21 @@ async function generate(type, args) {
|
|
|
333
417
|
if (!name) throw new Error("Missing name argument\nUsage: arc generate <type> <name>\nExample: arc generate resource product");
|
|
334
418
|
const capitalizedName = toPascalCase(name);
|
|
335
419
|
const lowerName = name.toLowerCase();
|
|
420
|
+
if ((type === "resource" || type === "r" || type === "model" || type === "m") && new Set([
|
|
421
|
+
"user",
|
|
422
|
+
"session",
|
|
423
|
+
"account",
|
|
424
|
+
"verification",
|
|
425
|
+
"organization",
|
|
426
|
+
"member",
|
|
427
|
+
"invitation",
|
|
428
|
+
"team",
|
|
429
|
+
"team-member",
|
|
430
|
+
"apikey"
|
|
431
|
+
]).has(lowerName)) {
|
|
432
|
+
console.warn(`\n[arc generate] "${lowerName}" is a Better Auth-owned collection.\nBetter Auth's organization/admin/bearer plugins write this collection directly,\nso generating a parallel arc model would create a duplicate registration.\n\nRecommended pattern:\n import { createBetterAuthOverlay } from '@classytic/mongokit/better-auth';\n const adapter = await createBetterAuthOverlay({\n auth, mongoose, collection: '${lowerName}',\n });\n // ...then \`defineResource({ name: '${lowerName}', adapter, ... })\` reads\n // BA's collection through arc with full pagination/filters/permissions.\n\nAborting. Re-run with a different name if you need a separate domain model.\n`);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
336
435
|
const projectConfig = readProjectConfig();
|
|
337
436
|
const ts = projectConfig.typescript ?? isTypeScriptProject();
|
|
338
437
|
const ext = ts ? "ts" : "js";
|
|
@@ -14,6 +14,19 @@ interface InitOptions {
|
|
|
14
14
|
adapter?: "mongokit" | "custom";
|
|
15
15
|
auth?: "jwt" | "better-auth";
|
|
16
16
|
tenant?: "multi" | "single";
|
|
17
|
+
/**
|
|
18
|
+
* Enable Better Auth's `apiKey` plugin (`@better-auth/api-key`) for
|
|
19
|
+
* machine-to-machine authentication alongside cookie/session auth.
|
|
20
|
+
* Only used when `auth === 'better-auth'`. Default: false.
|
|
21
|
+
*/
|
|
22
|
+
apiKey?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Session strategy when using Better Auth.
|
|
25
|
+
* - `cookie` (default): browser cookie + DB-backed session (BA's default)
|
|
26
|
+
* - `bearer`: Authorization: Bearer header (mobile apps, SPA, M2M)
|
|
27
|
+
* Both can coexist — `bearer: true` adds bearer alongside cookies.
|
|
28
|
+
*/
|
|
29
|
+
session?: "cookie" | "bearer" | "both";
|
|
17
30
|
typescript?: boolean;
|
|
18
31
|
edge?: boolean;
|
|
19
32
|
skipInstall?: boolean;
|