@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
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# Mongoose → @classytic/mongokit Migration
|
|
2
|
+
|
|
3
|
+
For projects that have arc installed but still use raw Mongoose models with hand-rolled repository classes, manual pagination, scattered `pre`/`post` hooks, and per-schema `toJSON` transforms.
|
|
4
|
+
|
|
5
|
+
Mongokit is a Mongoose enhancement layer that:
|
|
6
|
+
- Wraps a `mongoose.Model` in a `Repository<TDoc>` that implements `MinimalRepo` + `Partial<StandardRepo>` from `@classytic/repo-core` (so it plugs into the arc adapter with no casts).
|
|
7
|
+
- Ships the **arc adapter itself** at `@classytic/mongokit/adapter` (since 3.13.0 / arc 2.12). Through arc 2.12 the adapter lived in arc; in arc 2.12 every kit-specific adapter moved out (Mongoose to mongokit, Drizzle to sqlitekit, Prisma to prismakit) so arc no longer pulls a DB driver into any consumer's resolution graph.
|
|
8
|
+
- Provides 15+ plugins: `timestampPlugin`, `softDeletePlugin`, `multiTenantPlugin`, `validationChainPlugin`, `cachePlugin`, `auditTrailPlugin`, `auditLogPlugin`, `cascadePlugin`, `customIdPlugin`, `observabilityPlugin`, `elasticSearchPlugin`, `mongoOperationsPlugin`, `aggregateHelpersPlugin`, `fieldFilterPlugin`, `batchOperationsPlugin`.
|
|
9
|
+
- Ships `QueryParser`, `AggregationBuilder`, `LookupBuilder`, `PaginationEngine` (offset + keyset auto-detect).
|
|
10
|
+
- Provides `withTransaction` helper with auto-retry and standalone fallback.
|
|
11
|
+
|
|
12
|
+
**It does NOT**:
|
|
13
|
+
- Set `schema.set('toJSON', ...)` — read transforms are framework/controller responsibility (arc handles it via `fieldRules.hidden`).
|
|
14
|
+
- Own connection lifecycle — `mongoose.connect()` stays in your bootstrap.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Detection — is mongokit migration needed?
|
|
19
|
+
|
|
20
|
+
Add to the audit detection sweep:
|
|
21
|
+
|
|
22
|
+
| Signal | Detection |
|
|
23
|
+
|---|---|
|
|
24
|
+
| Mongoose without mongokit | `package.json`: `mongoose` present, `@classytic/mongokit` absent |
|
|
25
|
+
| Per-model repo class | `Grep "class \\w+Repository\\b"` — count classes |
|
|
26
|
+
| Manual `pre`/`post` hooks | `Grep "schema\\.(pre\\|post)\\(['\"]"` |
|
|
27
|
+
| Hand-rolled `toJSON` | `Grep "schema\\.set\\(['\"]toJSON\\|toJSON\\s*=\\s*function"` |
|
|
28
|
+
| Manual pagination | `Grep "skip\\(.*page\\|countDocuments\\("` |
|
|
29
|
+
| Per-method tenant filter | `Grep "organizationId.*req\\.user\\|orgId.*scope"` (paired with `find` / `findOne`) |
|
|
30
|
+
| Manual `findByIdAndUpdate(..., { runValidators: true })` | `Grep "runValidators"` |
|
|
31
|
+
| Manual transaction session threading | `Grep "session.*startSession\\|session\\.commitTransaction"` |
|
|
32
|
+
|
|
33
|
+
If `mongoose` deps + `@classytic/mongokit` absent → every model is a candidate. Estimate: a typical 150 LOC repo class shrinks to ~30 LOC with mongokit + plugins.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Mapping table — Mongoose pattern → mongokit replacement
|
|
38
|
+
|
|
39
|
+
| Hand-rolled Mongoose | Mongokit replacement |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `class UserRepository { async create(d) { return User.create(d) } /* + 15 more */ }` | `new Repository(User)` (or extend for domain verbs) |
|
|
42
|
+
| `userSchema.pre('save', function(next) { this.updatedAt = new Date(); next(); })` | `timestampPlugin()` |
|
|
43
|
+
| `userSchema.set('toJSON', { transform: (_, ret) => { delete ret.password; ... } })` | Arc `fieldRules: { password: { hidden: true } }` (mongokit doesn't do this) |
|
|
44
|
+
| `Model.find({ deletedAt: null, ...filters })` everywhere | `softDeletePlugin({ deletedField: 'deletedAt' })` |
|
|
45
|
+
| `Model.find({ ...filters, organizationId: orgId })` everywhere | `multiTenantPlugin({ tenantField: 'organizationId', contextKey: 'organizationId' })` |
|
|
46
|
+
| Manual pagination wrappers (skip/limit + countDocuments) | `repo.getAll({ page, limit, sort })` (offset) or `repo.getAll({ sort, after: cursor })` (keyset auto-detect) |
|
|
47
|
+
| `Model.findOne({ email })` for unique check before create | `validationChainPlugin([uniqueField('email')])` |
|
|
48
|
+
| Manual `findOneAndUpdate(..., { new: true, runValidators: true })` | `repo.update(id, data)` (validators on by default) |
|
|
49
|
+
| Manual `mongoose.startSession()` + `withTransaction` boilerplate | `repo.withTransaction(async (session) => { ... })` |
|
|
50
|
+
| Manual cache invalidation around `Model.find` | `cachePlugin({ adapter, ttl, byIdTtl })` |
|
|
51
|
+
| Manual audit-log writes on every mutation | `auditTrailPlugin()` + `auditLogPlugin()` |
|
|
52
|
+
| Hand-coded URL → query parsing | `new QueryParser({ schema, allowedFilterFields, ... })` |
|
|
53
|
+
| Manual `$lookup`/`$match` aggregation building | `AggregationBuilder` / `LookupBuilder` |
|
|
54
|
+
| Stripe-style prefixed IDs (`cus_xxx`, `ord_xxx`) | `customIdPlugin({ strategy, publicIdField })` |
|
|
55
|
+
|
|
56
|
+
Hooks fire with priority ordering: `POLICY` (100, multi-tenant + soft-delete pre-filtering) → `CACHE` (200) → `OBSERVABILITY` (300) → `DEFAULT` (500, user hooks).
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Recipe — User model migration
|
|
61
|
+
|
|
62
|
+
### Before — Mongoose only (~140 LOC)
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// models/user.ts
|
|
66
|
+
import { Schema, model, Document } from 'mongoose';
|
|
67
|
+
|
|
68
|
+
export interface UserDoc extends Document {
|
|
69
|
+
name: string;
|
|
70
|
+
email: string;
|
|
71
|
+
password: string;
|
|
72
|
+
organizationId: Types.ObjectId;
|
|
73
|
+
deletedAt: Date | null;
|
|
74
|
+
createdAt: Date;
|
|
75
|
+
updatedAt: Date;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const userSchema = new Schema<UserDoc>({
|
|
79
|
+
name: { type: String, required: true },
|
|
80
|
+
email: { type: String, required: true, unique: true },
|
|
81
|
+
password: { type: String, required: true },
|
|
82
|
+
organizationId: { type: Schema.Types.ObjectId, required: true, ref: 'Organization' },
|
|
83
|
+
deletedAt: { type: Date, default: null },
|
|
84
|
+
createdAt: { type: Date },
|
|
85
|
+
updatedAt: { type: Date },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
userSchema.pre('save', function (next) {
|
|
89
|
+
if (this.isNew) this.createdAt = new Date();
|
|
90
|
+
this.updatedAt = new Date();
|
|
91
|
+
next();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
userSchema.set('toJSON', {
|
|
95
|
+
transform: (_doc, ret) => {
|
|
96
|
+
delete ret.password;
|
|
97
|
+
delete ret.__v;
|
|
98
|
+
return ret;
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export const User = model<UserDoc>('User', userSchema);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// repositories/userRepository.ts
|
|
107
|
+
import { User, UserDoc } from '../models/user.js';
|
|
108
|
+
|
|
109
|
+
export class UserRepository {
|
|
110
|
+
async create(data: Partial<UserDoc>, orgId: string) {
|
|
111
|
+
const exists = await User.findOne({ email: data.email });
|
|
112
|
+
if (exists) throw new Error('Email taken');
|
|
113
|
+
return User.create({ ...data, organizationId: orgId });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async getById(id: string, orgId: string) {
|
|
117
|
+
return User.findOne({ _id: id, organizationId: orgId, deletedAt: null }).lean();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async list(orgId: string, page: number, limit: number, filters: Record<string, unknown>) {
|
|
121
|
+
const query = { ...filters, organizationId: orgId, deletedAt: null };
|
|
122
|
+
const skip = (page - 1) * limit;
|
|
123
|
+
const docs = await User.find(query).skip(skip).limit(limit).sort({ createdAt: -1 }).lean();
|
|
124
|
+
const total = await User.countDocuments(query);
|
|
125
|
+
return { docs, total, page, pages: Math.ceil(total / limit) };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async update(id: string, data: Partial<UserDoc>, orgId: string) {
|
|
129
|
+
return User.findOneAndUpdate(
|
|
130
|
+
{ _id: id, organizationId: orgId, deletedAt: null },
|
|
131
|
+
{ ...data, updatedAt: new Date() },
|
|
132
|
+
{ new: true, runValidators: true },
|
|
133
|
+
).lean();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async softDelete(id: string, orgId: string) {
|
|
137
|
+
return User.findOneAndUpdate(
|
|
138
|
+
{ _id: id, organizationId: orgId, deletedAt: null },
|
|
139
|
+
{ deletedAt: new Date() },
|
|
140
|
+
{ new: true },
|
|
141
|
+
).lean();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async restore(id: string, orgId: string) {
|
|
145
|
+
return User.findOneAndUpdate(
|
|
146
|
+
{ _id: id, organizationId: orgId, deletedAt: { $ne: null } },
|
|
147
|
+
{ deletedAt: null },
|
|
148
|
+
{ new: true },
|
|
149
|
+
).lean();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### After — Mongoose + mongokit (~50 LOC)
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// models/user.ts (unchanged structure — drop pre('save') and toJSON transform)
|
|
158
|
+
import { Schema, model, Document } from 'mongoose';
|
|
159
|
+
|
|
160
|
+
export interface UserDoc extends Document {
|
|
161
|
+
name: string;
|
|
162
|
+
email: string;
|
|
163
|
+
password: string;
|
|
164
|
+
organizationId: Types.ObjectId;
|
|
165
|
+
deletedAt: Date | null;
|
|
166
|
+
createdAt: Date;
|
|
167
|
+
updatedAt: Date;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const userSchema = new Schema<UserDoc>({
|
|
171
|
+
name: { type: String, required: true },
|
|
172
|
+
email: { type: String, required: true, unique: true },
|
|
173
|
+
password: { type: String, required: true },
|
|
174
|
+
organizationId: { type: Schema.Types.ObjectId, required: true, ref: 'Organization' },
|
|
175
|
+
deletedAt: { type: Date, default: null },
|
|
176
|
+
createdAt: { type: Date },
|
|
177
|
+
updatedAt: { type: Date },
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
export const User = model<UserDoc>('User', userSchema);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// repositories/userRepository.ts
|
|
185
|
+
import { Repository, methodRegistryPlugin, timestampPlugin, softDeletePlugin,
|
|
186
|
+
multiTenantPlugin, validationChainPlugin } from '@classytic/mongokit';
|
|
187
|
+
import { uniqueField } from '@classytic/mongokit/plugins/validators';
|
|
188
|
+
import { User, UserDoc } from '../models/user.js';
|
|
189
|
+
|
|
190
|
+
export const userRepo = new Repository<UserDoc>(User, [
|
|
191
|
+
methodRegistryPlugin(),
|
|
192
|
+
timestampPlugin(),
|
|
193
|
+
multiTenantPlugin({ tenantField: 'organizationId', contextKey: 'organizationId', required: true }),
|
|
194
|
+
softDeletePlugin({ deletedField: 'deletedAt' }),
|
|
195
|
+
validationChainPlugin([uniqueField('email', 'Email already in use')]),
|
|
196
|
+
]);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// resources/user/user.resource.ts — arc resource consuming the mongokit repo
|
|
201
|
+
import { defineResource, requireRoles, allowPublic, fields } from '@classytic/arc';
|
|
202
|
+
import { createMongooseAdapter } from '@classytic/mongokit/adapter'; // arc 2.12+: from the kit
|
|
203
|
+
import { buildCrudSchemasFromModel } from '@classytic/mongokit';
|
|
204
|
+
import { User } from '../../models/user.js';
|
|
205
|
+
import { userRepo } from '../../repositories/userRepository.js';
|
|
206
|
+
|
|
207
|
+
export const userResource = defineResource({
|
|
208
|
+
name: 'user',
|
|
209
|
+
adapter: createMongooseAdapter({ model: User, repository: userRepo, schemaGenerator: buildCrudSchemasFromModel }),
|
|
210
|
+
presets: ['softDelete'], // multiTenant is at the repo layer (mongokit), so we don't need the arc preset too — pick one
|
|
211
|
+
permissions: {
|
|
212
|
+
list: requireRoles(['admin']),
|
|
213
|
+
get: allowPublic(),
|
|
214
|
+
create: requireRoles(['admin']),
|
|
215
|
+
update: requireRoles(['admin']),
|
|
216
|
+
delete: requireRoles(['admin']),
|
|
217
|
+
},
|
|
218
|
+
schemaOptions: {
|
|
219
|
+
fieldRules: {
|
|
220
|
+
password: fields.hidden(),
|
|
221
|
+
organizationId: { systemManaged: true },
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**What changed:**
|
|
228
|
+
- Repository class: 80 LOC → 8 LOC.
|
|
229
|
+
- `pre('save')` for timestamps → `timestampPlugin()` (1 line).
|
|
230
|
+
- Tenant scoping: 5 duplicated places → 1 plugin.
|
|
231
|
+
- Soft-delete with `restore`/`getDeleted`: ~30 LOC → 1 plugin.
|
|
232
|
+
- Email uniqueness: hand-coded `findOne` race → `validationChainPlugin([uniqueField(...)])`.
|
|
233
|
+
- `toJSON` transform → arc `fields.hidden()` (works on `.lean()` too).
|
|
234
|
+
- `findByIdAndUpdate(..., { runValidators: true })` boilerplate → `repo.update(id, data)`.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Where to put multi-tenant scoping — repo or resource?
|
|
239
|
+
|
|
240
|
+
**Decision rule:** put it in **one** layer, not both. Two recommended patterns:
|
|
241
|
+
|
|
242
|
+
**Option A — at the repo layer (mongokit plugin):**
|
|
243
|
+
```typescript
|
|
244
|
+
new Repository(User, [multiTenantPlugin({ tenantField: 'organizationId', contextKey: 'organizationId' })]);
|
|
245
|
+
```
|
|
246
|
+
- Pros: works for non-HTTP callers (jobs, CLI, hooks). Centralized.
|
|
247
|
+
- Cons: requires passing a `RepositoryContext` with `organizationId` to every call.
|
|
248
|
+
|
|
249
|
+
**Option B — at the arc resource layer (preset):**
|
|
250
|
+
```typescript
|
|
251
|
+
defineResource({ presets: [{ name: 'multiTenant', tenantField: 'organizationId' }] });
|
|
252
|
+
```
|
|
253
|
+
- Pros: arc handles context threading from `request.scope` automatically.
|
|
254
|
+
- Cons: scoped only to HTTP/MCP entry points; jobs/CLI need to thread context themselves.
|
|
255
|
+
|
|
256
|
+
**For new code:** prefer (B) — arc is the boundary. If you have non-HTTP entrypoints that hit the same repo, layer (A) on top of (B) and let the repo plugin be a defense-in-depth check (it'll no-op when arc has already injected the filter).
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Hook system parity
|
|
261
|
+
|
|
262
|
+
Mongoose `schema.pre('save', ...)` is per-model and per-method. Mongokit hooks are per-repo and per-event with priority ordering:
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
repo.on('before:create', async (ctx) => { /* validate, mutate ctx.data */ });
|
|
266
|
+
repo.on('after:create', async (event) => { /* event = { context, result } */ });
|
|
267
|
+
repo.on('error:create', async (event) => { /* event = { context, error } */ });
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Events:** `before:{create|update|delete|findAll|getById|...}`, `after:*`, `error:*`.
|
|
271
|
+
**Priorities:** `POLICY = 100` (filter injection runs first) → `CACHE = 200` → `OBSERVABILITY = 300` → `DEFAULT = 500`.
|
|
272
|
+
|
|
273
|
+
User hooks default to `DEFAULT` (500). Override with `repo.on('before:create', fn, { priority: HOOK_PRIORITY.POLICY })`.
|
|
274
|
+
|
|
275
|
+
Migrating Mongoose hooks:
|
|
276
|
+
- `schema.pre('save', ...)` (insert + update both) → split into `before:create` and `before:update`.
|
|
277
|
+
- `schema.pre('findOneAndUpdate', ...)` → `before:update`.
|
|
278
|
+
- `schema.pre('remove', ...)` → `before:delete`.
|
|
279
|
+
- `schema.post('save', ...)` → `after:create` / `after:update`.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Pagination
|
|
284
|
+
|
|
285
|
+
Auto-detected by `getAll`:
|
|
286
|
+
```typescript
|
|
287
|
+
// Offset (page-based)
|
|
288
|
+
await repo.getAll({ page: 2, limit: 20, sort: { createdAt: -1 } });
|
|
289
|
+
// → { method: 'offset', docs, total, pages, hasNext, hasPrev }
|
|
290
|
+
|
|
291
|
+
// Keyset (cursor)
|
|
292
|
+
const p1 = await repo.getAll({ sort: { createdAt: -1 }, limit: 20 });
|
|
293
|
+
const p2 = await repo.getAll({ sort: { createdAt: -1 }, limit: 20, after: p1.next });
|
|
294
|
+
// → { method: 'keyset', docs, hasMore, next }
|
|
295
|
+
|
|
296
|
+
// Aggregate paginate (with $near rewriting handled)
|
|
297
|
+
await repo.aggregatePipelinePaginate(pipeline, { page, limit });
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Drop hand-rolled `Math.ceil(total / limit)` and pagination DTOs — mongokit returns the envelope.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Transactions
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
await repo.withTransaction(async (session) => {
|
|
308
|
+
await repo.update(id, { status: 'paid' }, { session });
|
|
309
|
+
await invoiceRepo.create(invoiceData, { session });
|
|
310
|
+
});
|
|
311
|
+
// Auto-retries on TransientTransactionError / UnknownTransactionCommitResult
|
|
312
|
+
// Falls back to non-transactional when running on standalone Mongo (with allowFallback: true)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Cross-repo transaction:
|
|
316
|
+
```typescript
|
|
317
|
+
import { withTransaction } from '@classytic/mongokit';
|
|
318
|
+
await withTransaction(mongoose.connection, async (session) => {
|
|
319
|
+
await orderRepo.update(id, ..., { session });
|
|
320
|
+
await invoiceRepo.create(..., { session });
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Per-resource adoption order
|
|
327
|
+
|
|
328
|
+
For each Mongoose model in the project:
|
|
329
|
+
|
|
330
|
+
1. Drop `pre('save')` for timestamps; add `timestampPlugin()` to the repo.
|
|
331
|
+
2. Drop `schema.set('toJSON', ...)`; declare sensitive fields in arc `fieldRules: { password: fields.hidden() }`.
|
|
332
|
+
3. Replace the per-model repository class with `new Repository(Model, [...plugins])`.
|
|
333
|
+
4. Convert Mongoose hooks (`schema.pre/post`) to `repo.on('before:*'/'after:*', ...)`.
|
|
334
|
+
5. If multi-tenant: pick repo-layer or resource-layer scoping (one, not both).
|
|
335
|
+
6. If soft-delete: add `softDeletePlugin` (drops the manual `deletedAt: null` filter scattered across reads).
|
|
336
|
+
7. Replace pagination wrappers with `repo.getAll(...)`.
|
|
337
|
+
8. Wire the repo into the arc resource via `createMongooseAdapter({ model, repository: repo, schemaGenerator: buildCrudSchemasFromModel })`.
|
|
338
|
+
9. Run the existing test suite — should pass with no logic changes (mongokit's defaults match Mongoose semantics).
|
|
339
|
+
10. Add `npm run typecheck:tests` step in CI to lock conformance with `@classytic/repo-core`'s `StandardRepo`.
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Conformance gate
|
|
344
|
+
|
|
345
|
+
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
|
+
In the client project, prefer `createMongooseAdapter({ model, repository, schemaGenerator })` from `@classytic/mongokit/adapter` over manual `RepositoryLike` shaping — it accepts mongokit-native repos with no casts.
|
|
348
|
+
|
|
349
|
+
## arc 2.12 / mongokit 3.13.0 — adapter split
|
|
350
|
+
|
|
351
|
+
Through mongokit 3.12 / arc 2.11, `createMongooseAdapter` shipped from `@classytic/arc`. In mongokit 3.13.0 + arc 2.12, the adapter lives in mongokit at `@classytic/mongokit/adapter`. Coordinated minimums:
|
|
352
|
+
|
|
353
|
+
| Package | Min |
|
|
354
|
+
|---|---|
|
|
355
|
+
| `@classytic/arc` | 2.12.0 |
|
|
356
|
+
| `@classytic/mongokit` | 3.13.0 |
|
|
357
|
+
| `@classytic/repo-core` | 0.4.0 |
|
|
358
|
+
|
|
359
|
+
Migration shape:
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
// arc 2.x
|
|
363
|
+
import { createMongooseAdapter } from '@classytic/arc';
|
|
364
|
+
import type { DataAdapter, RepositoryLike, AdapterRepositoryInput } from '@classytic/arc';
|
|
365
|
+
import type { InferMongooseDoc, MongooseAdapterOptions } from '@classytic/arc/adapters';
|
|
366
|
+
|
|
367
|
+
// arc 2.12+
|
|
368
|
+
import { createMongooseAdapter } from '@classytic/mongokit/adapter';
|
|
369
|
+
import type { DataAdapter, RepositoryLike, AdapterRepositoryInput } from '@classytic/repo-core/adapter';
|
|
370
|
+
import type { InferMongooseDoc, MongooseAdapterOptions } from '@classytic/mongokit/adapter';
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
The kit owns mongoose as its peer dep; arc dropped both `@classytic/mongokit` and `mongoose` from its `peerDependencies`. Hosts depend on `@classytic/mongokit` directly, which transitively pulls mongoose. Detection regex for stragglers: see `references/anti-patterns.md` §32g.
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## What mongokit does NOT do
|
|
378
|
+
|
|
379
|
+
Be explicit so the audit doesn't recommend it for the wrong job:
|
|
380
|
+
|
|
381
|
+
- **Connection management.** `mongoose.connect()` stays in your bootstrap.
|
|
382
|
+
- **Response transforms.** No `_id` → `id` rewriting, no field stripping. Use arc `fieldRules.hidden` or controller-level mapping.
|
|
383
|
+
- **Type generation.** No runtime codegen. Declare `interface UserDoc extends Document` manually (or use Mongoose's `InferSchemaType`).
|
|
384
|
+
- **Versioning.** Use `mongoOperationsPlugin` + `$inc` on `__v`, or external `mongoose-version`.
|
|
385
|
+
- **CLI scaffolding.** Mongokit ships no CLI — use `arc generate resource <name>` which creates a mongokit-style repo.
|
|
386
|
+
- **Vector/Atlas Search.** Mentioned in roadmap; not shipped in 3.12.x. Use `elasticSearchPlugin` for ES, or wire your own.
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Arc CLI & Scaffolding — what `arc init` and `arc generate` produce
|
|
2
|
+
|
|
3
|
+
When auditing a project, look for divergence between what `arc generate resource` would have created and what the team actually built. Hand-created files often:
|
|
4
|
+
- Skip the `*.repository.ts` layer entirely (Mongoose calls inline in routes).
|
|
5
|
+
- Misname files (`product-routes.ts`, `productController.ts` instead of `product.resource.ts`).
|
|
6
|
+
- Co-mingle resources in one file, killing tree-shake and `loadResources()` discovery.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## `arc init [name] [options]`
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
arc init my-api --mongokit --jwt --ts
|
|
14
|
+
arc init billing --custom --better-auth --multi
|
|
15
|
+
arc init edge-svc --mongokit --jwt --ts --edge
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Flag | Meaning | Default if omitted |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `--mongokit` | MongoDB + mongokit repository | (required choice) |
|
|
21
|
+
| `--custom` | Custom adapter — template points at the `RepositoryLike` contract from `@classytic/repo-core/adapter`; pick any kit (mongokit, sqlitekit, prismakit, custom) | (required choice) |
|
|
22
|
+
| `--jwt` | Arc JWT auth strategy | (required choice) |
|
|
23
|
+
| `--better-auth` | Better Auth integration | (required choice) |
|
|
24
|
+
| `--single` | Single-tenant template | `--single` |
|
|
25
|
+
| `--multi` | Multi-tenant template (org context) | — |
|
|
26
|
+
| `--ts` | TypeScript | `--ts` |
|
|
27
|
+
| `--js` | JavaScript | — |
|
|
28
|
+
| `--edge` | Serverless/edge runtime optimizations | — |
|
|
29
|
+
| `--skip-install` | Don't run `npm install` | — |
|
|
30
|
+
| `--force` | Overwrite existing dir | — |
|
|
31
|
+
|
|
32
|
+
**Output structure:**
|
|
33
|
+
```
|
|
34
|
+
my-api/
|
|
35
|
+
├── .arcrc # project config for arc generate
|
|
36
|
+
├── package.json # full deps + devDeps wired (npm install just works)
|
|
37
|
+
├── tsconfig.json
|
|
38
|
+
├── biome.json
|
|
39
|
+
├── .gitignore
|
|
40
|
+
├── .env.example
|
|
41
|
+
└── src/
|
|
42
|
+
├── app.ts # createApp() entry
|
|
43
|
+
├── server.ts # listen()
|
|
44
|
+
├── resources/ # empty, populated by `arc generate resource`
|
|
45
|
+
└── models/ # Mongoose schemas (--mongokit only)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The scaffold seeds full `dependencies` + `devDependencies` so `npm install` works without the CLI's pre-pass. Audit signal: a project missing `.arcrc` but using arc was likely *not* scaffolded — every file was hand-created and may diverge.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## `arc generate resource [name] [options]`
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
arc generate resource product
|
|
56
|
+
arc generate resource product --mcp --soft-delete
|
|
57
|
+
arc generate resource org-profile --bulk --tree
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Options:
|
|
61
|
+
- `--mcp` — emit `{name}.mcp.ts` for custom MCP tools alongside the resource
|
|
62
|
+
- `--tree` — wire `tree` preset (parent-child)
|
|
63
|
+
- `--soft-delete` — wire `softDelete` preset
|
|
64
|
+
- `--bulk` — wire `bulk` preset
|
|
65
|
+
|
|
66
|
+
**Generated files (mongokit template):**
|
|
67
|
+
```
|
|
68
|
+
src/resources/product/
|
|
69
|
+
├── product.model.ts # Mongoose schema (with required imports)
|
|
70
|
+
├── product.repository.ts # mongokit Repository or extends Repository<ProductDoc>
|
|
71
|
+
├── product.resource.ts # defineResource() with adapter wired
|
|
72
|
+
└── product.mcp.ts # (--mcp only) extraTools / bridge stubs
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Naming convention:**
|
|
76
|
+
- Input: kebab-case (`org-profile`).
|
|
77
|
+
- File names: `org-profile.model.ts`, `org-profile.repository.ts`, `org-profile.resource.ts`, `org-profile.mcp.ts`.
|
|
78
|
+
- Class names: PascalCase (`OrgProfile`, `OrgProfileRepository`).
|
|
79
|
+
- Variable names: camelCase (`orgProfile`, `orgProfileRepo`, `orgProfileResource`).
|
|
80
|
+
|
|
81
|
+
**Audit signal:** files like `productRoutes.ts`, `product-controller.ts`, `productHandlers.js`, `models/Product.js` (no resource file) → not scaffolded; team is fighting arc's conventions.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## `.arcrc` — project config
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"adapter": "mongokit",
|
|
90
|
+
"auth": "jwt",
|
|
91
|
+
"tenancy": "multi",
|
|
92
|
+
"language": "ts",
|
|
93
|
+
"mcp": true,
|
|
94
|
+
"templates": {
|
|
95
|
+
"resourceDir": "src/resources",
|
|
96
|
+
"modelDir": "src/models"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Fields:
|
|
102
|
+
- `adapter`: `'mongokit' | 'custom'` — controls which template `arc generate resource` uses.
|
|
103
|
+
- `auth`: `'jwt' | 'better-auth'` — auth example wiring in templates.
|
|
104
|
+
- `tenancy`: `'single' | 'multi'` — multi-tenant adds `multiTenant` preset and scope wiring by default.
|
|
105
|
+
- `language`: `'ts' | 'js'`.
|
|
106
|
+
- `mcp`: `true | false` — when true, `arc generate resource` always emits `.mcp.ts` (equivalent to `--mcp`).
|
|
107
|
+
- `templates.resourceDir` / `modelDir` — override default locations.
|
|
108
|
+
|
|
109
|
+
Audit: a stale `.arcrc` (e.g., points at deleted dirs) is a sign the team has drifted from CLI scaffolding. Either update or remove.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Other CLI commands
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
arc docs ./openapi.json --entry ./dist/index.js
|
|
117
|
+
```
|
|
118
|
+
Boots the app in introspect mode and emits OpenAPI 3.x to the output path. Wire into `prebuild` or a CI artifact step. Use this in audits to diff against any hand-maintained `swagger.yaml` — discrepancies are bugs in either side.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
arc introspect --entry ./dist/index.js
|
|
122
|
+
```
|
|
123
|
+
Lists every registered resource: name, route count, presets, permissions summary. Good first command when auditing an unfamiliar codebase.
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
arc describe product --entry ./dist/index.js
|
|
127
|
+
```
|
|
128
|
+
Detail one resource: routes, actions, permissions, field rules, presets, cache config, events. Shows what arc *thinks* the resource is — compare against hand-rolled equivalents.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
arc doctor
|
|
132
|
+
```
|
|
133
|
+
Diagnose env (Node version, TS version, peer dep versions). Run before audit to confirm environment is sane.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Resource file structure (canonical)
|
|
138
|
+
|
|
139
|
+
`src/resources/{name}/{name}.resource.ts`:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { defineResource, requireRoles, allowPublic, fields } from '@classytic/arc';
|
|
143
|
+
import { createMongooseAdapter } from '@classytic/mongokit/adapter'; // arc 2.12+
|
|
144
|
+
import { Repository, buildCrudSchemasFromModel } from '@classytic/mongokit';
|
|
145
|
+
import { Product } from './product.model.js';
|
|
146
|
+
|
|
147
|
+
const productRepo = new Repository(Product);
|
|
148
|
+
|
|
149
|
+
export const productResource = defineResource({
|
|
150
|
+
name: 'product',
|
|
151
|
+
displayName: 'Products',
|
|
152
|
+
module: 'catalog',
|
|
153
|
+
adapter: createMongooseAdapter({ model: Product, repository: productRepo, schemaGenerator: buildCrudSchemasFromModel }),
|
|
154
|
+
presets: ['softDelete'],
|
|
155
|
+
permissions: {
|
|
156
|
+
list: allowPublic(),
|
|
157
|
+
get: allowPublic(),
|
|
158
|
+
create: requireRoles(['admin']),
|
|
159
|
+
update: requireRoles(['admin']),
|
|
160
|
+
delete: requireRoles(['admin']),
|
|
161
|
+
},
|
|
162
|
+
schemaOptions: {
|
|
163
|
+
fieldRules: {
|
|
164
|
+
name: { type: 'string', minLength: 1, required: true },
|
|
165
|
+
price: { type: 'number', minimum: 0, required: true },
|
|
166
|
+
slug: { type: 'string', readonly: true },
|
|
167
|
+
deletedAt: { type: 'date', nullable: true, hidden: true },
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
cache: { staleTime: 30, gcTime: 300, tags: ['catalog'] },
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
export default productResource;
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`loadResources(import.meta.url)` discovers:
|
|
177
|
+
- `default` export (`export default productResource`)
|
|
178
|
+
- `export const resource`
|
|
179
|
+
- Any named export with `.toPlugin()`
|
|
180
|
+
|
|
181
|
+
Works with relative imports + Node `#` subpath imports — **NOT** tsconfig path aliases (`@/*`). See [anti-patterns.md §30](anti-patterns.md).
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Audit checks against scaffolding output
|
|
186
|
+
|
|
187
|
+
When you walk a client repo, compare against this baseline:
|
|
188
|
+
|
|
189
|
+
| Expected | Found | Implication |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| `.arcrc` at project root | missing | Team didn't scaffold — likely diverged conventions |
|
|
192
|
+
| `src/resources/{name}/{name}.resource.ts` | `src/routes/{name}.ts` | Hand-rolled — flag for §3 (manual CRUD) |
|
|
193
|
+
| `src/resources/{name}/{name}.repository.ts` extends `Repository` | `class FooRepository` with `Model.find` directly | Mongokit not adopted — flag for §28 |
|
|
194
|
+
| `src/app.ts` calls `createApp({ resources: [...] })` | manual `app.register(productPlugin)` per resource | Not using arc factory — possible boot-order issues |
|
|
195
|
+
| `src/resources/{name}/{name}.mcp.ts` (if `.arcrc` has `mcp: true`) | missing | MCP feature mentioned but not wired |
|
|
196
|
+
| `package.json` script `"docs:openapi": "arc docs ..."` | hand-maintained `openapi.yaml` | Out-of-band spec — flag for §6 |
|
|
197
|
+
| `npm run smoke` (CLI + subpath imports) | missing | No release gate — recommend wiring |
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Smoke commands worth running during audit
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
# Confirm arc + mongokit + sqlitekit + repo-core versions
|
|
205
|
+
npm ls @classytic/arc @classytic/mongokit @classytic/sqlitekit @classytic/repo-core
|
|
206
|
+
|
|
207
|
+
# List all defineResource calls
|
|
208
|
+
grep -rn "defineResource(" src/
|
|
209
|
+
|
|
210
|
+
# List all manual fastify routes
|
|
211
|
+
grep -rnE "fastify\\.(get|post|patch|put|delete)\\(" src/
|
|
212
|
+
|
|
213
|
+
# Find driver imports outside adapter dirs
|
|
214
|
+
grep -rln "from 'mongoose'\\|from '@prisma/client'\\|from 'drizzle-orm'" src/ \
|
|
215
|
+
| grep -v 'adapter\\|\\.model\\.'
|
|
216
|
+
|
|
217
|
+
# Check for hand-rolled toJSON
|
|
218
|
+
grep -rn "schema.set('toJSON'\\|toJSON = function" src/
|
|
219
|
+
|
|
220
|
+
# Count manual permission checks
|
|
221
|
+
grep -rcE "user\\.role|roles\\.includes|throw.*(Unauthorized|Forbidden)" src/
|
|
222
|
+
|
|
223
|
+
# Verify OpenAPI source of truth
|
|
224
|
+
ls -la openapi.* swagger.* api-spec.* 2>/dev/null
|
|
225
|
+
|
|
226
|
+
# Check arc CLI is wired
|
|
227
|
+
grep -E "arc (init|generate|docs)" package.json
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Use the counts to fill in the per-resource scorecard in the SKILL.md report template.
|