@classytic/arc 2.15.4 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/bin/arc.js +12 -0
- package/dist/{BaseController-dx3m2J8V.mjs → BaseController-DlCCTIxJ.mjs} +61 -19
- package/dist/{HookSystem-Iiebom92.mjs → HookSystem-Cmf7-Etp.mjs} +8 -4
- package/dist/{QueryCache-D41bfdBB.d.mts → QueryCache-SvmT_9ti.d.mts} +1 -1
- package/dist/{ResourceRegistry-CTERg_2x.mjs → ResourceRegistry-f48hFk3m.mjs} +52 -9
- package/dist/audit/index.d.mts +1 -1
- package/dist/audit/index.mjs +4 -2
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +4 -4
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi--M_i87dQ.mjs → betterAuthOpenApi-ClWxaceA.mjs} +10 -6
- package/dist/buildHandler-BZX6zzDM.mjs +300 -0
- package/dist/cache/index.d.mts +3 -3
- package/dist/cache/index.mjs +3 -3
- package/dist/{caching-SM8gghN6.mjs → caching-TeHE8G-v.mjs} +1 -1
- package/dist/cli/commands/describe.d.mts +35 -1
- package/dist/cli/commands/describe.mjs +52 -12
- package/dist/cli/commands/docs.d.mts +1 -4
- package/dist/cli/commands/docs.mjs +4 -16
- package/dist/cli/commands/generate.d.mts +2 -20
- package/dist/cli/commands/generate.mjs +1 -546
- package/dist/cli/commands/init.d.mts +2 -40
- package/dist/cli/commands/init.mjs +1 -3045
- package/dist/cli/commands/introspect.mjs +53 -64
- package/dist/cli/index.d.mts +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/{constants-Cxde4rpC.mjs → constants-TrJVIJl0.mjs} +7 -0
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -5
- package/dist/{core-CvmOqEms.mjs → core-DBJ_j6rX.mjs} +222 -44
- package/dist/createActionRouter-DUpN3Dd1.mjs +288 -0
- package/dist/{createAggregationRouter-B0bPDf5b.mjs → createAggregationRouter-Dq-TUCuY.mjs} +3 -2
- package/dist/{createApp-PFegs47-.mjs → createApp-DNccuhyI.mjs} +16 -14
- package/dist/{defineEvent-D5h7EvAx.mjs → defineEvent-DRwY0fYm.mjs} +1 -1
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/{errorHandler-Bk-AGhkU.mjs → errorHandler-DpoXQHZ9.mjs} +17 -14
- package/dist/errors-C1lX_jlm.d.mts +91 -0
- package/dist/{eventPlugin-CaKTYkYM.mjs → eventPlugin-C2cGqtRO.mjs} +1 -1
- package/dist/{eventPlugin-qXpqTebY.d.mts → eventPlugin-CtHC_av1.d.mts} +1 -1
- package/dist/events/index.d.mts +3 -3
- package/dist/events/index.mjs +5 -5
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +2 -2
- package/dist/{fields-COhcH3fk.d.mts → fields-Anj0xdih.d.mts} +1 -1
- package/dist/generate-BWFwgcCM.d.mts +38 -0
- package/dist/generate-CYac-OLv.mjs +654 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +2 -2
- package/dist/idempotency/index.mjs +1 -1
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-BTqLEvhu.d.mts → index-3oIimXQn.d.mts} +12 -12
- package/dist/{index-BstGxcc3.d.mts → index-B-ulKx5P.d.mts} +55 -4
- package/dist/{index-BswOSJCE.d.mts → index-CkW0flkU.d.mts} +355 -16
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +7 -8
- package/dist/init-Dv71MsJr.d.mts +71 -0
- package/dist/init-HDvoO9L5.mjs +3098 -0
- 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/jobs.mjs +3 -3
- package/dist/integrations/mcp/index.d.mts +239 -7
- package/dist/integrations/mcp/index.mjs +2 -528
- package/dist/integrations/mcp/testing.d.mts +2 -2
- package/dist/integrations/mcp/testing.mjs +6 -10
- package/dist/integrations/streamline.mjs +26 -1
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +1 -0
- package/dist/loadResourcesFromEntry-BLMEI2Xa.mjs +51 -0
- package/dist/{resourceToTools-tFYUNmM0.mjs → mcpPlugin-7vGV51ED.mjs} +1021 -318
- package/dist/{memory-UBydS5ku.mjs → memory-QOLe11D5.mjs} +2 -0
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/{openapi-BHXhoX8O.mjs → openapi-34T9yNwd.mjs} +47 -36
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +1 -1
- package/dist/{permissions-ohQyv50e.mjs → permissions-CTxMrreC.mjs} +2 -2
- package/dist/{pipe-Zr0KXjQe.mjs → pipe-DiCyvyPN.mjs} +1 -0
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +5 -5
- package/dist/plugins/index.mjs +10 -10
- package/dist/plugins/response-cache.mjs +5 -5
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{pluralize-DQgqgifU.mjs → pluralize-B9M8xvy-.mjs} +2 -1
- package/dist/presets/filesUpload.d.mts +4 -4
- package/dist/presets/filesUpload.mjs +2 -2
- 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 +4 -3
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-BbkjdPeH.mjs → presets-C9BE6WaZ.mjs} +2 -2
- package/dist/{queryCachePlugin-m1XsgAIJ.mjs → queryCachePlugin-B4XMSSe7.mjs} +2 -2
- package/dist/{queryCachePlugin-CqMdLI2-.d.mts → queryCachePlugin-Biqzfbi5.d.mts} +2 -2
- package/dist/{redis-DiMkdHEl.d.mts → redis-Cyzrz6SX.d.mts} +1 -1
- package/dist/{redis-stream-D6HzR1Z_.d.mts → redis-stream-DT-YjzrB.d.mts} +1 -1
- package/dist/registry/index.d.mts +319 -2
- package/dist/registry/index.mjs +3 -3
- package/dist/registry-BBE23CDj.mjs +576 -0
- package/dist/{routerShared-DrOa-26E.mjs → routerShared-CZV5aabX.mjs} +3 -3
- package/dist/scope/index.d.mts +3 -3
- package/dist/scope/index.mjs +3 -3
- package/dist/{sse-Bz-5ZeTt.mjs → sse-BY6sTy4P.mjs} +1 -1
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +16 -7
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +5 -5
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-C_s5moIu.mjs → types-Bi0r0vjG.mjs} +53 -1
- package/dist/{types-BQsjgQzS.d.mts → types-BsJMEQ4D.d.mts} +106 -12
- package/dist/{types-DrBaUwyV.d.mts → types-D-fYtKjb.d.mts} +33 -10
- package/dist/{types-CTYvcwHe.d.mts → types-DVfpSfx2.d.mts} +42 -1
- package/dist/utils/index.d.mts +1286 -2
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-_h9B3c57.mjs → utils-DC5ycPfr.mjs} +89 -40
- package/dist/{buildHandler-CcFOpJLh.mjs → validate-By96rH0r.mjs} +8 -299
- package/dist/{versioning-hmkPcDlX.d.mts → versioning-ZwX9tmbS.d.mts} +1 -1
- package/package.json +21 -28
- package/skills/arc/SKILL.md +300 -706
- package/skills/arc/references/auth.md +19 -7
- package/skills/arc-code-review/SKILL.md +1 -1
- package/skills/arc-code-review/references/arc-cheatsheet.md +100 -322
- package/dist/createActionRouter-S3MLVYot.mjs +0 -220
- package/dist/index-bRjYu21O.d.mts +0 -1320
- package/dist/org/index.d.mts +0 -66
- package/dist/org/index.mjs +0 -486
- package/dist/org/types.d.mts +0 -82
- package/dist/org/types.mjs +0 -1
- package/dist/registry-I-ogLgL9.mjs +0 -46
- /package/dist/{EventTransport-CT_52aWU.d.mts → EventTransport-C-2oAHtw.d.mts} +0 -0
- /package/dist/{EventTransport-DLWoUMHy.mjs → EventTransport-Hxvv5QQz.mjs} +0 -0
- /package/dist/{actionPermissions-CyUkQu6O.mjs → actionPermissions-Bjmvn7Eb.mjs} +0 -0
- /package/dist/{elevation-BXOWoGCF.d.mts → elevation-0YBpa663.d.mts} +0 -0
- /package/dist/{elevation-DgoeTyfX.mjs → elevation-Dci0AYLT.mjs} +0 -0
- /package/dist/{errorHandler-DFr45ZG4.d.mts → errorHandler-mHuyWzZE.d.mts} +0 -0
- /package/dist/{externalPaths-BD5nw6St.d.mts → externalPaths-DFg-2KTp.d.mts} +0 -0
- /package/dist/{interface-beEtJyWM.d.mts → interface-CH0OQudo.d.mts} +0 -0
- /package/dist/{interface-DfLGcus7.d.mts → interface-NwJ_qPlY.d.mts} +0 -0
- /package/dist/{keys-CGcCbNyu.mjs → keys-DopsCuyQ.mjs} +0 -0
- /package/dist/{loadResources-DBMQg_Aj.mjs → loadResources-ChQEj8ih.mjs} +0 -0
- /package/dist/{metrics-Qnvwc-LQ.mjs → metrics-TuOmguhi.mjs} +0 -0
- /package/dist/{replyHelpers-CK-FNO8E.mjs → replyHelpers-C-gD32oF.mjs} +0 -0
- /package/dist/{schemaIR-lYhC2gE5.mjs → schemaIR-Ctc89DSn.mjs} +0 -0
- /package/dist/{sessionManager-C4Le_UB3.d.mts → sessionManager-BqFegc0W.d.mts} +0 -0
- /package/dist/{storage-Dfzt4VTl.d.mts → storage-D2KZJAmn.d.mts} +0 -0
- /package/dist/{store-helpers-BkIN9-vu.mjs → store-helpers-B0sunfZZ.mjs} +0 -0
- /package/dist/{tracing-QJVprktp.d.mts → tracing-Dm8n7Cnn.d.mts} +0 -0
- /package/dist/{versioning-BUrT5aP4.mjs → versioning-B6mimogM.mjs} +0 -0
- /package/dist/{websocket-ChC2rqe1.d.mts → websocket-BkjeGZRn.d.mts} +0 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { t as pluralize } from "./pluralize-B9M8xvy-.mjs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
//#region src/cli/commands/generate/name-helpers.ts
|
|
5
|
+
/**
|
|
6
|
+
* Case conversion for resource-name normalisation.
|
|
7
|
+
*
|
|
8
|
+
* `arc generate r org-profile` accepts kebab-case names from the CLI;
|
|
9
|
+
* templates need PascalCase for class identifiers (`OrgProfileRepository`)
|
|
10
|
+
* and camelCase for variable / instance names (`orgProfileResource`).
|
|
11
|
+
* Centralising the converters here keeps every template emitting the
|
|
12
|
+
* same shape — no one-off `.charAt(0).toUpperCase()` chains scattered
|
|
13
|
+
* through template code.
|
|
14
|
+
*/
|
|
15
|
+
/** Convert kebab-case to PascalCase: `org-profile` → `OrgProfile`. */
|
|
16
|
+
function toPascalCase(name) {
|
|
17
|
+
return name.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
18
|
+
}
|
|
19
|
+
/** Convert PascalCase to camelCase: `OrgProfile` → `orgProfile`. */
|
|
20
|
+
function toCamelCase(pascalName) {
|
|
21
|
+
return pascalName.charAt(0).toLowerCase() + pascalName.slice(1);
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/cli/commands/generate/project-config.ts
|
|
25
|
+
/**
|
|
26
|
+
* Read the host project's `.arcrc` (written at scaffold time by `arc init`)
|
|
27
|
+
* and detect whether it's a TypeScript project via the presence of
|
|
28
|
+
* `tsconfig.json`. Both checks degrade gracefully — a missing `.arcrc`
|
|
29
|
+
* just means defaults, and the TS check uses the file marker rather than
|
|
30
|
+
* inspecting `package.json` because the marker is what `tsx` / `tsc` also
|
|
31
|
+
* key off.
|
|
32
|
+
*/
|
|
33
|
+
function readProjectConfig() {
|
|
34
|
+
try {
|
|
35
|
+
const rcPath = join(process.cwd(), ".arcrc");
|
|
36
|
+
return JSON.parse(readFileSync(rcPath, "utf-8"));
|
|
37
|
+
} catch {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isTypeScriptProject() {
|
|
42
|
+
return existsSync(join(process.cwd(), "tsconfig.json"));
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/cli/commands/generate/file-writer.ts
|
|
46
|
+
/**
|
|
47
|
+
* Scaffolding I/O for `arc generate`.
|
|
48
|
+
*
|
|
49
|
+
* Templates are pure (see `./templates.ts`); this module owns every
|
|
50
|
+
* disk-touching step:
|
|
51
|
+
*
|
|
52
|
+
* - `generateResource` writes the full resource folder
|
|
53
|
+
* (`<name>.model.ts` + `.repository.ts` + `.resource.ts` + optional
|
|
54
|
+
* `.mcp.ts`) plus the companion test file.
|
|
55
|
+
* - `generateFile` writes a single file (`controller` / `model` /
|
|
56
|
+
* `repository` / `schemas` / `mcp`).
|
|
57
|
+
*
|
|
58
|
+
* Both functions skip existing files with a clear console warning
|
|
59
|
+
* (idempotent re-runs are safe) and use `node:fs` sync APIs — the CLI
|
|
60
|
+
* is already synchronous in spirit and `Promise.all` parallelism on
|
|
61
|
+
* file writes is not worth the readability tax.
|
|
62
|
+
*/
|
|
63
|
+
async function generateResource(name, lowerName, resourcePath, templates, ext, includeMcp = false) {
|
|
64
|
+
console.log(`\nGenerating resource: ${name}...\n`);
|
|
65
|
+
if (!existsSync(resourcePath)) {
|
|
66
|
+
mkdirSync(resourcePath, { recursive: true });
|
|
67
|
+
console.log(` + Created: src/resources/${lowerName}/`);
|
|
68
|
+
}
|
|
69
|
+
const files = {
|
|
70
|
+
[`${lowerName}.model.${ext}`]: templates.model(name, lowerName),
|
|
71
|
+
[`${lowerName}.repository.${ext}`]: templates.repository(name, lowerName),
|
|
72
|
+
[`${lowerName}.resource.${ext}`]: templates.resource(name, lowerName)
|
|
73
|
+
};
|
|
74
|
+
if (includeMcp) files[`${lowerName}.mcp.${ext}`] = templates.mcp(name, lowerName);
|
|
75
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
76
|
+
const filepath = join(resourcePath, filename);
|
|
77
|
+
if (existsSync(filepath)) console.warn(` ! Skipped: ${filename} (already exists)`);
|
|
78
|
+
else {
|
|
79
|
+
writeFileSync(filepath, content);
|
|
80
|
+
console.log(` + Created: ${filename}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const testsDir = join(process.cwd(), "tests");
|
|
84
|
+
if (!existsSync(testsDir)) mkdirSync(testsDir, { recursive: true });
|
|
85
|
+
const testPath = join(testsDir, `${lowerName}.test.${ext}`);
|
|
86
|
+
if (!existsSync(testPath)) {
|
|
87
|
+
writeFileSync(testPath, templates.test(name, lowerName));
|
|
88
|
+
console.log(` + Created: tests/${lowerName}.test.${ext}`);
|
|
89
|
+
}
|
|
90
|
+
const camel = toCamelCase(name);
|
|
91
|
+
const isMultiTenant = readProjectConfig().tenant === "multi";
|
|
92
|
+
console.log(`
|
|
93
|
+
╔═══════════════════════════════════════════════════════════════╗
|
|
94
|
+
║ Resource Generated ║
|
|
95
|
+
╚═══════════════════════════════════════════════════════════════╝
|
|
96
|
+
|
|
97
|
+
Next steps:
|
|
98
|
+
|
|
99
|
+
1. Register in src/resources/index.${ext}:
|
|
100
|
+
import ${camel}Resource from './${lowerName}/${lowerName}.resource.js';
|
|
101
|
+
|
|
102
|
+
export const resources = [
|
|
103
|
+
// ... existing resources
|
|
104
|
+
${camel}Resource,
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
2. Customize the model schema in:
|
|
108
|
+
src/resources/${lowerName}/${lowerName}.model.${ext}
|
|
109
|
+
|
|
110
|
+
3. Adjust permissions in ${lowerName}.resource.${ext}:
|
|
111
|
+
${isMultiTenant ? ` - requireOrgMembership() → member of the current organization
|
|
112
|
+
- multiTenantPreset() → injects and filters organizationId
|
|
113
|
+
- requireRoles(['admin']) → admin writes inside the org scope` : ` - requireAuth() → any authenticated user
|
|
114
|
+
- requireRoles(['admin']) → specific platform roles`}
|
|
115
|
+
|
|
116
|
+
4. Run tests:
|
|
117
|
+
npm test
|
|
118
|
+
${includeMcp ? `
|
|
119
|
+
5. MCP tools file created: ${lowerName}.mcp.${ext}
|
|
120
|
+
Uncomment and customize the example tools.
|
|
121
|
+
Import and add to extraTools in your mcpPlugin config.
|
|
122
|
+
` : ""}`);
|
|
123
|
+
}
|
|
124
|
+
async function generateFile(name, lowerName, resourcePath, fileType, template, ext) {
|
|
125
|
+
console.log(`\nGenerating ${fileType}: ${name}...\n`);
|
|
126
|
+
if (!existsSync(resourcePath)) {
|
|
127
|
+
mkdirSync(resourcePath, { recursive: true });
|
|
128
|
+
console.log(` + Created: src/resources/${lowerName}/`);
|
|
129
|
+
}
|
|
130
|
+
const filename = `${lowerName}.${fileType}.${ext}`;
|
|
131
|
+
const filepath = join(resourcePath, filename);
|
|
132
|
+
if (existsSync(filepath)) throw new Error(`${filename} already exists. Remove it first or use a different name.`);
|
|
133
|
+
writeFileSync(filepath, template(name, lowerName));
|
|
134
|
+
console.log(` + Created: ${filename}`);
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/cli/commands/generate/templates.ts
|
|
138
|
+
/**
|
|
139
|
+
* Template factory for `arc generate`.
|
|
140
|
+
*
|
|
141
|
+
* `getTemplates(ts, config)` returns a record of per-file template
|
|
142
|
+
* functions. Each closure captures the language flag (`ts`) and project
|
|
143
|
+
* config (multi-tenant, adapter, etc.) so call sites only need to pass
|
|
144
|
+
* the resource-specific `name` / `fileName`. Templates remain pure —
|
|
145
|
+
* no I/O, so the scaffolder's file-writer is the only spot that touches
|
|
146
|
+
* disk.
|
|
147
|
+
*
|
|
148
|
+
* Templates included:
|
|
149
|
+
* - `model` Mongoose schema with optional tenant field + index
|
|
150
|
+
* - `repository` MongoKit-backed repo (or custom-adapter stub)
|
|
151
|
+
* - `controller` BaseController extension scaffold
|
|
152
|
+
* - `schemas` `buildCrudSchemasFromModel` with the field-rule menu
|
|
153
|
+
* - `resource` `defineResource` wiring — branches on multi-tenant
|
|
154
|
+
* - `mcp` Custom MCP tool examples (defineTool + zod schemas)
|
|
155
|
+
* - `test` Vitest spec using `createTestApp` + `expectArc`
|
|
156
|
+
*/
|
|
157
|
+
/**
|
|
158
|
+
* Template functions accept:
|
|
159
|
+
* - `name`: PascalCase class name (e.g. `OrgProfile`)
|
|
160
|
+
* - `fileName`: kebab-case for file paths (e.g. `org-profile`)
|
|
161
|
+
*/
|
|
162
|
+
function getTemplates(ts, config = {}) {
|
|
163
|
+
const isMultiTenant = config.tenant === "multi";
|
|
164
|
+
return {
|
|
165
|
+
model: (name, _fileName) => {
|
|
166
|
+
const camel = toCamelCase(name);
|
|
167
|
+
return `/**
|
|
168
|
+
* ${name} Model
|
|
169
|
+
* Generated by Arc CLI
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
${ts ? "import mongoose, { type HydratedDocument, type Model, type Types } from 'mongoose';" : "import mongoose from 'mongoose';"}
|
|
173
|
+
|
|
174
|
+
const { Schema } = mongoose;
|
|
175
|
+
${ts ? [
|
|
176
|
+
"",
|
|
177
|
+
"/**",
|
|
178
|
+
" * Persisted shape — what `.lean()` and `.toObject()` return. Carrying this",
|
|
179
|
+
` * through \`Model<I${name}>\` lets \`.select(...)\` / \`.find(...)\` / \`.lean()\``,
|
|
180
|
+
" * infer correctly so domain methods don't need `as` casts.",
|
|
181
|
+
" *",
|
|
182
|
+
" * Replace the placeholder fields with your real domain shape.",
|
|
183
|
+
" */",
|
|
184
|
+
`export interface I${name} {`,
|
|
185
|
+
" _id: Types.ObjectId;",
|
|
186
|
+
...isMultiTenant ? [" organizationId: Types.ObjectId;"] : [],
|
|
187
|
+
" // TODO: define your fields here",
|
|
188
|
+
" createdAt: Date;",
|
|
189
|
+
" updatedAt: Date;",
|
|
190
|
+
"}",
|
|
191
|
+
"",
|
|
192
|
+
`export type ${name}Document = HydratedDocument<I${name}>;`,
|
|
193
|
+
""
|
|
194
|
+
].join("\n") : ""}
|
|
195
|
+
const ${camel}Schema = new Schema${ts ? `<I${name}>` : ""}(
|
|
196
|
+
{
|
|
197
|
+
${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 },
|
|
198
|
+
},
|
|
199
|
+
{ timestamps: true }
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
${isMultiTenant ? `${camel}Schema.index({ organizationId: 1, createdAt: -1 });\n` : ""}${ts ? [
|
|
203
|
+
`const ${name}: Model<I${name}> =`,
|
|
204
|
+
` (mongoose.models.${name} as Model<I${name}> | undefined) ??`,
|
|
205
|
+
` mongoose.model<I${name}>('${name}', ${camel}Schema);`
|
|
206
|
+
].join("\n") : `const ${name} = mongoose.models.${name} || mongoose.model('${name}', ${camel}Schema);`}
|
|
207
|
+
|
|
208
|
+
export default ${name};
|
|
209
|
+
`;
|
|
210
|
+
},
|
|
211
|
+
repository: (name, fileName) => {
|
|
212
|
+
const camel = toCamelCase(name);
|
|
213
|
+
if (!(config.adapter === "mongokit" || !config.adapter)) {
|
|
214
|
+
const generic = ts ? `<I${name}>` : "";
|
|
215
|
+
return `/**
|
|
216
|
+
* ${name} Repository
|
|
217
|
+
* Generated by Arc CLI
|
|
218
|
+
*
|
|
219
|
+
* This project uses a custom adapter — wire your repository to whichever
|
|
220
|
+
* kit you're using (sqlitekit, prismakit, custom). Replace the body below
|
|
221
|
+
* with your kit's Repository constructor. Arc only requires the
|
|
222
|
+
* \`MinimalRepo\` floor (getAll/getById/create/update/delete) declared in
|
|
223
|
+
* \`@classytic/repo-core/repository\`.
|
|
224
|
+
*/
|
|
225
|
+
${ts ? `\nimport type { I${name} } from './${fileName}.model.js';` : ""}
|
|
226
|
+
|
|
227
|
+
// Replace with your kit's repository instance:
|
|
228
|
+
// import { Repository } from '@classytic/sqlitekit';
|
|
229
|
+
// const ${camel}Repository = new Repository${generic}(${name}Table);
|
|
230
|
+
// export default ${camel}Repository;
|
|
231
|
+
|
|
232
|
+
const ${camel}Repository = {
|
|
233
|
+
// TODO: implement MinimalRepo<${ts ? `I${name}` : "any"}>
|
|
234
|
+
} as never;
|
|
235
|
+
|
|
236
|
+
export default ${camel}Repository;
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
239
|
+
const generic = ts ? `<I${name}>` : "";
|
|
240
|
+
return `/**
|
|
241
|
+
* ${name} Repository
|
|
242
|
+
* Generated by Arc CLI
|
|
243
|
+
*/
|
|
244
|
+
|
|
245
|
+
import {
|
|
246
|
+
Repository,
|
|
247
|
+
methodRegistryPlugin,
|
|
248
|
+
softDeletePlugin,
|
|
249
|
+
} from '@classytic/mongokit';
|
|
250
|
+
import ${name} from './${fileName}.model.js';${ts ? `\nimport type { I${name} } from './${fileName}.model.js';` : ""}
|
|
251
|
+
|
|
252
|
+
class ${name}Repository extends Repository${generic} {
|
|
253
|
+
constructor() {
|
|
254
|
+
super(${name}, [methodRegistryPlugin(), softDeletePlugin()]);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Add domain methods here. arc + mongokit ship the standard CRUD
|
|
258
|
+
// (getAll/getById/create/update/delete) on the base class, so only
|
|
259
|
+
// write a method when there's real domain logic to capture.
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const ${camel}Repository = new ${name}Repository();
|
|
263
|
+
export default ${camel}Repository;
|
|
264
|
+
export { ${name}Repository };
|
|
265
|
+
`;
|
|
266
|
+
},
|
|
267
|
+
controller: (name, fileName) => {
|
|
268
|
+
const camel = toCamelCase(name);
|
|
269
|
+
return `/**
|
|
270
|
+
* ${name} Controller
|
|
271
|
+
* Generated by Arc CLI
|
|
272
|
+
*
|
|
273
|
+
* Note: defineResource() auto-creates a controller from the adapter.
|
|
274
|
+
* Only create a custom controller when you need custom methods.
|
|
275
|
+
*/
|
|
276
|
+
|
|
277
|
+
import { BaseController } from '@classytic/arc';
|
|
278
|
+
import ${camel}Repository from './${fileName}.repository.js';
|
|
279
|
+
|
|
280
|
+
class ${name}Controller extends BaseController {
|
|
281
|
+
constructor() {
|
|
282
|
+
super(${camel}Repository, {
|
|
283
|
+
resourceName: '${fileName}',
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Add custom controller methods here
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const ${camel}Controller = new ${name}Controller();
|
|
291
|
+
export default ${camel}Controller;
|
|
292
|
+
`;
|
|
293
|
+
},
|
|
294
|
+
schemas: (name, fileName) => `/**
|
|
295
|
+
* ${name} Schemas
|
|
296
|
+
* Generated by Arc CLI
|
|
297
|
+
*/
|
|
298
|
+
|
|
299
|
+
import ${name} from './${fileName}.model.js';
|
|
300
|
+
import { buildCrudSchemasFromModel } from '@classytic/mongokit';
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* CRUD Schemas with Field Rules
|
|
304
|
+
*/
|
|
305
|
+
const crudSchemas = buildCrudSchemasFromModel(${name}, {
|
|
306
|
+
strictAdditionalProperties: true,
|
|
307
|
+
fieldRules: {
|
|
308
|
+
// systemManaged: framework stamps it — strip from body + required[]
|
|
309
|
+
// deletedAt: { systemManaged: true },
|
|
310
|
+
// immutable: cannot be updated after creation
|
|
311
|
+
// slug: { immutable: true },
|
|
312
|
+
// immutableAfterCreate: alias for immutable
|
|
313
|
+
// organizationId: { immutableAfterCreate: true },
|
|
314
|
+
// optional: removed from required[] (properties kept)
|
|
315
|
+
// description: { optional: true },
|
|
316
|
+
// nullable: widen JSON-Schema type to accept null (Zod .nullable() rescue)
|
|
317
|
+
// priceMode: { nullable: true },
|
|
318
|
+
// preserveForElevated: elevated admins keep the field on ingest (cross-tenant writes)
|
|
319
|
+
// organizationId: { systemManaged: true, preserveForElevated: true },
|
|
320
|
+
},
|
|
321
|
+
query: {
|
|
322
|
+
// Add your filterable fields here. createdAt is the default so the
|
|
323
|
+
// generated routes accept ?createdAt[gte]=2026-01-01 with no extra
|
|
324
|
+
// wiring. Add domain fields below as your model grows.
|
|
325
|
+
filterableFields: {
|
|
326
|
+
createdAt: 'date',
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
export default crudSchemas;
|
|
332
|
+
`,
|
|
333
|
+
resource: (name, fileName) => {
|
|
334
|
+
const camel = toCamelCase(name);
|
|
335
|
+
const useMongoKit = config.adapter === "mongokit" || !config.adapter;
|
|
336
|
+
const schemaGeneratorImport = useMongoKit ? `import { buildCrudSchemasFromModel } from '@classytic/mongokit';\n` : "";
|
|
337
|
+
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` : "";
|
|
338
|
+
const adapterCall = useMongoKit ? `createMongooseAdapter({ model: ${name}, repository: ${camel}Repository, schemaGenerator: buildCrudSchemasFromModel })` : `createMongooseAdapter({ model: ${name}, repository: ${camel}Repository })`;
|
|
339
|
+
const queryParserConfig = useMongoKit ? `\n queryParser,` : "";
|
|
340
|
+
return isMultiTenant ? `/**
|
|
341
|
+
* ${name} Resource
|
|
342
|
+
* Generated by Arc CLI
|
|
343
|
+
*/
|
|
344
|
+
|
|
345
|
+
import { defineResource } from '@classytic/arc';
|
|
346
|
+
import { createMongooseAdapter } from '@classytic/mongokit/adapter';
|
|
347
|
+
import { allOf, requireOrgMembership, requireRoles } from '@classytic/arc/permissions';
|
|
348
|
+
import { multiTenantPreset } from '@classytic/arc/presets';
|
|
349
|
+
${schemaGeneratorImport}import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
|
|
350
|
+
import ${camel}Repository from './${fileName}.repository.js';${queryParserImport}
|
|
351
|
+
|
|
352
|
+
const ${camel}Resource = defineResource${ts ? `<I${name}>` : ""}({
|
|
353
|
+
name: '${fileName}',
|
|
354
|
+
adapter: ${adapterCall},${queryParserConfig}
|
|
355
|
+
|
|
356
|
+
// Multi-tenant default: scope reads/writes by \`organizationId\`. For
|
|
357
|
+
// company-wide tables (lookup data, platform settings) set
|
|
358
|
+
// \`tenantField: false\` instead — otherwise queries silently return
|
|
359
|
+
// nothing because the column doesn't exist.
|
|
360
|
+
//
|
|
361
|
+
// Multi-level tenancy (org + branch + project, etc.) — replace with:
|
|
362
|
+
// multiTenantPreset({ tenantFields: [
|
|
363
|
+
// { field: 'organizationId', type: 'org' },
|
|
364
|
+
// { field: 'branchId', contextKey: 'branchId' },
|
|
365
|
+
// ] })
|
|
366
|
+
presets: ['softDelete', multiTenantPreset({ tenantField: 'organizationId' })],
|
|
367
|
+
|
|
368
|
+
// Tenant-cleanup strategy. Runs when an organization is deleted —
|
|
369
|
+
// wire \`cascadeDeleteForOrganization(arc.registry, { organizationId })\`
|
|
370
|
+
// into your auth lifecycle (Better Auth's afterDeleteOrganization,
|
|
371
|
+
// billing webhook, etc.) and every flagged resource is cleaned in one
|
|
372
|
+
// pass. Strategies: 'hard' (delete), 'soft' (deletedAt), 'anonymize'
|
|
373
|
+
// (clear PII, keep row — for legal-retained ledgers), 'skip' (no-op
|
|
374
|
+
// with mandatory reason). See arc/docs/compliance/tenant-cleanup.md.
|
|
375
|
+
onTenantDelete: { strategy: { type: 'hard' } },
|
|
376
|
+
|
|
377
|
+
// 2.16 — declarative CRUD allow-list (positive form). Uncomment to
|
|
378
|
+
// restrict the surface; a future arc CRUD op won't silently leak in
|
|
379
|
+
// because every op is opt-in. Mutually exclusive with \`disabledRoutes\`.
|
|
380
|
+
// crud: { list: true, get: true },
|
|
381
|
+
|
|
382
|
+
permissions: {
|
|
383
|
+
list: requireOrgMembership(),
|
|
384
|
+
get: requireOrgMembership(),
|
|
385
|
+
create: allOf(requireOrgMembership(), requireRoles(['admin'])),
|
|
386
|
+
update: allOf(requireOrgMembership(), requireRoles(['admin'])),
|
|
387
|
+
delete: allOf(requireOrgMembership(), requireRoles(['admin'])),
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
// 2.16 — typed state-transition actions. \`defineAction()\` captures the
|
|
391
|
+
// schema's literal type so \`data\` is inferred — no \`as MyShape\` casts.
|
|
392
|
+
// Set \`id: false\` for resource-root actions (propose / search / bulk).
|
|
393
|
+
//
|
|
394
|
+
// import { defineAction } from '@classytic/arc';
|
|
395
|
+
// import { z } from 'zod';
|
|
396
|
+
//
|
|
397
|
+
// actions: {
|
|
398
|
+
// approve: defineAction({
|
|
399
|
+
// schema: z.object({ reviewedBy: z.string() }),
|
|
400
|
+
// permissions: requireRoles(['admin']),
|
|
401
|
+
// handler: async (id, data) => { /* data.reviewedBy is typed */ },
|
|
402
|
+
// }),
|
|
403
|
+
// },
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
export default ${camel}Resource;
|
|
407
|
+
` : `/**
|
|
408
|
+
* ${name} Resource
|
|
409
|
+
* Generated by Arc CLI
|
|
410
|
+
*/
|
|
411
|
+
|
|
412
|
+
import { defineResource } from '@classytic/arc';
|
|
413
|
+
import { createMongooseAdapter } from '@classytic/mongokit/adapter';
|
|
414
|
+
import { requireAuth, requireRoles } from '@classytic/arc/permissions';
|
|
415
|
+
${schemaGeneratorImport}import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
|
|
416
|
+
import ${camel}Repository from './${fileName}.repository.js';${queryParserImport}
|
|
417
|
+
|
|
418
|
+
const ${camel}Resource = defineResource${ts ? `<I${name}>` : ""}({
|
|
419
|
+
name: '${fileName}',
|
|
420
|
+
adapter: ${adapterCall},${queryParserConfig}
|
|
421
|
+
|
|
422
|
+
// Single-tenant default: arc auto-infers \`tenantField: false\` when
|
|
423
|
+
// the model has no \`organizationId\` path (silent-zero-results
|
|
424
|
+
// footgun closed in 2.12). Set \`tenantField: '<field>'\` to opt INTO
|
|
425
|
+
// tenant scoping, or \`tenantField: false\` to make it explicit.
|
|
426
|
+
presets: ['softDelete'],
|
|
427
|
+
|
|
428
|
+
// 2.16 — declarative CRUD allow-list (positive form). Uncomment to
|
|
429
|
+
// restrict the surface; a future arc CRUD op won't silently leak in
|
|
430
|
+
// because every op is opt-in. Mutually exclusive with \`disabledRoutes\`.
|
|
431
|
+
// crud: { list: true, get: true },
|
|
432
|
+
|
|
433
|
+
permissions: {
|
|
434
|
+
list: requireAuth(),
|
|
435
|
+
get: requireAuth(),
|
|
436
|
+
create: requireRoles(['admin']),
|
|
437
|
+
update: requireRoles(['admin']),
|
|
438
|
+
delete: requireRoles(['admin']),
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
// 2.16 — typed state-transition actions. \`defineAction()\` captures the
|
|
442
|
+
// schema's literal type so \`data\` is inferred. Set \`id: false\` for
|
|
443
|
+
// resource-root actions (propose / search / bulk).
|
|
444
|
+
//
|
|
445
|
+
// import { defineAction } from '@classytic/arc';
|
|
446
|
+
// import { z } from 'zod';
|
|
447
|
+
//
|
|
448
|
+
// actions: {
|
|
449
|
+
// publish: defineAction({
|
|
450
|
+
// schema: z.object({ scheduledFor: z.string().datetime().optional() }),
|
|
451
|
+
// permissions: requireRoles(['admin']),
|
|
452
|
+
// handler: async (id, data) => { /* data.scheduledFor is typed */ },
|
|
453
|
+
// }),
|
|
454
|
+
// },
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
export default ${camel}Resource;
|
|
458
|
+
`;
|
|
459
|
+
},
|
|
460
|
+
mcp: (name, fileName) => {
|
|
461
|
+
const camel = toCamelCase(name);
|
|
462
|
+
return `/**
|
|
463
|
+
* ${name} MCP Tools
|
|
464
|
+
* Generated by Arc CLI
|
|
465
|
+
*
|
|
466
|
+
* Custom MCP tools for the ${name} domain.
|
|
467
|
+
* CRUD tools (list/get/create/update/delete) are auto-generated by mcpPlugin.
|
|
468
|
+
* Add domain-specific tools here (actions, analytics, workflows).
|
|
469
|
+
*/
|
|
470
|
+
|
|
471
|
+
import { defineTool } from '@classytic/arc/mcp';
|
|
472
|
+
${ts ? "import { z } from 'zod';\n" : "const { z } = require('zod');\n"}
|
|
473
|
+
// Example: domain-specific action tool
|
|
474
|
+
// export const activate${name}Tool = defineTool('activate_${fileName}', {
|
|
475
|
+
// description: 'Activate a ${name.toLowerCase()} by ID',
|
|
476
|
+
// input: { id: z.string().describe('${name} ID') },
|
|
477
|
+
// annotations: { destructiveHint: true, idempotentHint: true },
|
|
478
|
+
// handler: async ({ id }, ctx) => {
|
|
479
|
+
// // Your logic here
|
|
480
|
+
// return { content: [{ type: 'text', text: \`Activated ${name.toLowerCase()} \${id}\` }] };
|
|
481
|
+
// },
|
|
482
|
+
// });
|
|
483
|
+
|
|
484
|
+
// Example: read-only analytics tool
|
|
485
|
+
// export const ${camel}StatsTool = defineTool('${fileName}_stats', {
|
|
486
|
+
// description: 'Get ${name.toLowerCase()} statistics',
|
|
487
|
+
// input: { period: z.enum(['7d', '30d', '90d']).optional() },
|
|
488
|
+
// annotations: { readOnlyHint: true },
|
|
489
|
+
// handler: async ({ period }) => {
|
|
490
|
+
// // Your logic here
|
|
491
|
+
// return { content: [{ type: 'text', text: JSON.stringify({ total: 0, period }) }] };
|
|
492
|
+
// },
|
|
493
|
+
// });
|
|
494
|
+
`;
|
|
495
|
+
},
|
|
496
|
+
test: (name, fileName) => {
|
|
497
|
+
const camel = toCamelCase(name);
|
|
498
|
+
return `/**
|
|
499
|
+
* ${name} Tests
|
|
500
|
+
* Generated by Arc CLI
|
|
501
|
+
*
|
|
502
|
+
* Testing surface (arc 2.12+):
|
|
503
|
+
* - createTestApp turnkey Fastify + in-memory Mongo + auth + fixtures
|
|
504
|
+
* - expectArc fluent matchers — .ok / .failed / .unauthorized /
|
|
505
|
+
* .forbidden / .notFound / .conflict / .validationError /
|
|
506
|
+
* .paginated / .hidesField / .hasData / .hasStatus
|
|
507
|
+
* - ctx.auth unified TestAuthProvider — register a role, reuse .headers
|
|
508
|
+
*
|
|
509
|
+
* Wire shape (post-2.12): single-doc responses are flat (\`{_id, name, ...}\`),
|
|
510
|
+
* paginated responses are \`{ method: 'offset', data: [...], page, ... }\`.
|
|
511
|
+
* No \`success\` envelope — HTTP status discriminates success vs error;
|
|
512
|
+
* errors carry the canonical ErrorContract \`{ code, message, status }\`.
|
|
513
|
+
*/
|
|
514
|
+
|
|
515
|
+
import { describe, it, beforeAll, afterAll, expect } from 'vitest';
|
|
516
|
+
import { createTestApp, expectArc } from '@classytic/arc/testing';
|
|
517
|
+
import type { TestAppContext } from '@classytic/arc/testing';
|
|
518
|
+
import ${camel}Resource from '../src/resources/${fileName}/${fileName}.resource.js';
|
|
519
|
+
|
|
520
|
+
describe('${name} Resource', () => {
|
|
521
|
+
let ctx${ts ? ": TestAppContext" : ""};
|
|
522
|
+
|
|
523
|
+
beforeAll(async () => {
|
|
524
|
+
ctx = await createTestApp({
|
|
525
|
+
resources: [${camel}Resource],
|
|
526
|
+
authMode: 'jwt',
|
|
527
|
+
connectMongoose: true,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
ctx.auth${ts ? "!" : ""}.register('admin', {
|
|
531
|
+
user: { id: '1', roles: ['admin'] },
|
|
532
|
+
orgId: 'org-1',
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
afterAll(() => ctx.close());
|
|
537
|
+
|
|
538
|
+
describe('GET /${pluralize(fileName)}', () => {
|
|
539
|
+
it('returns a paginated list', async () => {
|
|
540
|
+
const res = await ctx.app.inject({
|
|
541
|
+
method: 'GET',
|
|
542
|
+
url: '/${pluralize(fileName)}',
|
|
543
|
+
headers: ctx.auth${ts ? "!" : ""}.as('admin').headers,
|
|
544
|
+
});
|
|
545
|
+
expectArc(res).ok().paginated();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('rejects unauthenticated requests', async () => {
|
|
549
|
+
const res = await ctx.app.inject({ method: 'GET', url: '/${pluralize(fileName)}' });
|
|
550
|
+
expectArc(res).unauthorized();
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
describe('POST /${pluralize(fileName)}', () => {
|
|
555
|
+
it('creates a record (flat wire shape — no envelope)', async () => {
|
|
556
|
+
const res = await ctx.app.inject({
|
|
557
|
+
method: 'POST',
|
|
558
|
+
url: '/${pluralize(fileName)}',
|
|
559
|
+
headers: ctx.auth${ts ? "!" : ""}.as('admin').headers,
|
|
560
|
+
payload: { name: 'Example' },
|
|
561
|
+
});
|
|
562
|
+
expectArc(res).ok(201);
|
|
563
|
+
// Single-doc response is flat: \`body._id\` (not \`body.data._id\`).
|
|
564
|
+
const body = res.json()${ts ? " as { _id: string; name: string }" : ""};
|
|
565
|
+
expect(body._id).toBeDefined();
|
|
566
|
+
expect(body.name).toBe('Example');
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// More patterns to extend:
|
|
571
|
+
// - expectArc(res).forbidden() / .notFound() / .validationError()
|
|
572
|
+
// - expectArc(res).hidesField('password') for field-level perms
|
|
573
|
+
// - ctx.fixtures.create('${fileName}', {...}) for seeded data
|
|
574
|
+
// - error wire shape: body.code === 'arc.not_found' (ErrorContract)
|
|
575
|
+
});
|
|
576
|
+
`;
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
//#endregion
|
|
581
|
+
//#region src/cli/commands/generate/index.ts
|
|
582
|
+
/**
|
|
583
|
+
* `arc generate` orchestrator.
|
|
584
|
+
*
|
|
585
|
+
* Routes the user-supplied type (`resource` / `controller` / `model` /
|
|
586
|
+
* `repository` / `schemas` / `mcp`, plus their short aliases) into the
|
|
587
|
+
* matching file-writer call. The orchestrator owns:
|
|
588
|
+
*
|
|
589
|
+
* - Name normalisation (kebab-case → PascalCase, kebab stays as the
|
|
590
|
+
* filename root).
|
|
591
|
+
* - The Better Auth-owned collection guard — refusing to generate a
|
|
592
|
+
* model named `user` / `organization` / etc. since BA writes those
|
|
593
|
+
* itself and a duplicate Mongoose registration silently collides.
|
|
594
|
+
* - Project-config detection (`.arcrc` + `tsconfig.json`) to pick the
|
|
595
|
+
* right template set + extension.
|
|
596
|
+
*
|
|
597
|
+
* Every step downstream is pure (templates) or local I/O (file-writer);
|
|
598
|
+
* no network, no DB, no global state.
|
|
599
|
+
*/
|
|
600
|
+
async function generate(type, args) {
|
|
601
|
+
if (!type) throw new Error("Missing type argument\nUsage: arc generate <resource|controller|model|repository|schemas> <name>");
|
|
602
|
+
const [name, ...restArgs] = args;
|
|
603
|
+
if (!name) throw new Error("Missing name argument\nUsage: arc generate <type> <name>\nExample: arc generate resource product");
|
|
604
|
+
const capitalizedName = toPascalCase(name);
|
|
605
|
+
const lowerName = name.toLowerCase();
|
|
606
|
+
if ((type === "resource" || type === "r" || type === "model" || type === "m") && new Set([
|
|
607
|
+
"user",
|
|
608
|
+
"session",
|
|
609
|
+
"account",
|
|
610
|
+
"verification",
|
|
611
|
+
"organization",
|
|
612
|
+
"member",
|
|
613
|
+
"invitation",
|
|
614
|
+
"team",
|
|
615
|
+
"team-member",
|
|
616
|
+
"apikey"
|
|
617
|
+
]).has(lowerName)) {
|
|
618
|
+
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`);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const projectConfig = readProjectConfig();
|
|
622
|
+
const ts = projectConfig.typescript ?? isTypeScriptProject();
|
|
623
|
+
const ext = ts ? "ts" : "js";
|
|
624
|
+
const templates = getTemplates(ts, projectConfig);
|
|
625
|
+
const resourcePath = join(process.cwd(), "src", "resources", lowerName);
|
|
626
|
+
switch (type) {
|
|
627
|
+
case "resource":
|
|
628
|
+
case "r":
|
|
629
|
+
await generateResource(capitalizedName, lowerName, resourcePath, templates, ext, projectConfig.mcp === true || restArgs.includes("--mcp"));
|
|
630
|
+
break;
|
|
631
|
+
case "controller":
|
|
632
|
+
case "c":
|
|
633
|
+
await generateFile(capitalizedName, lowerName, resourcePath, "controller", templates.controller, ext);
|
|
634
|
+
break;
|
|
635
|
+
case "model":
|
|
636
|
+
case "m":
|
|
637
|
+
await generateFile(capitalizedName, lowerName, resourcePath, "model", templates.model, ext);
|
|
638
|
+
break;
|
|
639
|
+
case "repository":
|
|
640
|
+
case "repo":
|
|
641
|
+
await generateFile(capitalizedName, lowerName, resourcePath, "repository", templates.repository, ext);
|
|
642
|
+
break;
|
|
643
|
+
case "schemas":
|
|
644
|
+
case "s":
|
|
645
|
+
await generateFile(capitalizedName, lowerName, resourcePath, "schemas", templates.schemas, ext);
|
|
646
|
+
break;
|
|
647
|
+
case "mcp":
|
|
648
|
+
await generateFile(capitalizedName, lowerName, resourcePath, "mcp", templates.mcp, ext);
|
|
649
|
+
break;
|
|
650
|
+
default: throw new Error(`Unknown type: ${type}\nAvailable types: resource, controller, model, repository, schemas, mcp`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
//#endregion
|
|
654
|
+
export { generate as t };
|
package/dist/hooks/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { An as
|
|
1
|
+
import { An as HookSystem, Dn as HookOperation, En as HookHandler, Fn as beforeCreate, In as beforeDelete, Ln as beforeUpdate, Mn as afterCreate, Nn as afterDelete, On as HookPhase, Pn as afterUpdate, Rn as createHookSystem, Tn as HookContext, jn as HookSystemOptions, kn as HookRegistration, wn as DefineHookOptions, zn as defineHook } from "../index-CkW0flkU.mjs";
|
|
2
2
|
export { type DefineHookOptions, type HookContext, type HookHandler, type HookOperation, type HookPhase, type HookRegistration, HookSystem, type HookSystemOptions, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
|
package/dist/hooks/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as beforeCreate, c as createHookSystem, i as afterUpdate, l as defineHook, n as afterCreate, o as beforeDelete, r as afterDelete, s as beforeUpdate, t as HookSystem } from "../HookSystem-
|
|
1
|
+
import { a as beforeCreate, c as createHookSystem, i as afterUpdate, l as defineHook, n as afterCreate, o as beforeDelete, r as afterDelete, s as beforeUpdate, t as HookSystem } from "../HookSystem-Cmf7-Etp.mjs";
|
|
2
2
|
export { HookSystem, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as createIdempotencyResult, n as IdempotencyResult, r as IdempotencyStore, t as IdempotencyLock } from "../interface-
|
|
2
|
-
import { i as RedisIdempotencyStoreOptions, n as RedisClient } from "../redis-
|
|
1
|
+
import { i as createIdempotencyResult, n as IdempotencyResult, r as IdempotencyStore, t as IdempotencyLock } from "../interface-NwJ_qPlY.mjs";
|
|
2
|
+
import { i as RedisIdempotencyStoreOptions, n as RedisClient } from "../redis-Cyzrz6SX.mjs";
|
|
3
3
|
import { FastifyPluginAsync } from "fastify";
|
|
4
4
|
import { RepositoryLike } from "@classytic/repo-core/adapter";
|
|
5
5
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-
|
|
1
|
+
import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-B0sunfZZ.mjs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import fp from "fastify-plugin";
|
|
4
4
|
import { and, eq, exists, gt, lt, or, startsWith } from "@classytic/repo-core/filter";
|