@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
|
@@ -1,547 +1,2 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
-
//#region src/cli/commands/generate.ts
|
|
5
|
-
/**
|
|
6
|
-
* Arc CLI - Generate Command
|
|
7
|
-
*
|
|
8
|
-
* Scaffolds resources with consistent naming:
|
|
9
|
-
* - src/resources/product/product.model.ts
|
|
10
|
-
* - src/resources/product/product.repository.ts
|
|
11
|
-
* - src/resources/product/product.resource.ts
|
|
12
|
-
*
|
|
13
|
-
* Handles kebab-case names: `arc g r org-profile` generates:
|
|
14
|
-
* - Class names: OrgProfile, OrgProfileRepository
|
|
15
|
-
* - Variable names: orgProfileSchema, orgProfileRepository
|
|
16
|
-
* - File names: org-profile.model.ts, org-profile.repository.ts
|
|
17
|
-
*/
|
|
18
|
-
function readProjectConfig() {
|
|
19
|
-
try {
|
|
20
|
-
const rcPath = join(process.cwd(), ".arcrc");
|
|
21
|
-
return JSON.parse(readFileSync(rcPath, "utf-8"));
|
|
22
|
-
} catch {
|
|
23
|
-
return {};
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
function isTypeScriptProject() {
|
|
27
|
-
return existsSync(join(process.cwd(), "tsconfig.json"));
|
|
28
|
-
}
|
|
29
|
-
/** Convert kebab-case to PascalCase: org-profile → OrgProfile */
|
|
30
|
-
function toPascalCase(name) {
|
|
31
|
-
return name.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
32
|
-
}
|
|
33
|
-
/** Convert PascalCase to camelCase: OrgProfile → orgProfile */
|
|
34
|
-
function toCamelCase(pascalName) {
|
|
35
|
-
return pascalName.charAt(0).toLowerCase() + pascalName.slice(1);
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Template functions accept:
|
|
39
|
-
* - name: PascalCase class name (e.g., OrgProfile)
|
|
40
|
-
* - fileName: kebab-case for file paths (e.g., org-profile)
|
|
41
|
-
*/
|
|
42
|
-
function getTemplates(ts, config = {}) {
|
|
43
|
-
const isMultiTenant = config.tenant === "multi";
|
|
44
|
-
return {
|
|
45
|
-
model: (name, _fileName) => {
|
|
46
|
-
const camel = toCamelCase(name);
|
|
47
|
-
return `/**
|
|
48
|
-
* ${name} Model
|
|
49
|
-
* Generated by Arc CLI
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
|
-
${ts ? "import mongoose, { type HydratedDocument, type Model, type Types } from 'mongoose';" : "import mongoose from 'mongoose';"}
|
|
53
|
-
|
|
54
|
-
const { Schema } = mongoose;
|
|
55
|
-
${ts ? [
|
|
56
|
-
"",
|
|
57
|
-
"/**",
|
|
58
|
-
" * Persisted shape — what `.lean()` and `.toObject()` return. Carrying this",
|
|
59
|
-
` * through \`Model<I${name}>\` lets \`.select(...)\` / \`.find(...)\` / \`.lean()\``,
|
|
60
|
-
" * infer correctly so domain methods don't need `as` casts.",
|
|
61
|
-
" *",
|
|
62
|
-
" * Replace the placeholder fields with your real domain shape.",
|
|
63
|
-
" */",
|
|
64
|
-
`export interface I${name} {`,
|
|
65
|
-
" _id: Types.ObjectId;",
|
|
66
|
-
...isMultiTenant ? [" organizationId: Types.ObjectId;"] : [],
|
|
67
|
-
" // TODO: define your fields here",
|
|
68
|
-
" createdAt: Date;",
|
|
69
|
-
" updatedAt: Date;",
|
|
70
|
-
"}",
|
|
71
|
-
"",
|
|
72
|
-
`export type ${name}Document = HydratedDocument<I${name}>;`,
|
|
73
|
-
""
|
|
74
|
-
].join("\n") : ""}
|
|
75
|
-
const ${camel}Schema = new Schema${ts ? `<I${name}>` : ""}(
|
|
76
|
-
{
|
|
77
|
-
${isMultiTenant ? " organizationId: { type: Schema.Types.ObjectId, required: true, index: true },\n" : ""} // TODO: declare your fields here, e.g. name: { type: String, required: true, trim: true },
|
|
78
|
-
},
|
|
79
|
-
{ timestamps: true }
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
${isMultiTenant ? `${camel}Schema.index({ organizationId: 1, createdAt: -1 });\n` : ""}${ts ? [
|
|
83
|
-
`const ${name}: Model<I${name}> =`,
|
|
84
|
-
` (mongoose.models.${name} as Model<I${name}> | undefined) ??`,
|
|
85
|
-
` mongoose.model<I${name}>('${name}', ${camel}Schema);`
|
|
86
|
-
].join("\n") : `const ${name} = mongoose.models.${name} || mongoose.model('${name}', ${camel}Schema);`}
|
|
87
|
-
|
|
88
|
-
export default ${name};
|
|
89
|
-
`;
|
|
90
|
-
},
|
|
91
|
-
repository: (name, fileName) => {
|
|
92
|
-
const camel = toCamelCase(name);
|
|
93
|
-
if (!(config.adapter === "mongokit" || !config.adapter)) {
|
|
94
|
-
const generic = ts ? `<I${name}>` : "";
|
|
95
|
-
return `/**
|
|
96
|
-
* ${name} Repository
|
|
97
|
-
* Generated by Arc CLI
|
|
98
|
-
*
|
|
99
|
-
* This project uses a custom adapter — wire your repository to whichever
|
|
100
|
-
* kit you're using (sqlitekit, prismakit, custom). Replace the body below
|
|
101
|
-
* with your kit's Repository constructor. Arc only requires the
|
|
102
|
-
* \`MinimalRepo\` floor (getAll/getById/create/update/delete) declared in
|
|
103
|
-
* \`@classytic/repo-core/repository\`.
|
|
104
|
-
*/
|
|
105
|
-
${ts ? `\nimport type { I${name} } from './${fileName}.model.js';` : ""}
|
|
106
|
-
|
|
107
|
-
// Replace with your kit's repository instance:
|
|
108
|
-
// import { Repository } from '@classytic/sqlitekit';
|
|
109
|
-
// const ${camel}Repository = new Repository${generic}(${name}Table);
|
|
110
|
-
// export default ${camel}Repository;
|
|
111
|
-
|
|
112
|
-
const ${camel}Repository = {
|
|
113
|
-
// TODO: implement MinimalRepo<${ts ? `I${name}` : "any"}>
|
|
114
|
-
} as never;
|
|
115
|
-
|
|
116
|
-
export default ${camel}Repository;
|
|
117
|
-
`;
|
|
118
|
-
}
|
|
119
|
-
const generic = ts ? `<I${name}>` : "";
|
|
120
|
-
return `/**
|
|
121
|
-
* ${name} Repository
|
|
122
|
-
* Generated by Arc CLI
|
|
123
|
-
*/
|
|
124
|
-
|
|
125
|
-
import {
|
|
126
|
-
Repository,
|
|
127
|
-
methodRegistryPlugin,
|
|
128
|
-
softDeletePlugin,
|
|
129
|
-
} from '@classytic/mongokit';
|
|
130
|
-
import ${name} from './${fileName}.model.js';${ts ? `\nimport type { I${name} } from './${fileName}.model.js';` : ""}
|
|
131
|
-
|
|
132
|
-
class ${name}Repository extends Repository${generic} {
|
|
133
|
-
constructor() {
|
|
134
|
-
super(${name}, [methodRegistryPlugin(), softDeletePlugin()]);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Add domain methods here. arc + mongokit ship the standard CRUD
|
|
138
|
-
// (getAll/getById/create/update/delete) on the base class, so only
|
|
139
|
-
// write a method when there's real domain logic to capture.
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const ${camel}Repository = new ${name}Repository();
|
|
143
|
-
export default ${camel}Repository;
|
|
144
|
-
export { ${name}Repository };
|
|
145
|
-
`;
|
|
146
|
-
},
|
|
147
|
-
controller: (name, fileName) => {
|
|
148
|
-
const camel = toCamelCase(name);
|
|
149
|
-
return `/**
|
|
150
|
-
* ${name} Controller
|
|
151
|
-
* Generated by Arc CLI
|
|
152
|
-
*
|
|
153
|
-
* Note: defineResource() auto-creates a controller from the adapter.
|
|
154
|
-
* Only create a custom controller when you need custom methods.
|
|
155
|
-
*/
|
|
156
|
-
|
|
157
|
-
import { BaseController } from '@classytic/arc';
|
|
158
|
-
import ${camel}Repository from './${fileName}.repository.js';
|
|
159
|
-
|
|
160
|
-
class ${name}Controller extends BaseController {
|
|
161
|
-
constructor() {
|
|
162
|
-
super(${camel}Repository, {
|
|
163
|
-
resourceName: '${fileName}',
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Add custom controller methods here
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const ${camel}Controller = new ${name}Controller();
|
|
171
|
-
export default ${camel}Controller;
|
|
172
|
-
`;
|
|
173
|
-
},
|
|
174
|
-
schemas: (name, fileName) => `/**
|
|
175
|
-
* ${name} Schemas
|
|
176
|
-
* Generated by Arc CLI
|
|
177
|
-
*/
|
|
178
|
-
|
|
179
|
-
import ${name} from './${fileName}.model.js';
|
|
180
|
-
import { buildCrudSchemasFromModel } from '@classytic/mongokit';
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* CRUD Schemas with Field Rules
|
|
184
|
-
*/
|
|
185
|
-
const crudSchemas = buildCrudSchemasFromModel(${name}, {
|
|
186
|
-
strictAdditionalProperties: true,
|
|
187
|
-
fieldRules: {
|
|
188
|
-
// systemManaged: framework stamps it — strip from body + required[]
|
|
189
|
-
// deletedAt: { systemManaged: true },
|
|
190
|
-
// immutable: cannot be updated after creation
|
|
191
|
-
// slug: { immutable: true },
|
|
192
|
-
// immutableAfterCreate: alias for immutable
|
|
193
|
-
// organizationId: { immutableAfterCreate: true },
|
|
194
|
-
// optional: removed from required[] (properties kept)
|
|
195
|
-
// description: { optional: true },
|
|
196
|
-
// nullable: widen JSON-Schema type to accept null (Zod .nullable() rescue)
|
|
197
|
-
// priceMode: { nullable: true },
|
|
198
|
-
// preserveForElevated: elevated admins keep the field on ingest (cross-tenant writes)
|
|
199
|
-
// organizationId: { systemManaged: true, preserveForElevated: true },
|
|
200
|
-
},
|
|
201
|
-
query: {
|
|
202
|
-
// Add your filterable fields here. createdAt is the default so the
|
|
203
|
-
// generated routes accept ?createdAt[gte]=2026-01-01 with no extra
|
|
204
|
-
// wiring. Add domain fields below as your model grows.
|
|
205
|
-
filterableFields: {
|
|
206
|
-
createdAt: 'date',
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
export default crudSchemas;
|
|
212
|
-
`,
|
|
213
|
-
resource: (name, fileName) => {
|
|
214
|
-
const camel = toCamelCase(name);
|
|
215
|
-
const useMongoKit = config.adapter === "mongokit" || !config.adapter;
|
|
216
|
-
const schemaGeneratorImport = useMongoKit ? `import { buildCrudSchemasFromModel } from '@classytic/mongokit';\n` : "";
|
|
217
|
-
const queryParserImport = useMongoKit ? `\nimport { QueryParser } from '@classytic/mongokit';\n\nconst queryParser = new QueryParser({\n // Whitelist the fields this resource accepts in URL filters.\n // Empty by default — only \`createdAt\` is implicit; add yours.\n allowedFilterFields: [],\n});\n` : "";
|
|
218
|
-
const adapterCall = useMongoKit ? `createMongooseAdapter({ model: ${name}, repository: ${camel}Repository, schemaGenerator: buildCrudSchemasFromModel })` : `createMongooseAdapter({ model: ${name}, repository: ${camel}Repository })`;
|
|
219
|
-
const queryParserConfig = useMongoKit ? `\n queryParser,` : "";
|
|
220
|
-
return isMultiTenant ? `/**
|
|
221
|
-
* ${name} Resource
|
|
222
|
-
* Generated by Arc CLI
|
|
223
|
-
*/
|
|
224
|
-
|
|
225
|
-
import { defineResource } from '@classytic/arc';
|
|
226
|
-
import { createMongooseAdapter } from '@classytic/mongokit/adapter';
|
|
227
|
-
import { allOf, requireOrgMembership, requireRoles } from '@classytic/arc/permissions';
|
|
228
|
-
import { multiTenantPreset } from '@classytic/arc/presets';
|
|
229
|
-
${schemaGeneratorImport}import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
|
|
230
|
-
import ${camel}Repository from './${fileName}.repository.js';${queryParserImport}
|
|
231
|
-
|
|
232
|
-
const ${camel}Resource = defineResource${ts ? `<I${name}>` : ""}({
|
|
233
|
-
name: '${fileName}',
|
|
234
|
-
adapter: ${adapterCall},${queryParserConfig}
|
|
235
|
-
|
|
236
|
-
// Multi-tenant default: scope reads/writes by \`organizationId\`. For
|
|
237
|
-
// company-wide tables (lookup data, platform settings) set
|
|
238
|
-
// \`tenantField: false\` instead — otherwise queries silently return
|
|
239
|
-
// nothing because the column doesn't exist.
|
|
240
|
-
//
|
|
241
|
-
// Multi-level tenancy (org + branch + project, etc.) — replace with:
|
|
242
|
-
// multiTenantPreset({ tenantFields: [
|
|
243
|
-
// { field: 'organizationId', type: 'org' },
|
|
244
|
-
// { field: 'branchId', contextKey: 'branchId' },
|
|
245
|
-
// ] })
|
|
246
|
-
presets: ['softDelete', multiTenantPreset({ tenantField: 'organizationId' })],
|
|
247
|
-
|
|
248
|
-
permissions: {
|
|
249
|
-
list: requireOrgMembership(),
|
|
250
|
-
get: requireOrgMembership(),
|
|
251
|
-
create: allOf(requireOrgMembership(), requireRoles(['admin'])),
|
|
252
|
-
update: allOf(requireOrgMembership(), requireRoles(['admin'])),
|
|
253
|
-
delete: allOf(requireOrgMembership(), requireRoles(['admin'])),
|
|
254
|
-
},
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
export default ${camel}Resource;
|
|
258
|
-
` : `/**
|
|
259
|
-
* ${name} Resource
|
|
260
|
-
* Generated by Arc CLI
|
|
261
|
-
*/
|
|
262
|
-
|
|
263
|
-
import { defineResource } from '@classytic/arc';
|
|
264
|
-
import { createMongooseAdapter } from '@classytic/mongokit/adapter';
|
|
265
|
-
import { requireAuth, requireRoles } from '@classytic/arc/permissions';
|
|
266
|
-
${schemaGeneratorImport}import ${name}${ts ? `, { type I${name} }` : ""} from './${fileName}.model.js';
|
|
267
|
-
import ${camel}Repository from './${fileName}.repository.js';${queryParserImport}
|
|
268
|
-
|
|
269
|
-
const ${camel}Resource = defineResource${ts ? `<I${name}>` : ""}({
|
|
270
|
-
name: '${fileName}',
|
|
271
|
-
adapter: ${adapterCall},${queryParserConfig}
|
|
272
|
-
|
|
273
|
-
// Single-tenant default: arc auto-infers \`tenantField: false\` when
|
|
274
|
-
// the model has no \`organizationId\` path (silent-zero-results
|
|
275
|
-
// footgun closed in 2.12). Set \`tenantField: '<field>'\` to opt INTO
|
|
276
|
-
// tenant scoping, or \`tenantField: false\` to make it explicit.
|
|
277
|
-
presets: ['softDelete'],
|
|
278
|
-
|
|
279
|
-
permissions: {
|
|
280
|
-
list: requireAuth(),
|
|
281
|
-
get: requireAuth(),
|
|
282
|
-
create: requireRoles(['admin']),
|
|
283
|
-
update: requireRoles(['admin']),
|
|
284
|
-
delete: requireRoles(['admin']),
|
|
285
|
-
},
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
export default ${camel}Resource;
|
|
289
|
-
`;
|
|
290
|
-
},
|
|
291
|
-
mcp: (name, fileName) => {
|
|
292
|
-
const camel = toCamelCase(name);
|
|
293
|
-
return `/**
|
|
294
|
-
* ${name} MCP Tools
|
|
295
|
-
* Generated by Arc CLI
|
|
296
|
-
*
|
|
297
|
-
* Custom MCP tools for the ${name} domain.
|
|
298
|
-
* CRUD tools (list/get/create/update/delete) are auto-generated by mcpPlugin.
|
|
299
|
-
* Add domain-specific tools here (actions, analytics, workflows).
|
|
300
|
-
*/
|
|
301
|
-
|
|
302
|
-
import { defineTool } from '@classytic/arc/mcp';
|
|
303
|
-
${ts ? "import { z } from 'zod';\n" : "const { z } = require('zod');\n"}
|
|
304
|
-
// Example: domain-specific action tool
|
|
305
|
-
// export const activate${name}Tool = defineTool('activate_${fileName}', {
|
|
306
|
-
// description: 'Activate a ${name.toLowerCase()} by ID',
|
|
307
|
-
// input: { id: z.string().describe('${name} ID') },
|
|
308
|
-
// annotations: { destructiveHint: true, idempotentHint: true },
|
|
309
|
-
// handler: async ({ id }, ctx) => {
|
|
310
|
-
// // Your logic here
|
|
311
|
-
// return { content: [{ type: 'text', text: \`Activated ${name.toLowerCase()} \${id}\` }] };
|
|
312
|
-
// },
|
|
313
|
-
// });
|
|
314
|
-
|
|
315
|
-
// Example: read-only analytics tool
|
|
316
|
-
// export const ${camel}StatsTool = defineTool('${fileName}_stats', {
|
|
317
|
-
// description: 'Get ${name.toLowerCase()} statistics',
|
|
318
|
-
// input: { period: z.enum(['7d', '30d', '90d']).optional() },
|
|
319
|
-
// annotations: { readOnlyHint: true },
|
|
320
|
-
// handler: async ({ period }) => {
|
|
321
|
-
// // Your logic here
|
|
322
|
-
// return { content: [{ type: 'text', text: JSON.stringify({ total: 0, period }) }] };
|
|
323
|
-
// },
|
|
324
|
-
// });
|
|
325
|
-
`;
|
|
326
|
-
},
|
|
327
|
-
test: (name, fileName) => {
|
|
328
|
-
const camel = toCamelCase(name);
|
|
329
|
-
return `/**
|
|
330
|
-
* ${name} Tests
|
|
331
|
-
* Generated by Arc CLI
|
|
332
|
-
*
|
|
333
|
-
* Testing surface (arc 2.12+):
|
|
334
|
-
* - createTestApp turnkey Fastify + in-memory Mongo + auth + fixtures
|
|
335
|
-
* - expectArc fluent matchers — .ok / .failed / .unauthorized /
|
|
336
|
-
* .forbidden / .notFound / .conflict / .validationError /
|
|
337
|
-
* .paginated / .hidesField / .hasData / .hasStatus
|
|
338
|
-
* - ctx.auth unified TestAuthProvider — register a role, reuse .headers
|
|
339
|
-
*
|
|
340
|
-
* Wire shape (post-2.12): single-doc responses are flat (\`{_id, name, ...}\`),
|
|
341
|
-
* paginated responses are \`{ method: 'offset', data: [...], page, ... }\`.
|
|
342
|
-
* No \`success\` envelope — HTTP status discriminates success vs error;
|
|
343
|
-
* errors carry the canonical ErrorContract \`{ code, message, status }\`.
|
|
344
|
-
*/
|
|
345
|
-
|
|
346
|
-
import { describe, it, beforeAll, afterAll, expect } from 'vitest';
|
|
347
|
-
import { createTestApp, expectArc } from '@classytic/arc/testing';
|
|
348
|
-
import type { TestAppContext } from '@classytic/arc/testing';
|
|
349
|
-
import ${camel}Resource from '../src/resources/${fileName}/${fileName}.resource.js';
|
|
350
|
-
|
|
351
|
-
describe('${name} Resource', () => {
|
|
352
|
-
let ctx${ts ? ": TestAppContext" : ""};
|
|
353
|
-
|
|
354
|
-
beforeAll(async () => {
|
|
355
|
-
ctx = await createTestApp({
|
|
356
|
-
resources: [${camel}Resource],
|
|
357
|
-
authMode: 'jwt',
|
|
358
|
-
connectMongoose: true,
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
ctx.auth${ts ? "!" : ""}.register('admin', {
|
|
362
|
-
user: { id: '1', roles: ['admin'] },
|
|
363
|
-
orgId: 'org-1',
|
|
364
|
-
});
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
afterAll(() => ctx.close());
|
|
368
|
-
|
|
369
|
-
describe('GET /${pluralize(fileName)}', () => {
|
|
370
|
-
it('returns a paginated list', async () => {
|
|
371
|
-
const res = await ctx.app.inject({
|
|
372
|
-
method: 'GET',
|
|
373
|
-
url: '/${pluralize(fileName)}',
|
|
374
|
-
headers: ctx.auth${ts ? "!" : ""}.as('admin').headers,
|
|
375
|
-
});
|
|
376
|
-
expectArc(res).ok().paginated();
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
it('rejects unauthenticated requests', async () => {
|
|
380
|
-
const res = await ctx.app.inject({ method: 'GET', url: '/${pluralize(fileName)}' });
|
|
381
|
-
expectArc(res).unauthorized();
|
|
382
|
-
});
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
describe('POST /${pluralize(fileName)}', () => {
|
|
386
|
-
it('creates a record (flat wire shape — no envelope)', async () => {
|
|
387
|
-
const res = await ctx.app.inject({
|
|
388
|
-
method: 'POST',
|
|
389
|
-
url: '/${pluralize(fileName)}',
|
|
390
|
-
headers: ctx.auth${ts ? "!" : ""}.as('admin').headers,
|
|
391
|
-
payload: { name: 'Example' },
|
|
392
|
-
});
|
|
393
|
-
expectArc(res).ok(201);
|
|
394
|
-
// Single-doc response is flat: \`body._id\` (not \`body.data._id\`).
|
|
395
|
-
const body = res.json()${ts ? " as { _id: string; name: string }" : ""};
|
|
396
|
-
expect(body._id).toBeDefined();
|
|
397
|
-
expect(body.name).toBe('Example');
|
|
398
|
-
});
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
// More patterns to extend:
|
|
402
|
-
// - expectArc(res).forbidden() / .notFound() / .validationError()
|
|
403
|
-
// - expectArc(res).hidesField('password') for field-level perms
|
|
404
|
-
// - ctx.fixtures.create('${fileName}', {...}) for seeded data
|
|
405
|
-
// - error wire shape: body.code === 'arc.not_found' (ErrorContract)
|
|
406
|
-
});
|
|
407
|
-
`;
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Generate command handler
|
|
413
|
-
*/
|
|
414
|
-
async function generate(type, args) {
|
|
415
|
-
if (!type) throw new Error("Missing type argument\nUsage: arc generate <resource|controller|model|repository|schemas> <name>");
|
|
416
|
-
const [name, ...restArgs] = args;
|
|
417
|
-
if (!name) throw new Error("Missing name argument\nUsage: arc generate <type> <name>\nExample: arc generate resource product");
|
|
418
|
-
const capitalizedName = toPascalCase(name);
|
|
419
|
-
const lowerName = name.toLowerCase();
|
|
420
|
-
if ((type === "resource" || type === "r" || type === "model" || type === "m") && new Set([
|
|
421
|
-
"user",
|
|
422
|
-
"session",
|
|
423
|
-
"account",
|
|
424
|
-
"verification",
|
|
425
|
-
"organization",
|
|
426
|
-
"member",
|
|
427
|
-
"invitation",
|
|
428
|
-
"team",
|
|
429
|
-
"team-member",
|
|
430
|
-
"apikey"
|
|
431
|
-
]).has(lowerName)) {
|
|
432
|
-
console.warn(`\n[arc generate] "${lowerName}" is a Better Auth-owned collection.\nBetter Auth's organization/admin/bearer plugins write this collection directly,\nso generating a parallel arc model would create a duplicate registration.\n\nRecommended pattern:\n import { createBetterAuthOverlay } from '@classytic/mongokit/better-auth';\n const adapter = await createBetterAuthOverlay({\n auth, mongoose, collection: '${lowerName}',\n });\n // ...then \`defineResource({ name: '${lowerName}', adapter, ... })\` reads\n // BA's collection through arc with full pagination/filters/permissions.\n\nAborting. Re-run with a different name if you need a separate domain model.\n`);
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
const projectConfig = readProjectConfig();
|
|
436
|
-
const ts = projectConfig.typescript ?? isTypeScriptProject();
|
|
437
|
-
const ext = ts ? "ts" : "js";
|
|
438
|
-
const templates = getTemplates(ts, projectConfig);
|
|
439
|
-
const resourcePath = join(process.cwd(), "src", "resources", lowerName);
|
|
440
|
-
switch (type) {
|
|
441
|
-
case "resource":
|
|
442
|
-
case "r":
|
|
443
|
-
await generateResource(capitalizedName, lowerName, resourcePath, templates, ext, projectConfig.mcp === true || restArgs.includes("--mcp"));
|
|
444
|
-
break;
|
|
445
|
-
case "controller":
|
|
446
|
-
case "c":
|
|
447
|
-
await generateFile(capitalizedName, lowerName, resourcePath, "controller", templates.controller, ext);
|
|
448
|
-
break;
|
|
449
|
-
case "model":
|
|
450
|
-
case "m":
|
|
451
|
-
await generateFile(capitalizedName, lowerName, resourcePath, "model", templates.model, ext);
|
|
452
|
-
break;
|
|
453
|
-
case "repository":
|
|
454
|
-
case "repo":
|
|
455
|
-
await generateFile(capitalizedName, lowerName, resourcePath, "repository", templates.repository, ext);
|
|
456
|
-
break;
|
|
457
|
-
case "schemas":
|
|
458
|
-
case "s":
|
|
459
|
-
await generateFile(capitalizedName, lowerName, resourcePath, "schemas", templates.schemas, ext);
|
|
460
|
-
break;
|
|
461
|
-
case "mcp":
|
|
462
|
-
await generateFile(capitalizedName, lowerName, resourcePath, "mcp", templates.mcp, ext);
|
|
463
|
-
break;
|
|
464
|
-
default: throw new Error(`Unknown type: ${type}\nAvailable types: resource, controller, model, repository, schemas, mcp`);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
/**
|
|
468
|
-
* Generate a resource-first scaffold
|
|
469
|
-
*/
|
|
470
|
-
async function generateResource(name, lowerName, resourcePath, templates, ext, includeMcp = false) {
|
|
471
|
-
console.log(`\nGenerating resource: ${name}...\n`);
|
|
472
|
-
if (!existsSync(resourcePath)) {
|
|
473
|
-
mkdirSync(resourcePath, { recursive: true });
|
|
474
|
-
console.log(` + Created: src/resources/${lowerName}/`);
|
|
475
|
-
}
|
|
476
|
-
const files = {
|
|
477
|
-
[`${lowerName}.model.${ext}`]: templates.model(name, lowerName),
|
|
478
|
-
[`${lowerName}.repository.${ext}`]: templates.repository(name, lowerName),
|
|
479
|
-
[`${lowerName}.resource.${ext}`]: templates.resource(name, lowerName)
|
|
480
|
-
};
|
|
481
|
-
if (includeMcp) files[`${lowerName}.mcp.${ext}`] = templates.mcp(name, lowerName);
|
|
482
|
-
for (const [filename, content] of Object.entries(files)) {
|
|
483
|
-
const filepath = join(resourcePath, filename);
|
|
484
|
-
if (existsSync(filepath)) console.warn(` ! Skipped: ${filename} (already exists)`);
|
|
485
|
-
else {
|
|
486
|
-
writeFileSync(filepath, content);
|
|
487
|
-
console.log(` + Created: ${filename}`);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
const testsDir = join(process.cwd(), "tests");
|
|
491
|
-
if (!existsSync(testsDir)) mkdirSync(testsDir, { recursive: true });
|
|
492
|
-
const testPath = join(testsDir, `${lowerName}.test.${ext}`);
|
|
493
|
-
if (!existsSync(testPath)) {
|
|
494
|
-
writeFileSync(testPath, templates.test(name, lowerName));
|
|
495
|
-
console.log(` + Created: tests/${lowerName}.test.${ext}`);
|
|
496
|
-
}
|
|
497
|
-
const camel = toCamelCase(name);
|
|
498
|
-
const isMultiTenant = readProjectConfig().tenant === "multi";
|
|
499
|
-
console.log(`
|
|
500
|
-
╔═══════════════════════════════════════════════════════════════╗
|
|
501
|
-
║ Resource Generated ║
|
|
502
|
-
╚═══════════════════════════════════════════════════════════════╝
|
|
503
|
-
|
|
504
|
-
Next steps:
|
|
505
|
-
|
|
506
|
-
1. Register in src/resources/index.${ext}:
|
|
507
|
-
import ${camel}Resource from './${lowerName}/${lowerName}.resource.js';
|
|
508
|
-
|
|
509
|
-
export const resources = [
|
|
510
|
-
// ... existing resources
|
|
511
|
-
${camel}Resource,
|
|
512
|
-
];
|
|
513
|
-
|
|
514
|
-
2. Customize the model schema in:
|
|
515
|
-
src/resources/${lowerName}/${lowerName}.model.${ext}
|
|
516
|
-
|
|
517
|
-
3. Adjust permissions in ${lowerName}.resource.${ext}:
|
|
518
|
-
${isMultiTenant ? ` - requireOrgMembership() → member of the current organization
|
|
519
|
-
- multiTenantPreset() → injects and filters organizationId
|
|
520
|
-
- requireRoles(['admin']) → admin writes inside the org scope` : ` - requireAuth() → any authenticated user
|
|
521
|
-
- requireRoles(['admin']) → specific platform roles`}
|
|
522
|
-
|
|
523
|
-
4. Run tests:
|
|
524
|
-
npm test
|
|
525
|
-
${includeMcp ? `
|
|
526
|
-
5. MCP tools file created: ${lowerName}.mcp.${ext}
|
|
527
|
-
Uncomment and customize the example tools.
|
|
528
|
-
Import and add to extraTools in your mcpPlugin config.
|
|
529
|
-
` : ""}`);
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
532
|
-
* Generate a single file
|
|
533
|
-
*/
|
|
534
|
-
async function generateFile(name, lowerName, resourcePath, fileType, template, ext) {
|
|
535
|
-
console.log(`\nGenerating ${fileType}: ${name}...\n`);
|
|
536
|
-
if (!existsSync(resourcePath)) {
|
|
537
|
-
mkdirSync(resourcePath, { recursive: true });
|
|
538
|
-
console.log(` + Created: src/resources/${lowerName}/`);
|
|
539
|
-
}
|
|
540
|
-
const filename = `${lowerName}.${fileType}.${ext}`;
|
|
541
|
-
const filepath = join(resourcePath, filename);
|
|
542
|
-
if (existsSync(filepath)) throw new Error(`${filename} already exists. Remove it first or use a different name.`);
|
|
543
|
-
writeFileSync(filepath, template(name, lowerName));
|
|
544
|
-
console.log(` + Created: ${filename}`);
|
|
545
|
-
}
|
|
546
|
-
//#endregion
|
|
1
|
+
import { t as generate } from "../../generate-CYac-OLv.mjs";
|
|
547
2
|
export { generate };
|
|
@@ -1,40 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Arc CLI - Init Command
|
|
4
|
-
*
|
|
5
|
-
* Scaffolds a new Arc project with clean architecture:
|
|
6
|
-
* - MongoKit or Custom adapter
|
|
7
|
-
* - Multi-tenant or Single-tenant
|
|
8
|
-
* - TypeScript or JavaScript
|
|
9
|
-
*
|
|
10
|
-
* Automatically installs dependencies using detected package manager.
|
|
11
|
-
*/
|
|
12
|
-
interface InitOptions {
|
|
13
|
-
name?: string;
|
|
14
|
-
adapter?: "mongokit" | "custom";
|
|
15
|
-
auth?: "jwt" | "better-auth";
|
|
16
|
-
tenant?: "multi" | "single";
|
|
17
|
-
/**
|
|
18
|
-
* Enable Better Auth's `apiKey` plugin (`@better-auth/api-key`) for
|
|
19
|
-
* machine-to-machine authentication alongside cookie/session auth.
|
|
20
|
-
* Only used when `auth === 'better-auth'`. Default: false.
|
|
21
|
-
*/
|
|
22
|
-
apiKey?: boolean;
|
|
23
|
-
/**
|
|
24
|
-
* Session strategy when using Better Auth.
|
|
25
|
-
* - `cookie` (default): browser cookie + DB-backed session (BA's default)
|
|
26
|
-
* - `bearer`: Authorization: Bearer header (mobile apps, SPA, M2M)
|
|
27
|
-
* Both can coexist — `bearer: true` adds bearer alongside cookies.
|
|
28
|
-
*/
|
|
29
|
-
session?: "cookie" | "bearer" | "both";
|
|
30
|
-
typescript?: boolean;
|
|
31
|
-
edge?: boolean;
|
|
32
|
-
skipInstall?: boolean;
|
|
33
|
-
force?: boolean;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Initialize a new Arc project
|
|
37
|
-
*/
|
|
38
|
-
declare function init(options?: InitOptions): Promise<void>;
|
|
39
|
-
//#endregion
|
|
40
|
-
export { InitOptions, init };
|
|
1
|
+
import { a as ProjectConfig, i as PackageManager, n as DependencyManifest, r as InitOptions, t as init } from "../../init-Dv71MsJr.mjs";
|
|
2
|
+
export { DependencyManifest, InitOptions, PackageManager, ProjectConfig, init };
|