@classytic/arc 2.21.0 → 2.23.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/dist/{BaseController-BnLYgE9A.mjs → BaseController-BQ8KNvWC.mjs} +3 -2
- package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
- package/dist/{actionPermissions-ze70xXJi.mjs → actionPermissions-lszoJle1.mjs} +1 -1
- package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
- package/dist/audit/index.d.mts +2 -2
- package/dist/auth/index.d.mts +30 -2
- package/dist/auth/index.mjs +6 -3
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +1 -1
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +3 -3
- package/dist/{core-DZ6Xe1jR.mjs → core-DQ6rDTyH.mjs} +126 -68
- package/dist/{createActionRouter-BWMDW-dL.mjs → createActionRouter-BuOv-m4Y.mjs} +10 -6
- package/dist/{createAggregationRouter-l5mNdYrZ.mjs → createAggregationRouter-CgJv_BU_.mjs} +3 -3
- package/dist/{createApp-Bi61XxqU.mjs → createApp-CjOeWJ9_.mjs} +252 -57
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/encryption/index.mjs +9 -5
- package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-Bq2OeEQL.mjs} +34 -10
- package/dist/{errorHandler-B0e-bq-O.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -0
- package/dist/events/index.mjs +5 -2
- package/dist/factory/index.d.mts +46 -9
- package/dist/factory/index.mjs +64 -13
- package/dist/{fastify-augmentation-DafigGQp.d.mts → fastify-augmentation-g_f9omSc.d.mts} +2 -2
- package/dist/fields-D3lhgzhD.d.mts +121 -0
- package/dist/health-ScyjIVo5.mjs +202 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +7 -0
- package/dist/idempotency/index.mjs +7 -17
- package/dist/{index-Dtrj6XZk.d.mts → index-BBsvQt7U.d.mts} +1310 -1226
- package/dist/{types-DpxoNX5v.d.mts → index-Ba10fdKp.d.mts} +196 -66
- package/dist/{index-B5N-NaHs.d.mts → index-CC5ruDGh.d.mts} +3 -3
- package/dist/{index-B5PoNbfu.d.mts → index-wMoWBLkn.d.mts} +68 -2
- package/dist/index.d.mts +6 -5
- package/dist/index.mjs +6 -5
- package/dist/{init-0YhHkXGk.mjs → init-CjO4p_q1.mjs} +123 -4
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +67 -4
- 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/webhooks.d.mts +131 -7
- package/dist/integrations/webhooks.mjs +169 -10
- package/dist/interface-n5hpKg1d.mjs +16 -0
- package/dist/jsonBody-BlzAoGCe.mjs +43 -0
- package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
- package/dist/logger/index.d.mts +26 -1
- package/dist/logger/index.mjs +39 -1
- package/dist/{mcpPlugin-B9XjjNVG.mjs → mcpPlugin-BtzIL_kM.mjs} +312 -39
- package/dist/middleware/index.d.mts +1 -1
- package/dist/{module-WeuU5jfd.d.mts → module-B4u10iOJ.d.mts} +26 -1
- package/dist/{openapi-Kyod_di6.mjs → openapi-jSDx-VDv.mjs} +3 -3
- package/dist/permissions/index.d.mts +4 -3
- package/dist/permissions/index.mjs +3 -2
- package/dist/{permissions-BJMuKMl7.mjs → permissions-tbxFHxjZ.mjs} +77 -264
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +24 -7
- package/dist/plugins/index.mjs +14 -282
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.mjs +7 -4
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.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/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-gw0ciIUD.mjs → presets-BOe4Ugkl.mjs} +1 -1
- package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
- package/dist/realtime-I8OrfnMR.mjs +375 -0
- package/dist/{registerRoute-oGAl0oA8.mjs → registerRoute-CWO6mdYe.mjs} +38 -18
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/requestId-5BhTVzBv.mjs +113 -0
- package/dist/schemaConverter-DJvodiHj.mjs +171 -0
- package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-Ca7ySNPx.mjs} +2 -2
- package/dist/scim/index.mjs +6 -7
- package/dist/scope/index.mjs +2 -76
- package/dist/scope-LONQ3mfZ.mjs +37 -0
- package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
- package/dist/{sse-CfUjwjDk.mjs → sse-0u4P4-OI.mjs} +7 -30
- package/dist/sseStream-BQ7Qaynp.mjs +57 -0
- package/dist/testing/index.d.mts +83 -3
- package/dist/testing/index.mjs +70 -3
- package/dist/types/index.d.mts +2 -2
- package/dist/{types-DRj-Ut4n.d.mts → types-5OuqeTDX.d.mts} +37 -2
- package/dist/{fields-BGqrAa_u.d.mts → types-Csxga7d3.d.mts} +1 -121
- package/dist/usage/index.d.mts +142 -0
- package/dist/usage/index.mjs +0 -0
- package/dist/utils/index.d.mts +101 -19
- package/dist/utils/index.mjs +5 -3
- package/dist/{utils-CO8a3B_1.mjs → utils-CZyeRXTh.mjs} +80 -82
- package/dist/{versioning-DQ4mCvg0.d.mts → versioning-D6PvCwpq.d.mts} +43 -9
- package/llms.txt +39 -0
- package/package.json +513 -447
- package/skills/arc/SKILL.md +14 -8
- package/skills/arc/references/api-reference.md +34 -0
- package/skills/arc/references/auth.md +21 -4
- package/skills/arc/references/mcp.md +1 -1
- package/skills/arc/references/production.md +734 -654
- package/skills/arc-code-review/SKILL.md +1 -1
- package/skills/arc-code-review/references/anti-patterns.md +71 -18
- package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
- package/skills/arc-code-review/references/migration-recipes.md +7 -7
- package/skills/arc-code-review/references/mongokit-migration.md +4 -3
- package/skills/arc-module-publishing/SKILL.md +2 -1
- package/skills/arc-module-publishing/references/authoring-modules.md +55 -0
- package/skills/arc-module-publishing/references/package-manifest.md +4 -2
- package/dist/preloadResources-CHw3_oA_.mjs +0 -45
- package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
|
@@ -84,7 +84,7 @@ One `defineResource()` call **replaces all of these** in a typical Fastify servi
|
|
|
84
84
|
```markdown
|
|
85
85
|
# Arc Convention Audit — <project-name>
|
|
86
86
|
|
|
87
|
-
**Arc version:**
|
|
87
|
+
**Arc version:** <installed version> · **Mongokit:** <version or "not installed"> · **Sqlitekit:** <version or "n/a"> · **Date:** <YYYY-MM-DD>
|
|
88
88
|
**Files scanned:** <N> · **Findings:** <N critical · <N high · <N medium · <N low>
|
|
89
89
|
|
|
90
90
|
## Executive summary
|
|
@@ -565,6 +565,8 @@ resources: async () => loadResources(import.meta.url, { context: { engine } }),
|
|
|
565
565
|
// Each *.resource.ts then exports: (ctx) => defineResource({ adapter: ctx.engine.adapter() })
|
|
566
566
|
```
|
|
567
567
|
|
|
568
|
+
For the DB **connection** itself (as opposed to engines), arc 2.21 adds `createApp({ beforeBoot: async () => connectDatabase() })` — awaited before Fastify exists, before module thunks resolve, and before any resource file imports. A composition root that connects the DB then dynamic-imports the app to control evaluation order is the pre-2.21 workaround; flag it and move the connect into `beforeBoot`.
|
|
569
|
+
|
|
568
570
|
---
|
|
569
571
|
|
|
570
572
|
## §21. Field-write denied as silent strip when reject is needed (or vice versa) 🟡
|
|
@@ -803,31 +805,19 @@ const userId = requireUserId(scope, 'finalize-checkout'); // hint surfaces in
|
|
|
803
805
|
|
|
804
806
|
---
|
|
805
807
|
|
|
806
|
-
## §32f.
|
|
808
|
+
## §32f. Missing / dodged `schemaGenerator` 🟠
|
|
807
809
|
|
|
808
810
|
**Detection:**
|
|
809
811
|
```
|
|
810
812
|
pattern: "createMongooseAdapter\\s*\\(\\s*\\{[^}]*\\}\\s*\\)"
|
|
811
813
|
multiline: true
|
|
812
814
|
```
|
|
813
|
-
Then check
|
|
814
|
-
|
|
815
|
-
Also: `createDrizzleAdapter` without `schemaGenerator: buildCrudSchemasFromTable`.
|
|
815
|
+
Then check the `schemaGenerator:` key — what to flag depends on the kit version:
|
|
816
816
|
|
|
817
|
-
**
|
|
817
|
+
- **mongokit >=3.21**: `schemaGenerator` DEFAULTS to `buildCrudSchemasFromModel` — omitting it is now correct. Flag the opposite: `schemaGenerator: false` used to dodge a wire-vs-model shape mismatch (minor-unit integers vs Money objects, enums, etc.). That opt-out silently kills OpenAPI bodies AND MCP tool input schemas for the whole resource. The right fix is a real wire contract: `customSchemas: { create: { body }, update: { body } }` with the generator ON — in arc 2.21 a custom part REPLACES the generated part wholesale (no `required[]` union), so wire and model shapes can differ cleanly.
|
|
818
|
+
- **mongokit <3.21**: omitted `schemaGenerator` emits `null` OpenAPI bodies (arc 2.12 deleted the built-in fallback). Fix: upgrade the kit, or wire `schemaGenerator: buildCrudSchemasFromModel` explicitly.
|
|
818
819
|
|
|
819
|
-
|
|
820
|
-
```typescript
|
|
821
|
-
import { createMongooseAdapter } from '@classytic/mongokit/adapter'; // arc 2.12+
|
|
822
|
-
import { buildCrudSchemasFromModel } from '@classytic/mongokit';
|
|
823
|
-
|
|
824
|
-
createMongooseAdapter({
|
|
825
|
-
model: ProductModel,
|
|
826
|
-
repository: productRepo,
|
|
827
|
-
schemaGenerator: buildCrudSchemasFromModel, // required
|
|
828
|
-
});
|
|
829
|
-
```
|
|
830
|
-
For drizzle: `import { createDrizzleAdapter } from '@classytic/sqlitekit/adapter'` + `schemaGenerator: buildCrudSchemasFromTable` from `@classytic/sqlitekit`.
|
|
820
|
+
For drizzle: `createDrizzleAdapter` still wants `schemaGenerator: buildCrudSchemasFromTable` from `@classytic/sqlitekit` — check the kit's own version before flagging.
|
|
831
821
|
|
|
832
822
|
---
|
|
833
823
|
|
|
@@ -937,6 +927,68 @@ For backends without `repo.aggregate`, declare a `materialized` hook on the aggr
|
|
|
937
927
|
|
|
938
928
|
---
|
|
939
929
|
|
|
930
|
+
## §34. `as never` / `as unknown as` casts on engine bags in module packages 🟠
|
|
931
|
+
|
|
932
|
+
**Detection:**
|
|
933
|
+
```
|
|
934
|
+
pattern: "as never|as unknown as"
|
|
935
|
+
```
|
|
936
|
+
Concentrated hits in `@classytic/arc-*` (or `@scope/arc-*`) module packages — especially around `defineResource(...)` assembly, adapter construction from an engine, or hand-rolled config spread-merges.
|
|
937
|
+
|
|
938
|
+
**Anti-pattern (pre-2.21 module-author tax):**
|
|
939
|
+
```typescript
|
|
940
|
+
interface EngineLike { models: Record<string, unknown>; repositories: Record<string, unknown> }
|
|
941
|
+
adapter: createMongooseAdapter(engine.models.Party as never, repo as never),
|
|
942
|
+
...(cfg.extraRoutes ? { routes: [...base.routes, ...cfg.extraRoutes] as never } : {}),
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
**Why high:** every cast is a hole where the kernel's real types (optional sub-module members, role unions, doc shapes) stop flowing — typos compile, wrong repos pair with wrong models, and hand-rolled spread-merges clobber host arrays.
|
|
946
|
+
|
|
947
|
+
**Fix (arc 2.21):** type the engine port with the KERNEL's own bags and assemble via `mergeResourceConfig`:
|
|
948
|
+
```typescript
|
|
949
|
+
import { mergeResourceConfig, type ResourceSeams, type AdapterLike } from '@classytic/arc';
|
|
950
|
+
|
|
951
|
+
interface CatalogEngineLike { // kernel is already a peer — no new coupling
|
|
952
|
+
models: CatalogEngine['models'];
|
|
953
|
+
repositories: CatalogEngine['repositories'];
|
|
954
|
+
}
|
|
955
|
+
defineResource(mergeResourceConfig(
|
|
956
|
+
{ name: 'product', adapter: createMongooseAdapter({ model: models.Product, repository: repositories.product }), ... },
|
|
957
|
+
hostSeams, // typed ResourceSeams — arrays concat, objects deep-merge,
|
|
958
|
+
)); // instances last-win, undefined never clobbers
|
|
959
|
+
```
|
|
960
|
+
Remaining honest boundaries (hydrated-vs-lean id widening at a kernel bridge, wire-body narrowing in action handlers) get a comment, not a blanket `as never`.
|
|
961
|
+
|
|
962
|
+
---
|
|
963
|
+
|
|
964
|
+
## §35. Hand-rolled recurring jobs (`setInterval` / cron + DB lock) 🟠
|
|
965
|
+
|
|
966
|
+
**Detection:**
|
|
967
|
+
```
|
|
968
|
+
pattern: "setInterval\\s*\\(|node-cron|cron\\.schedule|new CronJob"
|
|
969
|
+
```
|
|
970
|
+
Plus home-grown "job registry" / "leader lock" / "acquireLock" modules paired with timers.
|
|
971
|
+
|
|
972
|
+
**Why high:** every host regrows the same layer — overlap prevention, multi-replica leader lock, shutdown timer cleanup, failure counters — and gets at least one of them wrong (stacking `setInterval` runs, timers holding the process open, no lease recovery after a crashed leader).
|
|
973
|
+
|
|
974
|
+
**Fix (arc 2.21):** `schedulesPlugin` from `@classytic/arc/plugins` — interval jobs with no-overlap by construction (timeout chain), optional `lock` (any `LockAdapter`: `@classytic/mongokit/lock`, sqlitekit, memory) for multi-replica lease safety, `jitterMs`, `runOnStart`, `getScheduleStats()`, clean `onClose` drain. Calendar cron-expressions / durable retries stay in the BullMQ tier (`@classytic/arc/integrations/jobs`) — don't flag hosts that need real cron semantics for using it.
|
|
975
|
+
|
|
976
|
+
---
|
|
977
|
+
|
|
978
|
+
## §36. Wrong tenant config key — `{ field }` instead of `{ tenantField }` 🔴
|
|
979
|
+
|
|
980
|
+
**Detection:**
|
|
981
|
+
```
|
|
982
|
+
pattern: "tenant:\\s*\\{\\s*field\\s*:"
|
|
983
|
+
```
|
|
984
|
+
Also any object passed as tenant config whose keys aren't in repo-core's `TenantConfig`.
|
|
985
|
+
|
|
986
|
+
**Why critical:** unknown keys are **silently ignored** — `{ tenant: { field: 'branchId' } }` compiles (loose host types), the resource falls back to the default tenant field, and cross-tenant reads/writes go unscoped for hosts on a custom field name. Found live in published spine packages (arc-assets / arc-pricelist 0.1.x).
|
|
987
|
+
|
|
988
|
+
**Fix:** the key is `tenantField` (repo-core `TenantConfig`). In arc resources prefer the top-level `tenantField: 'branchId'` shorthand or the `multiTenant` preset; when passing a tenant object through a module seam, type it as `TenantConfig` so unknown keys fail the build.
|
|
989
|
+
|
|
990
|
+
---
|
|
991
|
+
|
|
940
992
|
## Detection checklist (run-order)
|
|
941
993
|
|
|
942
994
|
Run sweeps in this order — early hits often invalidate later context:
|
|
@@ -951,5 +1003,6 @@ Run sweeps in this order — early hits often invalidate later context:
|
|
|
951
1003
|
8. §7, §8, §22, §23 — events/cache/idempotency/audit (silent inconsistency)
|
|
952
1004
|
9. §14, §18, §19 — preset adoption + custom controller wiring
|
|
953
1005
|
10. §11, §12, §13, §29, §30, §31 — style and edges
|
|
954
|
-
11. §32a–§32g — canonical-contract drift (pagination / events / tenant / errors / adapter imports), missing `requireOrgId` accessors, missing `schemaGenerator`, kit-specific adapter imports from arc (3.0 break)
|
|
1006
|
+
11. §32a–§32g — canonical-contract drift (pagination / events / tenant / errors / adapter imports), missing `requireOrgId` accessors, missing/dodged `schemaGenerator`, kit-specific adapter imports from arc (3.0 break)
|
|
955
1007
|
12. §33 — hand-rolled `Model.aggregate([...])` in resource routes vs declarative `aggregations: { … }`
|
|
1008
|
+
13. §36 — tenant config `{ field }` key (security: silent unscoped queries), then §34–§35 — module-package `as never` casts vs 2.21 seams; hand-rolled recurring jobs vs `schedulesPlugin`
|
|
@@ -23,12 +23,22 @@ A condensed map of what arc provides, so during audit you can spot what the team
|
|
|
23
23
|
| Custom `withRetry` / `withDLQ` plumbing on events | `RedisStreamTransport` (durable, consumer groups, DLQ) |
|
|
24
24
|
| Hand-rolled idempotency token check | `idempotencyPlugin` (header-based, configurable store) |
|
|
25
25
|
| Manual SCIM provisioning endpoints | `@classytic/arc/scim` — `scimPlugin({ users, groups, bearer })` |
|
|
26
|
+
| `setInterval` sweeps + hand-rolled leader lock / job registry | `schedulesPlugin` from `@classytic/arc/plugins` (2.21 — interval jobs, LockAdapter lease, no Redis) |
|
|
27
|
+
| `as never` casts + spread-merges assembling resources in module packages | `mergeResourceConfig` + `ResourceSeams` (2.21) with kernel-typed engine ports |
|
|
28
|
+
| Same `view`/`manage` gate repeated across 5 CRUD keys | `permissionMatrix({ read, write, delete? })` (2.21) |
|
|
29
|
+
| DB connect + dynamic-import ordering in the composition root | `createApp({ beforeBoot })` (2.21 — runs before Fastify + all imports) |
|
|
30
|
+
| `import { x } from 'better-auth/plugins'` (barrel) | dedicated path `better-auth/plugins/<name>` — tree-shaking (BA best practice) |
|
|
31
|
+
| Hand-rolled per-tenant usage/billing counters | `usagePlugin` from `@classytic/arc/usage` (2.22 — per-actor per-period counters, fail-safe, `UsageStore` backends) |
|
|
32
|
+
| Per-plan rate limits via custom middleware | `rateLimit.plan { resolve, limits, default }` (2.22 — `false` = unlimited, fail-safe fallback, tenant-keyed buckets) |
|
|
33
|
+
| Hand-built audit-timeline endpoints (`GET /:id/history` handlers over audit rows) | `history: true` on `defineResource` (2.22 — implies `audit: true`, stricter-than-read gate) |
|
|
26
34
|
|
|
27
35
|
## Boot order (FIXED)
|
|
28
36
|
|
|
29
37
|
```
|
|
38
|
+
0. beforeBoot() ← pre-everything (DB connect; before Fastify + imports) — 2.21
|
|
39
|
+
0.5 module resolve ← thunk imports + dependsOn topo-sort; module errorMappers merge
|
|
30
40
|
1. Arc core (security, auth, events)
|
|
31
|
-
2. plugins() ← user infra (
|
|
41
|
+
2. plugins() ← user infra (docs, webhooks)
|
|
32
42
|
3. bootstrap[] ← domain init (engines, singletons)
|
|
33
43
|
4. resources (factory runs after bootstrap)
|
|
34
44
|
5. resources[] ← register each
|
|
@@ -54,7 +64,7 @@ defineResource({
|
|
|
54
64
|
rateLimit?, tenantField?, idField?,
|
|
55
65
|
prefix?, skipGlobalPrefix?,
|
|
56
66
|
queryParser?, onFieldWriteDenied?,
|
|
57
|
-
audit?, mcp?,
|
|
67
|
+
audit?, history?, mcp?, // mcp: false to exclude from MCP tool gen; history: true → GET /:id/history (2.22)
|
|
58
68
|
displayName?, module?,
|
|
59
69
|
onTenantDelete?, // GDPR cascade strategy
|
|
60
70
|
});
|
|
@@ -69,6 +79,7 @@ requireRoles(['admin']) requireServiceScope('jobs:bulk')
|
|
|
69
79
|
requireOwnership('userId') requireScopeContext('branchId')
|
|
70
80
|
requireOrgMembership() requireOrgInScope(targetId)
|
|
71
81
|
allOf(...) · anyOf(...) · not(...) · when(...) · denyAll()
|
|
82
|
+
permissionMatrix({ read, write, delete? }) // 2.21 — expands to the full CRUD map
|
|
72
83
|
createDynamicPermissionMatrix({ resolveRolePermissions, cacheStore })
|
|
73
84
|
```
|
|
74
85
|
|
|
@@ -159,7 +170,9 @@ import type { DataAdapter, RepositoryLike } from '@classytic/repo-core/adapter';
|
|
|
159
170
|
|
|
160
171
|
```typescript
|
|
161
172
|
import { defineResource, BaseController, allowPublic } from '@classytic/arc';
|
|
173
|
+
import { mergeResourceConfig, type ResourceSeams } from '@classytic/arc'; // 2.21
|
|
162
174
|
import { createApp, loadResources } from '@classytic/arc/factory';
|
|
175
|
+
import { schedulesPlugin } from '@classytic/arc/plugins'; // 2.21
|
|
163
176
|
import { MemoryCacheStore, RedisCacheStore, QueryCache } from '@classytic/arc/cache';
|
|
164
177
|
import { eventPlugin, EventOutbox } from '@classytic/arc/events';
|
|
165
178
|
import { RedisEventTransport, RedisStreamTransport } from '@classytic/arc/events/redis-stream';
|
|
@@ -21,13 +21,13 @@ In arc 2.12:
|
|
|
21
21
|
|
|
22
22
|
### Coordinated versions
|
|
23
23
|
|
|
24
|
-
| Package | Min |
|
|
25
|
-
|
|
26
|
-
| `@classytic/arc` | 2.12.0 |
|
|
27
|
-
| `@classytic/repo-core` | 0.4.0 |
|
|
28
|
-
| `@classytic/mongokit` | 3.13.0 |
|
|
29
|
-
| `@classytic/sqlitekit` | 0.3.0 |
|
|
30
|
-
| `@classytic/prismakit` | 0.1.0 |
|
|
24
|
+
| Package | Min (adapter split) | Current best-practice floor |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `@classytic/arc` | 2.12.0 | 2.21.0 — seams/`mergeResourceConfig`, `beforeBoot`, `permissionMatrix`, `schedulesPlugin` |
|
|
27
|
+
| `@classytic/repo-core` | 0.4.0 | 0.9.0 — `StandardRepo.getByIds` contract member |
|
|
28
|
+
| `@classytic/mongokit` | 3.13.0 | 3.21.0 — adapter `schemaGenerator` defaults ON |
|
|
29
|
+
| `@classytic/sqlitekit` | 0.3.0 | 0.7.0 |
|
|
30
|
+
| `@classytic/prismakit` | 0.1.0 | 0.1.0 |
|
|
31
31
|
|
|
32
32
|
### Import migration table
|
|
33
33
|
|
|
@@ -200,13 +200,14 @@ export const userRepo = new Repository<UserDoc>(User, [
|
|
|
200
200
|
// resources/user/user.resource.ts — arc resource consuming the mongokit repo
|
|
201
201
|
import { defineResource, requireRoles, allowPublic, fields } from '@classytic/arc';
|
|
202
202
|
import { createMongooseAdapter } from '@classytic/mongokit/adapter'; // arc 2.12+: from the kit
|
|
203
|
-
import { buildCrudSchemasFromModel } from '@classytic/mongokit';
|
|
204
203
|
import { User } from '../../models/user.js';
|
|
205
204
|
import { userRepo } from '../../repositories/userRepository.js';
|
|
206
205
|
|
|
207
206
|
export const userResource = defineResource({
|
|
208
207
|
name: 'user',
|
|
209
|
-
|
|
208
|
+
// mongokit >=3.21 defaults schemaGenerator to buildCrudSchemasFromModel
|
|
209
|
+
// (pre-3.21: wire it explicitly, or OpenAPI/MCP bodies come out null)
|
|
210
|
+
adapter: createMongooseAdapter({ model: User, repository: userRepo }),
|
|
210
211
|
presets: ['softDelete'], // multiTenant is at the repo layer (mongokit), so we don't need the arc preset too — pick one
|
|
211
212
|
permissions: {
|
|
212
213
|
list: requireRoles(['admin']),
|
|
@@ -344,7 +345,7 @@ For each Mongoose model in the project:
|
|
|
344
345
|
|
|
345
346
|
If consumers see `as unknown as RepositoryLike<T>` casts when wiring a repo into the mongoose adapter, that's drift. Mongokit's `tests/unit/standard-repo-assignment.test-d.ts` proves whole-interface assignment via TypeScript. Run `npm run typecheck:tests` in mongokit; if it fails, the published version drifted from the contract — pin to a known-good version or open a PR.
|
|
346
347
|
|
|
347
|
-
In the client project, prefer `createMongooseAdapter({ model, repository
|
|
348
|
+
In the client project, prefer `createMongooseAdapter({ model, repository })` from `@classytic/mongokit/adapter` over manual `RepositoryLike` shaping — it accepts mongokit-native repos with no casts, and since mongokit 3.21 `schemaGenerator` defaults to `buildCrudSchemasFromModel` (`schemaGenerator: false` opts out — see anti-patterns §32f before recommending that).
|
|
348
349
|
|
|
349
350
|
## arc 2.12 / mongokit 3.13.0 — adapter split
|
|
350
351
|
|
|
@@ -11,7 +11,8 @@ description: |
|
|
|
11
11
|
writing a defineModule() or an action preset, or preparing an arc module for npm publish.
|
|
12
12
|
Triggers: publish arc module, arc package, arc ecosystem, defineModule, ArcModule, arc
|
|
13
13
|
plugin package, arc action preset, extract domain to package, arc-approval, arc-involvement,
|
|
14
|
-
peer dependencies arc, tsdown arc package, arc module conventions, classytic publish
|
|
14
|
+
peer dependencies arc, tsdown arc package, arc module conventions, classytic publish,
|
|
15
|
+
mergeResourceConfig, ResourceSeams, kernel-typed engine port, module errorMappers.
|
|
15
16
|
license: MIT
|
|
16
17
|
metadata:
|
|
17
18
|
author: Classytic
|
|
@@ -129,6 +129,11 @@ export const accountingModule = (deps: AccountingDeps) =>
|
|
|
129
129
|
|
|
130
130
|
afterResources: (f) => { /* cross-resource event subscriptions the module owns */ },
|
|
131
131
|
onClose: (f) => { /* stop timers, flush outbox, destroy engine */ },
|
|
132
|
+
|
|
133
|
+
errorMappers: [{ // 2.21 — domain error → wire envelope, merged into the
|
|
134
|
+
type: AccountingError, // host's error handler at boot (host mappers keep priority)
|
|
135
|
+
toResponse: (err) => ({ status: err.status, code: err.code, message: err.message }),
|
|
136
|
+
}],
|
|
132
137
|
});
|
|
133
138
|
```
|
|
134
139
|
|
|
@@ -198,6 +203,56 @@ packages (or several subpath exports) and let the host list them — don't retur
|
|
|
198
203
|
modules from one factory. The host owns the flat `modules:` list and its ordering; keep that
|
|
199
204
|
control theirs.
|
|
200
205
|
|
|
206
|
+
### Resource assembly — kernel-typed ports + `mergeResourceConfig` (2.21 standard)
|
|
207
|
+
|
|
208
|
+
Two rules kill the `as never` tax that plagued pre-2.21 module packages:
|
|
209
|
+
|
|
210
|
+
**1. Type the engine port with the KERNEL's own bags.** The kernel is already your peer, so
|
|
211
|
+
indexing its exported engine type adds zero coupling — and its truths (optional sub-module
|
|
212
|
+
members, literal unions) flow into your resource builders:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import type { CatalogEngine } from "@classytic/catalog";
|
|
216
|
+
|
|
217
|
+
export interface CatalogEngineLike { // structural — any conforming engine composes
|
|
218
|
+
models: CatalogEngine["models"];
|
|
219
|
+
repositories: CatalogEngine["repositories"];
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Never re-declare the bags as `Record<string, unknown>` — that's what forced every
|
|
224
|
+
`createMongooseAdapter(engine.models.X as never, ...)` cast.
|
|
225
|
+
|
|
226
|
+
**2. Merge base config + host seams with `mergeResourceConfig`, not hand-rolled spreads.**
|
|
227
|
+
Expose your per-resource override surface as projections of arc's `ResourceSeams` and let
|
|
228
|
+
arc's slot-aware merge do assembly — arrays CONCAT (host routes append, never clobber),
|
|
229
|
+
plain objects deep-merge, class instances (queryParser, adapter) last-win, `undefined`
|
|
230
|
+
never overwrites, `as const` readonly host tables are accepted:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { defineResource, mergeResourceConfig, type ResourceSeams } from "@classytic/arc";
|
|
234
|
+
import { permissionMatrix } from "@classytic/arc/permissions";
|
|
235
|
+
|
|
236
|
+
export interface ProductSeams { // your public seam names, arc's slot types
|
|
237
|
+
extraRoutes?: ResourceSeams["routes"];
|
|
238
|
+
extraActions?: ResourceSeams["actions"];
|
|
239
|
+
cache?: ResourceSeams["cache"];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
defineResource(mergeResourceConfig(
|
|
243
|
+
{
|
|
244
|
+
name: "product",
|
|
245
|
+
adapter: createMongooseAdapter({ model: models.Product, repository: repositories.product }),
|
|
246
|
+
permissions: permissionMatrix({ read: deps.permissions.view, write: deps.permissions.manage }),
|
|
247
|
+
},
|
|
248
|
+
{ routes: cfg.extraRoutes, actions: cfg.extraActions, cache: cfg.cache },
|
|
249
|
+
));
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The only casts left in a well-formed package are documented boundaries: wire-body narrowing
|
|
253
|
+
in action handlers (`data as { role: PartyRole }` — narrow to the KERNEL's union, not
|
|
254
|
+
`string`) and hydrated-vs-lean id widening at a kernel bridge. Anything else is a smell.
|
|
255
|
+
|
|
201
256
|
---
|
|
202
257
|
|
|
203
258
|
## Testing a package
|
|
@@ -38,8 +38,10 @@ optional-peer typing pattern. Values below are the real shapes shipped by
|
|
|
38
38
|
},
|
|
39
39
|
|
|
40
40
|
// CONTRACTS — what the host must provide. `>=`, never `^`.
|
|
41
|
+
// Floor >=2.21.0 if you use ResourceSeams / mergeResourceConfig / permissionMatrix /
|
|
42
|
+
// schedulesPlugin / errorMappers — declare the floor that matches the APIs you import.
|
|
41
43
|
"peerDependencies": {
|
|
42
|
-
"@classytic/arc": ">=2.
|
|
44
|
+
"@classytic/arc": ">=2.21.0",
|
|
43
45
|
"@classytic/primitives": ">=0.9.0"
|
|
44
46
|
},
|
|
45
47
|
|
|
@@ -52,7 +54,7 @@ optional-peer typing pattern. Values below are the real shapes shipped by
|
|
|
52
54
|
|
|
53
55
|
// TEST versions — pin with ^, floor >= each peer floor. Not shipped.
|
|
54
56
|
"devDependencies": {
|
|
55
|
-
"@classytic/arc": "^2.
|
|
57
|
+
"@classytic/arc": "^2.21.0",
|
|
56
58
|
"@classytic/primitives": "^0.9.1",
|
|
57
59
|
"@biomejs/biome": "^2.5.2",
|
|
58
60
|
"@types/node": "^22.19.21",
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
//#region src/testing/preloadResources.ts
|
|
2
|
-
/**
|
|
3
|
-
* Normalize an eager `import.meta.glob` result into a `ResourceLike[]`.
|
|
4
|
-
*
|
|
5
|
-
* Accepts either:
|
|
6
|
-
* - `{ import: 'default' }` form: values are the resource directly
|
|
7
|
-
* - default form: values are the full module — picks first export with `toPlugin()`
|
|
8
|
-
*
|
|
9
|
-
* Throws if any module doesn't yield a valid `ResourceLike`.
|
|
10
|
-
*/
|
|
11
|
-
function preloadResources(globResult) {
|
|
12
|
-
const resources = [];
|
|
13
|
-
for (const [path, value] of Object.entries(globResult)) {
|
|
14
|
-
const resource = pickResource(value);
|
|
15
|
-
if (!resource) throw new Error(`preloadResources: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
|
|
16
|
-
resources.push(resource);
|
|
17
|
-
}
|
|
18
|
-
return resources.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Normalize a lazy `import.meta.glob` result into a `Promise<ResourceLike[]>`.
|
|
22
|
-
*
|
|
23
|
-
* Use this when resources depend on prior bootstrap (e.g., engine init) and
|
|
24
|
-
* cannot be evaluated at import time of the preload file.
|
|
25
|
-
*/
|
|
26
|
-
async function preloadResourcesAsync(globResult) {
|
|
27
|
-
return (await Promise.all(Object.entries(globResult).map(async ([path, loader]) => {
|
|
28
|
-
const resource = pickResource(await loader());
|
|
29
|
-
if (!resource) throw new Error(`preloadResourcesAsync: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
|
|
30
|
-
return resource;
|
|
31
|
-
}))).sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
|
|
32
|
-
}
|
|
33
|
-
function pickResource(value) {
|
|
34
|
-
if (!value || typeof value !== "object") return void 0;
|
|
35
|
-
if (typeof value.toPlugin === "function") return value;
|
|
36
|
-
const mod = value;
|
|
37
|
-
const candidates = [
|
|
38
|
-
mod.default,
|
|
39
|
-
mod.resource,
|
|
40
|
-
...Object.values(mod)
|
|
41
|
-
];
|
|
42
|
-
for (const c of candidates) if (c && typeof c === "object" && typeof c.toPlugin === "function") return c;
|
|
43
|
-
}
|
|
44
|
-
//#endregion
|
|
45
|
-
export { preloadResourcesAsync as n, preloadResources as t };
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
//#region src/utils/schemaConverter.ts
|
|
2
|
-
/** Default target for Fastify-consumed schemas (matches Fastify v5's default AJV draft). */
|
|
3
|
-
const DEFAULT_FASTIFY_TARGET = "draft-7";
|
|
4
|
-
/** Default target for OpenAPI document generation (matches arc's emitted OpenAPI version). */
|
|
5
|
-
const DEFAULT_OPENAPI_TARGET = "openapi-3.0";
|
|
6
|
-
let _toJSONSchema = null;
|
|
7
|
-
import("zod").then(({ z }) => {
|
|
8
|
-
if (typeof z?.toJSONSchema === "function") _toJSONSchema = (schema, opts) => z.toJSONSchema(schema, opts);
|
|
9
|
-
}).catch(() => {});
|
|
10
|
-
/**
|
|
11
|
-
* Check if an object is already a plain JSON Schema.
|
|
12
|
-
* Returns true if it has JSON Schema markers (`type`, `properties`, `$ref`,
|
|
13
|
-
* `allOf`, `anyOf`, `oneOf`, `items`, `enum`) and does NOT have Zod markers.
|
|
14
|
-
*/
|
|
15
|
-
function isJsonSchema(input) {
|
|
16
|
-
if (input === null || typeof input !== "object") return false;
|
|
17
|
-
const obj = input;
|
|
18
|
-
if ("_def" in obj || "_zod" in obj) return false;
|
|
19
|
-
return "type" in obj || "properties" in obj || "$ref" in obj || "allOf" in obj || "anyOf" in obj || "oneOf" in obj || "items" in obj || "enum" in obj;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Check if an object is a Zod schema (has `_zod` marker from Zod v4).
|
|
23
|
-
*/
|
|
24
|
-
function isZodSchema(input) {
|
|
25
|
-
return input !== null && typeof input === "object" && "_zod" in input;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Convert any schema input to JSON Schema.
|
|
29
|
-
*
|
|
30
|
-
* Detection order:
|
|
31
|
-
* 1. `null`/`undefined` → `undefined`
|
|
32
|
-
* 2. Already JSON Schema → pass through as-is (zero overhead)
|
|
33
|
-
* 3. Zod v4 schema → `z.toJSONSchema(schema, { target })`
|
|
34
|
-
* 4. Unrecognized object → return as-is (treat as opaque schema)
|
|
35
|
-
*
|
|
36
|
-
* @param input Schema (Zod, plain JSON Schema, or opaque object)
|
|
37
|
-
* @param target Output target — defaults to `draft-7` for Fastify compatibility.
|
|
38
|
-
* Pass `openapi-3.0`/`openapi-3.1` for OpenAPI document generation.
|
|
39
|
-
*/
|
|
40
|
-
function toJsonSchema(input, target = DEFAULT_FASTIFY_TARGET) {
|
|
41
|
-
if (input == null) return void 0;
|
|
42
|
-
if (typeof input !== "object") return void 0;
|
|
43
|
-
if (isJsonSchema(input)) return input;
|
|
44
|
-
if (isZodSchema(input)) {
|
|
45
|
-
if (!_toJSONSchema) return input;
|
|
46
|
-
try {
|
|
47
|
-
const converted = _toJSONSchema(input, { target });
|
|
48
|
-
if ("$schema" in converted) delete converted.$schema;
|
|
49
|
-
return converted;
|
|
50
|
-
} catch {
|
|
51
|
-
return { type: "object" };
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return input;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Convert all schema fields in an OpenApiSchemas object.
|
|
58
|
-
* JSON Schema values pass through unchanged. Only Zod schemas are converted.
|
|
59
|
-
*
|
|
60
|
-
* Defaults to the `openapi-3.0` target since this function feeds OpenAPI doc
|
|
61
|
-
* generation, not Fastify route validation.
|
|
62
|
-
*/
|
|
63
|
-
function convertOpenApiSchemas(schemas, target = DEFAULT_OPENAPI_TARGET) {
|
|
64
|
-
const result = {};
|
|
65
|
-
const schemaFields = [
|
|
66
|
-
"entity",
|
|
67
|
-
"createBody",
|
|
68
|
-
"updateBody",
|
|
69
|
-
"params",
|
|
70
|
-
"listQuery",
|
|
71
|
-
"response"
|
|
72
|
-
];
|
|
73
|
-
for (const field of schemaFields) {
|
|
74
|
-
const value = schemas[field];
|
|
75
|
-
if (value !== void 0) result[field] = toJsonSchema(value, target) ?? value;
|
|
76
|
-
}
|
|
77
|
-
for (const [key, value] of Object.entries(schemas)) if (!schemaFields.includes(key)) result[key] = value;
|
|
78
|
-
return result;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Convert schema values in a Fastify route schema record.
|
|
82
|
-
*
|
|
83
|
-
* Handles `body`, `querystring`, `params`, `headers` (top-level conversion)
|
|
84
|
-
* and `response` (iterates by status code — each value converted individually).
|
|
85
|
-
*
|
|
86
|
-
* JSON Schema values pass through unchanged. Only Zod schemas are converted.
|
|
87
|
-
*
|
|
88
|
-
* Used for both custom routes and customSchemas (CRUD overrides).
|
|
89
|
-
*
|
|
90
|
-
* Defaults to `draft-7` so Fastify v5's bundled AJV 8 accepts the output.
|
|
91
|
-
* Pass `openapi-3.0` (or `openapi-3.1`) when generating OpenAPI documents.
|
|
92
|
-
*/
|
|
93
|
-
function convertRouteSchema(schema, target = DEFAULT_FASTIFY_TARGET) {
|
|
94
|
-
const result = { ...schema };
|
|
95
|
-
for (const field of [
|
|
96
|
-
"body",
|
|
97
|
-
"querystring",
|
|
98
|
-
"params",
|
|
99
|
-
"headers"
|
|
100
|
-
]) if (result[field] !== void 0) result[field] = toJsonSchema(result[field], target) ?? result[field];
|
|
101
|
-
if (result.response !== void 0 && typeof result.response === "object" && result.response !== null) {
|
|
102
|
-
const responseObj = result.response;
|
|
103
|
-
const convertedResponse = {};
|
|
104
|
-
for (const [statusCode, responseSchema] of Object.entries(responseObj)) convertedResponse[statusCode] = toJsonSchema(responseSchema, target) ?? responseSchema;
|
|
105
|
-
result.response = convertedResponse;
|
|
106
|
-
}
|
|
107
|
-
return result;
|
|
108
|
-
}
|
|
109
|
-
//#endregion
|
|
110
|
-
export { toJsonSchema as a, isZodSchema as i, convertRouteSchema as n, isJsonSchema as r, convertOpenApiSchemas as t };
|