@classytic/arc 1.1.0 → 2.1.2
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 +247 -794
- package/bin/arc.js +91 -52
- package/dist/EventTransport-BD2U0BTc.d.mts +100 -0
- package/dist/EventTransport-BD2U0BTc.d.mts.map +1 -0
- package/dist/HookSystem-BsGV-j2l.mjs +405 -0
- package/dist/HookSystem-BsGV-j2l.mjs.map +1 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs +250 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs.map +1 -0
- package/dist/adapters/index.d.mts +5 -0
- package/dist/adapters/index.mjs +3 -0
- package/dist/audit/index.d.mts +82 -0
- package/dist/audit/index.d.mts.map +1 -0
- package/dist/audit/index.mjs +276 -0
- package/dist/audit/index.mjs.map +1 -0
- package/dist/audit/mongodb.d.mts +5 -0
- package/dist/audit/mongodb.mjs +3 -0
- package/dist/audited-C3T5DTUx.mjs +141 -0
- package/dist/audited-C3T5DTUx.mjs.map +1 -0
- package/dist/auth/index.d.mts +189 -0
- package/dist/auth/index.d.mts.map +1 -0
- package/dist/auth/index.mjs +1102 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/auth/redis-session.d.mts +44 -0
- package/dist/auth/redis-session.d.mts.map +1 -0
- package/dist/auth/redis-session.mjs +76 -0
- package/dist/auth/redis-session.mjs.map +1 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs +250 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs.map +1 -0
- package/dist/cache/index.d.mts +146 -0
- package/dist/cache/index.d.mts.map +1 -0
- package/dist/cache/index.mjs +92 -0
- package/dist/cache/index.mjs.map +1 -0
- package/dist/caching-Bl28lYsR.mjs +94 -0
- package/dist/caching-Bl28lYsR.mjs.map +1 -0
- package/dist/chunk-C7Uep-_p.mjs +20 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs +1097 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs.map +1 -0
- package/dist/cli/commands/describe.d.mts +19 -0
- package/dist/cli/commands/describe.d.mts.map +1 -0
- package/dist/cli/commands/describe.mjs +239 -0
- package/dist/cli/commands/describe.mjs.map +1 -0
- package/dist/cli/commands/docs.d.mts +14 -0
- package/dist/cli/commands/docs.d.mts.map +1 -0
- package/dist/cli/commands/docs.mjs +53 -0
- package/dist/cli/commands/docs.mjs.map +1 -0
- package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -1
- package/dist/cli/commands/generate.d.mts.map +1 -0
- package/dist/cli/commands/generate.mjs +358 -0
- package/dist/cli/commands/generate.mjs.map +1 -0
- package/dist/cli/commands/{init.d.ts → init.d.mts} +12 -8
- package/dist/cli/commands/init.d.mts.map +1 -0
- package/dist/cli/commands/{init.js → init.mjs} +807 -616
- package/dist/cli/commands/init.mjs.map +1 -0
- package/dist/cli/commands/introspect.d.mts +11 -0
- package/dist/cli/commands/introspect.d.mts.map +1 -0
- package/dist/cli/commands/introspect.mjs +76 -0
- package/dist/cli/commands/introspect.mjs.map +1 -0
- package/dist/cli/index.d.mts +17 -0
- package/dist/cli/index.d.mts.map +1 -0
- package/dist/cli/index.mjs +157 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/constants-DdXFXQtN.mjs +85 -0
- package/dist/constants-DdXFXQtN.mjs.map +1 -0
- package/dist/core/index.d.mts +5 -0
- package/dist/core/index.mjs +4 -0
- package/dist/createApp-CUgNqegw.mjs +560 -0
- package/dist/createApp-CUgNqegw.mjs.map +1 -0
- package/dist/defineResource-k0_BDn8v.mjs +2197 -0
- package/dist/defineResource-k0_BDn8v.mjs.map +1 -0
- package/dist/discovery/index.d.mts +47 -0
- package/dist/discovery/index.d.mts.map +1 -0
- package/dist/discovery/index.mjs +110 -0
- package/dist/discovery/index.mjs.map +1 -0
- package/dist/docs/index.d.mts +163 -0
- package/dist/docs/index.d.mts.map +1 -0
- package/dist/docs/index.mjs +73 -0
- package/dist/docs/index.mjs.map +1 -0
- package/dist/elevation-BRy3yFWT.mjs +113 -0
- package/dist/elevation-BRy3yFWT.mjs.map +1 -0
- package/dist/elevation-B_2dRLVP.d.mts +88 -0
- package/dist/elevation-B_2dRLVP.d.mts.map +1 -0
- package/dist/errorHandler-BbcgBmIH.d.mts +73 -0
- package/dist/errorHandler-BbcgBmIH.d.mts.map +1 -0
- package/dist/errorHandler-C1okiriz.mjs +109 -0
- package/dist/errorHandler-C1okiriz.mjs.map +1 -0
- package/dist/errors-B9bZok84.mjs +212 -0
- package/dist/errors-B9bZok84.mjs.map +1 -0
- package/dist/errors-ChKiFz62.d.mts +125 -0
- package/dist/errors-ChKiFz62.d.mts.map +1 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts +125 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts.map +1 -0
- package/dist/eventPlugin-DGR_B2on.mjs +230 -0
- package/dist/eventPlugin-DGR_B2on.mjs.map +1 -0
- package/dist/events/index.d.mts +54 -0
- package/dist/events/index.d.mts.map +1 -0
- package/dist/events/index.mjs +52 -0
- package/dist/events/index.mjs.map +1 -0
- package/dist/events/transports/redis-stream-entry.d.mts +2 -0
- package/dist/events/transports/redis-stream-entry.mjs +178 -0
- package/dist/events/transports/redis-stream-entry.mjs.map +1 -0
- package/dist/events/transports/redis.d.mts +77 -0
- package/dist/events/transports/redis.d.mts.map +1 -0
- package/dist/events/transports/redis.mjs +125 -0
- package/dist/events/transports/redis.mjs.map +1 -0
- package/dist/externalPaths-DlINfKbP.d.mts +51 -0
- package/dist/externalPaths-DlINfKbP.d.mts.map +1 -0
- package/dist/factory/index.d.mts +64 -0
- package/dist/factory/index.d.mts.map +1 -0
- package/dist/factory/index.mjs +3 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts +217 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts.map +1 -0
- package/dist/fields-DyaDVX4J.d.mts +110 -0
- package/dist/fields-DyaDVX4J.d.mts.map +1 -0
- package/dist/fields-iagOozy0.mjs +115 -0
- package/dist/fields-iagOozy0.mjs.map +1 -0
- package/dist/hooks/index.d.mts +4 -0
- package/dist/hooks/index.mjs +3 -0
- package/dist/idempotency/index.d.mts +97 -0
- package/dist/idempotency/index.d.mts.map +1 -0
- package/dist/idempotency/index.mjs +320 -0
- package/dist/idempotency/index.mjs.map +1 -0
- package/dist/idempotency/mongodb.d.mts +2 -0
- package/dist/idempotency/mongodb.mjs +115 -0
- package/dist/idempotency/mongodb.mjs.map +1 -0
- package/dist/idempotency/redis.d.mts +2 -0
- package/dist/idempotency/redis.mjs +104 -0
- package/dist/idempotency/redis.mjs.map +1 -0
- package/dist/index.d.mts +261 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +105 -0
- package/dist/index.mjs.map +1 -0
- package/dist/integrations/event-gateway.d.mts +47 -0
- package/dist/integrations/event-gateway.d.mts.map +1 -0
- package/dist/integrations/event-gateway.mjs +44 -0
- package/dist/integrations/event-gateway.mjs.map +1 -0
- package/dist/integrations/index.d.mts +5 -0
- package/dist/integrations/index.mjs +1 -0
- package/dist/integrations/jobs.d.mts +104 -0
- package/dist/integrations/jobs.d.mts.map +1 -0
- package/dist/integrations/jobs.mjs +124 -0
- package/dist/integrations/jobs.mjs.map +1 -0
- package/dist/integrations/streamline.d.mts +61 -0
- package/dist/integrations/streamline.d.mts.map +1 -0
- package/dist/integrations/streamline.mjs +126 -0
- package/dist/integrations/streamline.mjs.map +1 -0
- package/dist/integrations/websocket.d.mts +83 -0
- package/dist/integrations/websocket.d.mts.map +1 -0
- package/dist/integrations/websocket.mjs +289 -0
- package/dist/integrations/websocket.mjs.map +1 -0
- package/dist/interface-B01JvPVc.d.mts +78 -0
- package/dist/interface-B01JvPVc.d.mts.map +1 -0
- package/dist/interface-CZe8IkMf.d.mts +55 -0
- package/dist/interface-CZe8IkMf.d.mts.map +1 -0
- package/dist/interface-Ch8HU9uM.d.mts +1098 -0
- package/dist/interface-Ch8HU9uM.d.mts.map +1 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs +54 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs.map +1 -0
- package/dist/keys-BqNejWup.mjs +43 -0
- package/dist/keys-BqNejWup.mjs.map +1 -0
- package/dist/logger-Df2O2WsW.mjs +79 -0
- package/dist/logger-Df2O2WsW.mjs.map +1 -0
- package/dist/memory-cQgelFOj.mjs +144 -0
- package/dist/memory-cQgelFOj.mjs.map +1 -0
- package/dist/migrations/index.d.mts +157 -0
- package/dist/migrations/index.d.mts.map +1 -0
- package/dist/migrations/index.mjs +261 -0
- package/dist/migrations/index.mjs.map +1 -0
- package/dist/mongodb-BfJVlUJH.mjs +94 -0
- package/dist/mongodb-BfJVlUJH.mjs.map +1 -0
- package/dist/mongodb-CGzRbfAK.d.mts +119 -0
- package/dist/mongodb-CGzRbfAK.d.mts.map +1 -0
- package/dist/mongodb-JN-9JA7K.d.mts +72 -0
- package/dist/mongodb-JN-9JA7K.d.mts.map +1 -0
- package/dist/openapi-G3Cw7XuM.mjs +524 -0
- package/dist/openapi-G3Cw7XuM.mjs.map +1 -0
- package/dist/org/index.d.mts +69 -0
- package/dist/org/index.d.mts.map +1 -0
- package/dist/org/index.mjs +514 -0
- package/dist/org/index.mjs.map +1 -0
- package/dist/org/types.d.mts +83 -0
- package/dist/org/types.d.mts.map +1 -0
- package/dist/org/types.mjs +1 -0
- package/dist/permissions/index.d.mts +279 -0
- package/dist/permissions/index.d.mts.map +1 -0
- package/dist/permissions/index.mjs +579 -0
- package/dist/permissions/index.mjs.map +1 -0
- package/dist/plugins/index.d.mts +173 -0
- package/dist/plugins/index.d.mts.map +1 -0
- package/dist/plugins/index.mjs +523 -0
- package/dist/plugins/index.mjs.map +1 -0
- package/dist/plugins/response-cache.d.mts +88 -0
- package/dist/plugins/response-cache.d.mts.map +1 -0
- package/dist/plugins/response-cache.mjs +284 -0
- package/dist/plugins/response-cache.mjs.map +1 -0
- package/dist/plugins/tracing-entry.d.mts +2 -0
- package/dist/plugins/tracing-entry.mjs +186 -0
- package/dist/plugins/tracing-entry.mjs.map +1 -0
- package/dist/pluralize-CEweyOEm.mjs +87 -0
- package/dist/pluralize-CEweyOEm.mjs.map +1 -0
- package/dist/policies/{index.d.ts → index.d.mts} +204 -169
- package/dist/policies/index.d.mts.map +1 -0
- package/dist/policies/index.mjs +322 -0
- package/dist/policies/index.mjs.map +1 -0
- package/dist/presets/{index.d.ts → index.d.mts} +63 -131
- package/dist/presets/index.d.mts.map +1 -0
- package/dist/presets/index.mjs +144 -0
- package/dist/presets/index.mjs.map +1 -0
- package/dist/presets/multiTenant.d.mts +25 -0
- package/dist/presets/multiTenant.d.mts.map +1 -0
- package/dist/presets/multiTenant.mjs +114 -0
- package/dist/presets/multiTenant.mjs.map +1 -0
- package/dist/presets-BITljm96.mjs +120 -0
- package/dist/presets-BITljm96.mjs.map +1 -0
- package/dist/presets-DzSMwlKj.d.mts +58 -0
- package/dist/presets-DzSMwlKj.d.mts.map +1 -0
- package/dist/prisma-DJbMt3yf.mjs +628 -0
- package/dist/prisma-DJbMt3yf.mjs.map +1 -0
- package/dist/prisma-Dg9GoVdj.d.mts +275 -0
- package/dist/prisma-Dg9GoVdj.d.mts.map +1 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts +72 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts.map +1 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs +139 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs.map +1 -0
- package/dist/redis-D-JAeLtm.d.mts +50 -0
- package/dist/redis-D-JAeLtm.d.mts.map +1 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts +104 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts.map +1 -0
- package/dist/registry/index.d.mts +12 -0
- package/dist/registry/index.d.mts.map +1 -0
- package/dist/registry/index.mjs +4 -0
- package/dist/requestContext-QQD6ROJc.mjs +56 -0
- package/dist/requestContext-QQD6ROJc.mjs.map +1 -0
- package/dist/schemaConverter-BwrmWroW.mjs +99 -0
- package/dist/schemaConverter-BwrmWroW.mjs.map +1 -0
- package/dist/schemas/index.d.mts +64 -0
- package/dist/schemas/index.d.mts.map +1 -0
- package/dist/schemas/index.mjs +83 -0
- package/dist/schemas/index.mjs.map +1 -0
- package/dist/scope/index.d.mts +22 -0
- package/dist/scope/index.d.mts.map +1 -0
- package/dist/scope/index.mjs +66 -0
- package/dist/scope/index.mjs.map +1 -0
- package/dist/sessionManager-jPKLbHE0.d.mts +187 -0
- package/dist/sessionManager-jPKLbHE0.d.mts.map +1 -0
- package/dist/sse-B3c3_yZp.mjs +124 -0
- package/dist/sse-B3c3_yZp.mjs.map +1 -0
- package/dist/testing/index.d.mts +908 -0
- package/dist/testing/index.d.mts.map +1 -0
- package/dist/testing/index.mjs +1977 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/tracing-Cc7vVQPp.d.mts +71 -0
- package/dist/tracing-Cc7vVQPp.d.mts.map +1 -0
- package/dist/typeGuards-DhMNLuvU.mjs +10 -0
- package/dist/typeGuards-DhMNLuvU.mjs.map +1 -0
- package/dist/types/index.d.mts +947 -0
- package/dist/types/index.d.mts.map +1 -0
- package/dist/types/index.mjs +15 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/types-Beqn1Un7.mjs +39 -0
- package/dist/types-Beqn1Un7.mjs.map +1 -0
- package/dist/types-CIgB7UUl.d.mts +446 -0
- package/dist/types-CIgB7UUl.d.mts.map +1 -0
- package/dist/types-aYB4V7uN.d.mts +87 -0
- package/dist/types-aYB4V7uN.d.mts.map +1 -0
- package/dist/utils/index.d.mts +748 -0
- package/dist/utils/index.d.mts.map +1 -0
- package/dist/utils/index.mjs +6 -0
- package/package.json +194 -68
- package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
- package/dist/adapters/index.d.ts +0 -237
- package/dist/adapters/index.js +0 -668
- package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
- package/dist/audit/index.d.ts +0 -195
- package/dist/audit/index.js +0 -319
- package/dist/auth/index.d.ts +0 -47
- package/dist/auth/index.js +0 -174
- package/dist/cli/commands/docs.d.ts +0 -11
- package/dist/cli/commands/docs.js +0 -474
- package/dist/cli/commands/generate.js +0 -334
- package/dist/cli/commands/introspect.d.ts +0 -8
- package/dist/cli/commands/introspect.js +0 -338
- package/dist/cli/index.d.ts +0 -4
- package/dist/cli/index.js +0 -3269
- package/dist/core/index.d.ts +0 -220
- package/dist/core/index.js +0 -2786
- package/dist/createApp-Ce9wl8W9.d.ts +0 -77
- package/dist/docs/index.d.ts +0 -166
- package/dist/docs/index.js +0 -658
- package/dist/errors-8WIxGS_6.d.ts +0 -122
- package/dist/events/index.d.ts +0 -117
- package/dist/events/index.js +0 -89
- package/dist/factory/index.d.ts +0 -38
- package/dist/factory/index.js +0 -1652
- package/dist/hooks/index.d.ts +0 -4
- package/dist/hooks/index.js +0 -199
- package/dist/idempotency/index.d.ts +0 -323
- package/dist/idempotency/index.js +0 -500
- package/dist/index-B4t03KQ0.d.ts +0 -1366
- package/dist/index.d.ts +0 -135
- package/dist/index.js +0 -4756
- package/dist/migrations/index.d.ts +0 -185
- package/dist/migrations/index.js +0 -274
- package/dist/org/index.d.ts +0 -129
- package/dist/org/index.js +0 -220
- package/dist/permissions/index.d.ts +0 -144
- package/dist/permissions/index.js +0 -103
- package/dist/plugins/index.d.ts +0 -46
- package/dist/plugins/index.js +0 -1069
- package/dist/policies/index.js +0 -196
- package/dist/presets/index.js +0 -384
- package/dist/presets/multiTenant.d.ts +0 -39
- package/dist/presets/multiTenant.js +0 -112
- package/dist/registry/index.d.ts +0 -16
- package/dist/registry/index.js +0 -253
- package/dist/testing/index.d.ts +0 -618
- package/dist/testing/index.js +0 -48020
- package/dist/types/index.d.ts +0 -4
- package/dist/types/index.js +0 -8
- package/dist/types-B99TBmFV.d.ts +0 -76
- package/dist/types-BvckRbs2.d.ts +0 -143
- package/dist/utils/index.d.ts +0 -679
- package/dist/utils/index.js +0 -931
|
@@ -1,325 +1,317 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as path from
|
|
3
|
-
import * as
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import { accessSync } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as readline from "node:readline";
|
|
5
|
+
import { execSync, spawn } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
//#region src/cli/commands/init.ts
|
|
8
|
+
/**
|
|
9
|
+
* Arc CLI - Init Command
|
|
10
|
+
*
|
|
11
|
+
* Scaffolds a new Arc project with clean architecture:
|
|
12
|
+
* - MongoKit or Custom adapter
|
|
13
|
+
* - Multi-tenant or Single-tenant
|
|
14
|
+
* - TypeScript or JavaScript
|
|
15
|
+
*
|
|
16
|
+
* Automatically installs dependencies using detected package manager.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Initialize a new Arc project
|
|
20
|
+
*/
|
|
12
21
|
async function init(options = {}) {
|
|
13
|
-
|
|
22
|
+
console.log(`
|
|
14
23
|
╔═══════════════════════════════════════════════════════════════╗
|
|
15
|
-
║
|
|
24
|
+
║ Arc Project Setup ║
|
|
16
25
|
║ Resource-Oriented Backend Framework ║
|
|
17
26
|
╚═══════════════════════════════════════════════════════════════╝
|
|
18
27
|
`);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
`);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
await installDependencies(projectPath, config, packageManager);
|
|
42
|
-
}
|
|
43
|
-
printSuccessMessage(config, options.skipInstall);
|
|
28
|
+
const config = await gatherConfig(options);
|
|
29
|
+
console.log(`\nCreating project: ${config.name}`);
|
|
30
|
+
console.log(` Adapter: ${config.adapter === "mongokit" ? "MongoKit (MongoDB)" : "Custom"}`);
|
|
31
|
+
console.log(` Auth: ${config.auth === "better-auth" ? "Better Auth (recommended)" : "Arc JWT"}`);
|
|
32
|
+
console.log(` Tenant: ${config.tenant === "multi" ? "Multi-tenant" : "Single-tenant"}`);
|
|
33
|
+
console.log(` Language: ${config.typescript ? "TypeScript" : "JavaScript"}`);
|
|
34
|
+
console.log(` Target: ${config.edge ? "Edge/Serverless" : "Node.js Server"}\n`);
|
|
35
|
+
const projectPath = path.join(process.cwd(), config.name);
|
|
36
|
+
try {
|
|
37
|
+
await fs.access(projectPath);
|
|
38
|
+
if (!options.force) throw new Error(`Directory "${config.name}" already exists. Use --force to overwrite.`);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (!(err && typeof err === "object" && "code" in err && err.code === "ENOENT")) throw err;
|
|
41
|
+
}
|
|
42
|
+
const packageManager = detectPackageManager();
|
|
43
|
+
console.log(`Using package manager: ${packageManager}\n`);
|
|
44
|
+
await createProjectStructure(projectPath, config);
|
|
45
|
+
if (!options.skipInstall) {
|
|
46
|
+
console.log("\n📥 Installing dependencies...\n");
|
|
47
|
+
await installDependencies(projectPath, config, packageManager);
|
|
48
|
+
}
|
|
49
|
+
printSuccessMessage(config, options.skipInstall);
|
|
44
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Detect which package manager to use
|
|
53
|
+
* Priority: pnpm > yarn > bun > npm (based on lockfile or global availability)
|
|
54
|
+
*/
|
|
45
55
|
function detectPackageManager() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return "npm";
|
|
56
|
+
try {
|
|
57
|
+
const cwd = process.cwd();
|
|
58
|
+
if (existsSync$1(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
59
|
+
if (existsSync$1(path.join(cwd, "yarn.lock"))) return "yarn";
|
|
60
|
+
if (existsSync$1(path.join(cwd, "bun.lockb"))) return "bun";
|
|
61
|
+
if (existsSync$1(path.join(cwd, "package-lock.json"))) return "npm";
|
|
62
|
+
} catch {}
|
|
63
|
+
if (isCommandAvailable("pnpm")) return "pnpm";
|
|
64
|
+
if (isCommandAvailable("yarn")) return "yarn";
|
|
65
|
+
if (isCommandAvailable("bun")) return "bun";
|
|
66
|
+
return "npm";
|
|
58
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if a command is available in PATH
|
|
70
|
+
*/
|
|
59
71
|
function isCommandAvailable(command) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
try {
|
|
73
|
+
execSync(`${command} --version`, { stdio: "ignore" });
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
66
78
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Sync check if file exists (ESM-compatible — no require())
|
|
81
|
+
*/
|
|
82
|
+
function existsSync$1(filePath) {
|
|
83
|
+
try {
|
|
84
|
+
accessSync(filePath);
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
74
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Install dependencies using the detected package manager
|
|
92
|
+
*/
|
|
75
93
|
async function installDependencies(projectPath, config, pm) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"typescript@latest",
|
|
99
|
-
"@types/node@latest",
|
|
100
|
-
"@types/jsonwebtoken@latest",
|
|
101
|
-
"tsx@latest"
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
const installCmd = getInstallCommand(pm, deps, false);
|
|
105
|
-
const installDevCmd = getInstallCommand(pm, devDeps, true);
|
|
106
|
-
console.log(` Installing dependencies...`);
|
|
107
|
-
await runCommand(installCmd, projectPath);
|
|
108
|
-
console.log(` Installing dev dependencies...`);
|
|
109
|
-
await runCommand(installDevCmd, projectPath);
|
|
110
|
-
console.log(`
|
|
111
|
-
✅ Dependencies installed successfully!`);
|
|
94
|
+
const deps = [
|
|
95
|
+
"@classytic/arc@latest",
|
|
96
|
+
"fastify@latest",
|
|
97
|
+
"@fastify/cors@latest",
|
|
98
|
+
"@fastify/helmet@latest",
|
|
99
|
+
"@fastify/rate-limit@latest",
|
|
100
|
+
"@fastify/sensible@latest",
|
|
101
|
+
"@fastify/under-pressure@latest",
|
|
102
|
+
"dotenv@latest"
|
|
103
|
+
];
|
|
104
|
+
if (config.auth === "better-auth") deps.push("better-auth@latest", "mongodb@latest");
|
|
105
|
+
else deps.push("@fastify/jwt@latest", "bcryptjs@latest");
|
|
106
|
+
if (config.adapter === "mongokit") deps.push("@classytic/mongokit@latest", "mongoose@latest");
|
|
107
|
+
const devDeps = ["vitest@latest", "pino-pretty@latest"];
|
|
108
|
+
if (config.typescript) devDeps.push("typescript@latest", "@types/node@latest", "tsx@latest");
|
|
109
|
+
const installCmd = getInstallCommand(pm, deps, false);
|
|
110
|
+
const installDevCmd = getInstallCommand(pm, devDeps, true);
|
|
111
|
+
console.log(` Installing dependencies...`);
|
|
112
|
+
await runCommand(installCmd, projectPath);
|
|
113
|
+
console.log(` Installing dev dependencies...`);
|
|
114
|
+
await runCommand(installDevCmd, projectPath);
|
|
115
|
+
console.log(`\nDependencies installed successfully.`);
|
|
112
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Get the install command for a package manager
|
|
119
|
+
*/
|
|
113
120
|
function getInstallCommand(pm, packages, isDev) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return `bun add ${isDev ? "-d" : ""} ${pkgList}`;
|
|
122
|
-
case "npm":
|
|
123
|
-
default:
|
|
124
|
-
return `npm install ${isDev ? "--save-dev" : ""} ${pkgList}`;
|
|
125
|
-
}
|
|
121
|
+
const pkgList = packages.join(" ");
|
|
122
|
+
switch (pm) {
|
|
123
|
+
case "pnpm": return `pnpm add ${isDev ? "-D" : ""} ${pkgList}`;
|
|
124
|
+
case "yarn": return `yarn add ${isDev ? "-D" : ""} ${pkgList}`;
|
|
125
|
+
case "bun": return `bun add ${isDev ? "-d" : ""} ${pkgList}`;
|
|
126
|
+
default: return `npm install ${isDev ? "--save-dev" : ""} ${pkgList}`;
|
|
127
|
+
}
|
|
126
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Run a shell command in a directory
|
|
131
|
+
*/
|
|
127
132
|
function runCommand(command, cwd) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
child.on("error", reject);
|
|
145
|
-
});
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const isWindows = process.platform === "win32";
|
|
135
|
+
const child = spawn(isWindows ? "cmd" : "/bin/sh", [isWindows ? "/c" : "-c", command], {
|
|
136
|
+
cwd,
|
|
137
|
+
stdio: "inherit",
|
|
138
|
+
env: {
|
|
139
|
+
...process.env,
|
|
140
|
+
FORCE_COLOR: "1"
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
child.on("close", (code) => {
|
|
144
|
+
if (code === 0) resolve();
|
|
145
|
+
else reject(/* @__PURE__ */ new Error(`Command failed with exit code ${code}`));
|
|
146
|
+
});
|
|
147
|
+
child.on("error", reject);
|
|
148
|
+
});
|
|
146
149
|
}
|
|
147
150
|
async function gatherConfig(options) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
151
|
+
const rl = readline.createInterface({
|
|
152
|
+
input: process.stdin,
|
|
153
|
+
output: process.stdout
|
|
154
|
+
});
|
|
155
|
+
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
156
|
+
const nonInteractive = !!options.name;
|
|
157
|
+
try {
|
|
158
|
+
const name = options.name || await question("Project name: ") || "my-arc-app";
|
|
159
|
+
let adapter = options.adapter || "mongokit";
|
|
160
|
+
if (!options.adapter && !nonInteractive) adapter = await question("Database adapter [1=MongoKit (recommended), 2=Custom]: ") === "2" ? "custom" : "mongokit";
|
|
161
|
+
let auth = options.auth || "better-auth";
|
|
162
|
+
if (!options.auth && !nonInteractive) auth = await question("Auth strategy [1=Better Auth (recommended), 2=Arc JWT]: ") === "2" ? "jwt" : "better-auth";
|
|
163
|
+
let tenant = options.tenant || "single";
|
|
164
|
+
if (!options.tenant && !nonInteractive) tenant = await question("Tenant mode [1=Single-tenant, 2=Multi-tenant]: ") === "2" ? "multi" : "single";
|
|
165
|
+
let typescript = options.typescript ?? true;
|
|
166
|
+
if (options.typescript === void 0 && !nonInteractive) typescript = await question("Language [1=TypeScript (recommended), 2=JavaScript]: ") !== "2";
|
|
167
|
+
let edge = options.edge ?? false;
|
|
168
|
+
if (options.edge === void 0 && !nonInteractive) edge = await question("Deployment target [1=Node.js Server (default), 2=Edge/Serverless]: ") === "2";
|
|
169
|
+
return {
|
|
170
|
+
name,
|
|
171
|
+
adapter,
|
|
172
|
+
auth,
|
|
173
|
+
tenant,
|
|
174
|
+
typescript,
|
|
175
|
+
edge
|
|
176
|
+
};
|
|
177
|
+
} finally {
|
|
178
|
+
rl.close();
|
|
179
|
+
}
|
|
174
180
|
}
|
|
175
181
|
async function createProjectStructure(projectPath, config) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
244
|
-
await fs.writeFile(fullPath, content);
|
|
245
|
-
console.log(` ✅ Created: ${filePath}`);
|
|
246
|
-
}
|
|
182
|
+
const ext = config.typescript ? "ts" : "js";
|
|
183
|
+
const dirs = [
|
|
184
|
+
"",
|
|
185
|
+
"src",
|
|
186
|
+
"src/config",
|
|
187
|
+
"src/shared",
|
|
188
|
+
"src/shared/presets",
|
|
189
|
+
"src/plugins",
|
|
190
|
+
"src/resources",
|
|
191
|
+
...config.auth === "jwt" ? ["src/resources/user", "src/resources/auth"] : [],
|
|
192
|
+
"src/resources/example",
|
|
193
|
+
"tests"
|
|
194
|
+
];
|
|
195
|
+
for (const dir of dirs) {
|
|
196
|
+
await fs.mkdir(path.join(projectPath, dir), { recursive: true });
|
|
197
|
+
console.log(` + Created: ${dir || "/"}`);
|
|
198
|
+
}
|
|
199
|
+
const files = {
|
|
200
|
+
"package.json": packageJsonTemplate(config),
|
|
201
|
+
".gitignore": gitignoreTemplate(),
|
|
202
|
+
".env.example": envExampleTemplate(config),
|
|
203
|
+
".env.dev": envDevTemplate(config),
|
|
204
|
+
"README.md": readmeTemplate(config)
|
|
205
|
+
};
|
|
206
|
+
if (config.typescript) files["tsconfig.json"] = tsconfigTemplate();
|
|
207
|
+
files["vitest.config.ts"] = vitestConfigTemplate(config);
|
|
208
|
+
files[`src/config/env.${ext}`] = envLoaderTemplate(config);
|
|
209
|
+
files[`src/config/index.${ext}`] = configTemplate(config);
|
|
210
|
+
files[`src/app.${ext}`] = appTemplate(config);
|
|
211
|
+
files[`src/index.${ext}`] = indexTemplate(config);
|
|
212
|
+
files[`src/shared/index.${ext}`] = sharedIndexTemplate(config);
|
|
213
|
+
files[`src/shared/adapter.${ext}`] = config.adapter === "mongokit" ? createAdapterTemplate(config) : customAdapterTemplate(config);
|
|
214
|
+
files[`src/shared/permissions.${ext}`] = permissionsTemplate(config);
|
|
215
|
+
if (config.tenant === "multi") {
|
|
216
|
+
files[`src/shared/presets/index.${ext}`] = presetsMultiTenantTemplate(config);
|
|
217
|
+
files[`src/shared/presets/flexible-multi-tenant.${ext}`] = flexibleMultiTenantPresetTemplate(config);
|
|
218
|
+
} else files[`src/shared/presets/index.${ext}`] = presetsSingleTenantTemplate(config);
|
|
219
|
+
files[`src/plugins/index.${ext}`] = pluginsIndexTemplate(config);
|
|
220
|
+
files[`src/resources/index.${ext}`] = resourcesIndexTemplate(config);
|
|
221
|
+
if (config.auth === "better-auth") files[`src/auth.${ext}`] = betterAuthSetupTemplate(config);
|
|
222
|
+
else {
|
|
223
|
+
files[`src/resources/user/user.model.${ext}`] = userModelTemplate(config);
|
|
224
|
+
files[`src/resources/user/user.repository.${ext}`] = userRepositoryTemplate(config);
|
|
225
|
+
files[`src/resources/user/user.controller.${ext}`] = userControllerTemplate(config);
|
|
226
|
+
files[`src/resources/auth/auth.resource.${ext}`] = authResourceTemplate(config);
|
|
227
|
+
files[`src/resources/auth/auth.handlers.${ext}`] = authHandlersTemplate(config);
|
|
228
|
+
files[`src/resources/auth/auth.schemas.${ext}`] = authSchemasTemplate(config);
|
|
229
|
+
}
|
|
230
|
+
files[`src/resources/example/example.model.${ext}`] = exampleModelTemplate(config);
|
|
231
|
+
files[`src/resources/example/example.repository.${ext}`] = exampleRepositoryTemplate(config);
|
|
232
|
+
files[`src/resources/example/example.resource.${ext}`] = exampleResourceTemplate(config);
|
|
233
|
+
files[`src/resources/example/example.controller.${ext}`] = exampleControllerTemplate(config);
|
|
234
|
+
files[`src/resources/example/example.schemas.${ext}`] = exampleSchemasTemplate(config);
|
|
235
|
+
files[`tests/example.test.${ext}`] = exampleTestTemplate(config);
|
|
236
|
+
if (config.auth === "jwt") files[`tests/auth.test.${ext}`] = authTestTemplate(config);
|
|
237
|
+
files[".arcrc"] = JSON.stringify({
|
|
238
|
+
adapter: config.adapter,
|
|
239
|
+
auth: config.auth,
|
|
240
|
+
tenant: config.tenant,
|
|
241
|
+
typescript: config.typescript
|
|
242
|
+
}, null, 2) + "\n";
|
|
243
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
244
|
+
const fullPath = path.join(projectPath, filePath);
|
|
245
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
246
|
+
await fs.writeFile(fullPath, content);
|
|
247
|
+
console.log(` + Created: ${filePath}`);
|
|
248
|
+
}
|
|
247
249
|
}
|
|
248
250
|
function packageJsonTemplate(config) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
node: ">=20"
|
|
282
|
-
}
|
|
283
|
-
},
|
|
284
|
-
null,
|
|
285
|
-
2
|
|
286
|
-
);
|
|
251
|
+
const scripts = config.typescript ? {
|
|
252
|
+
dev: "tsx watch src/index.ts",
|
|
253
|
+
build: "tsc",
|
|
254
|
+
start: "node dist/index.js",
|
|
255
|
+
test: "vitest run",
|
|
256
|
+
"test:watch": "vitest"
|
|
257
|
+
} : {
|
|
258
|
+
dev: "node --watch src/index.js",
|
|
259
|
+
start: "node src/index.js",
|
|
260
|
+
test: "vitest run",
|
|
261
|
+
"test:watch": "vitest"
|
|
262
|
+
};
|
|
263
|
+
const imports = config.typescript ? {
|
|
264
|
+
"#config/*": "./dist/config/*",
|
|
265
|
+
"#shared/*": "./dist/shared/*",
|
|
266
|
+
"#resources/*": "./dist/resources/*",
|
|
267
|
+
"#plugins/*": "./dist/plugins/*"
|
|
268
|
+
} : {
|
|
269
|
+
"#config/*": "./src/config/*",
|
|
270
|
+
"#shared/*": "./src/shared/*",
|
|
271
|
+
"#resources/*": "./src/resources/*",
|
|
272
|
+
"#plugins/*": "./src/plugins/*"
|
|
273
|
+
};
|
|
274
|
+
return JSON.stringify({
|
|
275
|
+
name: config.name,
|
|
276
|
+
version: "1.0.0",
|
|
277
|
+
type: "module",
|
|
278
|
+
main: config.typescript ? "dist/index.js" : "src/index.js",
|
|
279
|
+
imports,
|
|
280
|
+
scripts,
|
|
281
|
+
engines: { node: ">=20" }
|
|
282
|
+
}, null, 2);
|
|
287
283
|
}
|
|
288
284
|
function tsconfigTemplate() {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
},
|
|
316
|
-
null,
|
|
317
|
-
2
|
|
318
|
-
);
|
|
285
|
+
return JSON.stringify({
|
|
286
|
+
compilerOptions: {
|
|
287
|
+
target: "ES2022",
|
|
288
|
+
module: "NodeNext",
|
|
289
|
+
moduleResolution: "NodeNext",
|
|
290
|
+
lib: ["ES2022"],
|
|
291
|
+
outDir: "./dist",
|
|
292
|
+
rootDir: "./src",
|
|
293
|
+
strict: true,
|
|
294
|
+
esModuleInterop: true,
|
|
295
|
+
skipLibCheck: true,
|
|
296
|
+
forceConsistentCasingInFileNames: true,
|
|
297
|
+
declaration: true,
|
|
298
|
+
declarationMap: true,
|
|
299
|
+
sourceMap: true,
|
|
300
|
+
resolveJsonModule: true,
|
|
301
|
+
paths: {
|
|
302
|
+
"#shared/*": ["./src/shared/*"],
|
|
303
|
+
"#resources/*": ["./src/resources/*"],
|
|
304
|
+
"#config/*": ["./src/config/*"],
|
|
305
|
+
"#plugins/*": ["./src/plugins/*"]
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
include: ["src/**/*"],
|
|
309
|
+
exclude: ["node_modules", "dist"]
|
|
310
|
+
}, null, 2);
|
|
319
311
|
}
|
|
320
312
|
function vitestConfigTemplate(config) {
|
|
321
|
-
|
|
322
|
-
|
|
313
|
+
const srcDir = config.typescript ? "./src" : "./src";
|
|
314
|
+
return `import { defineConfig } from 'vitest/config';
|
|
323
315
|
import { resolve } from 'path';
|
|
324
316
|
|
|
325
317
|
export default defineConfig({
|
|
@@ -339,7 +331,7 @@ export default defineConfig({
|
|
|
339
331
|
`;
|
|
340
332
|
}
|
|
341
333
|
function gitignoreTemplate() {
|
|
342
|
-
|
|
334
|
+
return `# Dependencies
|
|
343
335
|
node_modules/
|
|
344
336
|
|
|
345
337
|
# Build
|
|
@@ -370,31 +362,45 @@ coverage/
|
|
|
370
362
|
`;
|
|
371
363
|
}
|
|
372
364
|
function envExampleTemplate(config) {
|
|
373
|
-
|
|
365
|
+
let content = `# Server
|
|
374
366
|
PORT=8040
|
|
375
367
|
HOST=0.0.0.0
|
|
376
368
|
NODE_ENV=development
|
|
377
|
-
|
|
369
|
+
`;
|
|
370
|
+
if (config.auth === "better-auth") content += `
|
|
371
|
+
# Better Auth
|
|
372
|
+
BETTER_AUTH_SECRET=your-32-character-minimum-secret-here
|
|
373
|
+
FRONTEND_URL=http://localhost:3000
|
|
374
|
+
|
|
375
|
+
# Google OAuth (optional)
|
|
376
|
+
# GOOGLE_CLIENT_ID=
|
|
377
|
+
# GOOGLE_CLIENT_SECRET=
|
|
378
|
+
`;
|
|
379
|
+
else content += `
|
|
378
380
|
# JWT
|
|
379
381
|
JWT_SECRET=your-32-character-minimum-secret-here
|
|
382
|
+
JWT_EXPIRES_IN=7d
|
|
380
383
|
`;
|
|
381
|
-
|
|
382
|
-
|
|
384
|
+
content += `
|
|
385
|
+
# CORS - Allowed origins
|
|
386
|
+
# Options:
|
|
387
|
+
# * = allow all origins (not recommended for production)
|
|
388
|
+
# Comma-separated list = specific origins only
|
|
389
|
+
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
|
|
390
|
+
`;
|
|
391
|
+
if (config.adapter === "mongokit") content += `
|
|
383
392
|
# MongoDB
|
|
384
393
|
MONGODB_URI=mongodb://localhost:27017/${config.name}
|
|
385
394
|
`;
|
|
386
|
-
|
|
387
|
-
if (config.tenant === "multi") {
|
|
388
|
-
content += `
|
|
395
|
+
if (config.tenant === "multi") content += `
|
|
389
396
|
# Multi-tenant
|
|
390
|
-
|
|
397
|
+
ORG_HEADER=x-organization-id
|
|
391
398
|
`;
|
|
392
|
-
|
|
393
|
-
return content;
|
|
399
|
+
return content;
|
|
394
400
|
}
|
|
395
401
|
function readmeTemplate(config) {
|
|
396
|
-
|
|
397
|
-
|
|
402
|
+
const ext = config.typescript ? "ts" : "js";
|
|
403
|
+
return `# ${config.name}
|
|
398
404
|
|
|
399
405
|
Built with [Arc](https://github.com/classytic/arc) - Resource-Oriented Backend Framework
|
|
400
406
|
|
|
@@ -500,9 +506,9 @@ arc docs
|
|
|
500
506
|
|
|
501
507
|
## Environment Files
|
|
502
508
|
|
|
503
|
-
- \`.env.dev\` - Development (default)
|
|
504
|
-
- \`.env.test\` - Testing
|
|
505
|
-
- \`.env.prod\` - Production
|
|
509
|
+
- \`.env.development\` / \`.env.dev\` - Development (default)
|
|
510
|
+
- \`.env.test\` / \`.env.qa\` - Testing / QA
|
|
511
|
+
- \`.env.production\` / \`.env.prod\` - Production
|
|
506
512
|
- \`.env\` - Fallback
|
|
507
513
|
|
|
508
514
|
## API Documentation
|
|
@@ -526,8 +532,8 @@ API documentation is available via Scalar UI:
|
|
|
526
532
|
`;
|
|
527
533
|
}
|
|
528
534
|
function indexTemplate(config) {
|
|
529
|
-
|
|
530
|
-
|
|
535
|
+
const ts = config.typescript;
|
|
536
|
+
return `/**
|
|
531
537
|
* ${config.name} - Server Entry Point
|
|
532
538
|
* Generated by Arc CLI
|
|
533
539
|
*
|
|
@@ -543,30 +549,37 @@ ${config.adapter === "mongokit" ? "import mongoose from 'mongoose';" : ""}
|
|
|
543
549
|
import { createAppInstance } from './app.js';
|
|
544
550
|
|
|
545
551
|
async function main()${ts ? ": Promise<void>" : ""} {
|
|
546
|
-
console.log(
|
|
552
|
+
console.log(\`Environment: \${config.env}\`);
|
|
547
553
|
${config.adapter === "mongokit" ? `
|
|
548
554
|
// Connect to MongoDB
|
|
549
555
|
await mongoose.connect(config.database.uri);
|
|
550
|
-
console.log('
|
|
556
|
+
console.log('Connected to MongoDB');
|
|
551
557
|
` : ""}
|
|
552
558
|
// Create and configure app
|
|
553
559
|
const app = await createAppInstance();
|
|
554
560
|
|
|
555
561
|
// Start server
|
|
556
562
|
await app.listen({ port: config.server.port, host: config.server.host });
|
|
557
|
-
console.log(
|
|
563
|
+
console.log(\`Server running at http://\${config.server.host}:\${config.server.port}\`);
|
|
558
564
|
}
|
|
559
565
|
|
|
560
566
|
main().catch((err) => {
|
|
561
|
-
console.error('
|
|
567
|
+
console.error('Failed to start server:', err);
|
|
562
568
|
process.exit(1);
|
|
563
569
|
});
|
|
564
570
|
`;
|
|
565
571
|
}
|
|
566
572
|
function appTemplate(config) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
573
|
+
const ts = config.typescript;
|
|
574
|
+
const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
|
|
575
|
+
const betterAuthImport = config.auth === "better-auth" ? `import { createBetterAuthAdapter } from '@classytic/arc/auth';
|
|
576
|
+
import { getAuth } from './auth.js';
|
|
577
|
+
` : "";
|
|
578
|
+
const authConfig = config.auth === "better-auth" ? config.tenant === "multi" ? `auth: { type: 'betterAuth', betterAuth: createBetterAuthAdapter({ auth: getAuth(), orgContext: true }) },` : `auth: { type: 'betterAuth', betterAuth: createBetterAuthAdapter({ auth: getAuth() }) },` : `auth: {
|
|
579
|
+
type: 'jwt',
|
|
580
|
+
jwt: { secret: config.jwt.secret },
|
|
581
|
+
},`;
|
|
582
|
+
return `/**
|
|
570
583
|
* ${config.name} - App Factory
|
|
571
584
|
* Generated by Arc CLI
|
|
572
585
|
*
|
|
@@ -579,7 +592,7 @@ function appTemplate(config) {
|
|
|
579
592
|
|
|
580
593
|
${typeImport}import config from '#config/index.js';
|
|
581
594
|
import { createApp } from '@classytic/arc/factory';
|
|
582
|
-
|
|
595
|
+
${betterAuthImport}
|
|
583
596
|
// App-specific plugins
|
|
584
597
|
import { registerPlugins } from '#plugins/index.js';
|
|
585
598
|
|
|
@@ -594,16 +607,15 @@ import { registerResources } from '#resources/index.js';
|
|
|
594
607
|
export async function createAppInstance()${ts ? ": Promise<FastifyInstance>" : ""} {
|
|
595
608
|
// Create Arc app with base configuration
|
|
596
609
|
const app = await createApp({
|
|
597
|
-
preset: config.env === 'production' ? 'production' : 'development',
|
|
598
|
-
|
|
599
|
-
jwt: { secret: config.jwt.secret },
|
|
600
|
-
},
|
|
610
|
+
preset: config.env === 'production' ? (${config.edge ? "'edge'" : "'production'"}) : 'development',
|
|
611
|
+
${authConfig}
|
|
601
612
|
cors: {
|
|
602
613
|
origin: config.cors.origins,
|
|
603
614
|
methods: config.cors.methods,
|
|
604
615
|
allowedHeaders: config.cors.allowedHeaders,
|
|
605
616
|
credentials: config.cors.credentials,
|
|
606
617
|
},
|
|
618
|
+
trustProxy: true,
|
|
607
619
|
});
|
|
608
620
|
|
|
609
621
|
// Register app-specific plugins (explicit dependency injection)
|
|
@@ -619,8 +631,8 @@ export default createAppInstance;
|
|
|
619
631
|
`;
|
|
620
632
|
}
|
|
621
633
|
function envLoaderTemplate(config) {
|
|
622
|
-
|
|
623
|
-
|
|
634
|
+
const ts = config.typescript;
|
|
635
|
+
return `/**
|
|
624
636
|
* Environment Loader
|
|
625
637
|
*
|
|
626
638
|
* MUST be imported FIRST before any other imports.
|
|
@@ -653,12 +665,12 @@ const defaultEnvFile = resolve(process.cwd(), '.env');
|
|
|
653
665
|
|
|
654
666
|
if (existsSync(envFile)) {
|
|
655
667
|
dotenv.config({ path: envFile });
|
|
656
|
-
console.log(
|
|
668
|
+
console.log(\`Loaded: .env.\${env}\`);
|
|
657
669
|
} else if (existsSync(defaultEnvFile)) {
|
|
658
670
|
dotenv.config({ path: defaultEnvFile });
|
|
659
|
-
console.log('
|
|
671
|
+
console.log('Loaded: .env');
|
|
660
672
|
} else {
|
|
661
|
-
console.warn('
|
|
673
|
+
console.warn('Warning: No .env file found');
|
|
662
674
|
}
|
|
663
675
|
|
|
664
676
|
// Export for reference
|
|
@@ -666,43 +678,50 @@ export const ENV = env;
|
|
|
666
678
|
`;
|
|
667
679
|
}
|
|
668
680
|
function envDevTemplate(config) {
|
|
669
|
-
|
|
681
|
+
let content = `# Development Environment
|
|
670
682
|
NODE_ENV=development
|
|
671
683
|
|
|
672
684
|
# Server
|
|
673
685
|
PORT=8040
|
|
674
686
|
HOST=0.0.0.0
|
|
675
|
-
|
|
687
|
+
`;
|
|
688
|
+
if (config.auth === "better-auth") content += `
|
|
689
|
+
# Better Auth
|
|
690
|
+
BETTER_AUTH_SECRET=dev-secret-change-in-production-min-32-chars
|
|
691
|
+
FRONTEND_URL=http://localhost:3000
|
|
692
|
+
|
|
693
|
+
# Google OAuth (optional — leave empty to disable)
|
|
694
|
+
GOOGLE_CLIENT_ID=
|
|
695
|
+
GOOGLE_CLIENT_SECRET=
|
|
696
|
+
`;
|
|
697
|
+
else content += `
|
|
676
698
|
# JWT
|
|
677
699
|
JWT_SECRET=dev-secret-change-in-production-min-32-chars
|
|
678
700
|
JWT_EXPIRES_IN=7d
|
|
679
|
-
|
|
701
|
+
`;
|
|
702
|
+
content += `
|
|
680
703
|
# CORS - Allowed origins
|
|
681
704
|
# Options:
|
|
682
705
|
# * = allow all origins (not recommended for production)
|
|
683
706
|
# Comma-separated list = specific origins only
|
|
684
707
|
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
|
|
685
708
|
`;
|
|
686
|
-
|
|
687
|
-
content += `
|
|
709
|
+
if (config.adapter === "mongokit") content += `
|
|
688
710
|
# MongoDB
|
|
689
711
|
MONGODB_URI=mongodb://localhost:27017/${config.name}
|
|
690
712
|
`;
|
|
691
|
-
|
|
692
|
-
if (config.tenant === "multi") {
|
|
693
|
-
content += `
|
|
713
|
+
if (config.tenant === "multi") content += `
|
|
694
714
|
# Multi-tenant
|
|
695
715
|
ORG_HEADER=x-organization-id
|
|
696
716
|
`;
|
|
697
|
-
|
|
698
|
-
return content;
|
|
717
|
+
return content;
|
|
699
718
|
}
|
|
700
719
|
function pluginsIndexTemplate(config) {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
720
|
+
const ts = config.typescript;
|
|
721
|
+
const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
|
|
722
|
+
const configType = ts ? ": { config: AppConfig }" : "";
|
|
723
|
+
const appType = ts ? ": FastifyInstance" : "";
|
|
724
|
+
let content = `/**
|
|
706
725
|
* App Plugins Registry
|
|
707
726
|
*
|
|
708
727
|
* Register your app-specific plugins here.
|
|
@@ -710,12 +729,9 @@ function pluginsIndexTemplate(config) {
|
|
|
710
729
|
*/
|
|
711
730
|
|
|
712
731
|
${typeImport}${ts ? "import type { AppConfig } from '../config/index.js';\n" : ""}import { openApiPlugin, scalarPlugin } from '@classytic/arc/docs';
|
|
732
|
+
import { errorHandlerPlugin } from '@classytic/arc/plugins';
|
|
713
733
|
`;
|
|
714
|
-
|
|
715
|
-
content += `import { orgScopePlugin } from '@classytic/arc/org';
|
|
716
|
-
`;
|
|
717
|
-
}
|
|
718
|
-
content += `
|
|
734
|
+
content += `
|
|
719
735
|
/**
|
|
720
736
|
* Register all app-specific plugins
|
|
721
737
|
*
|
|
@@ -728,6 +744,11 @@ export async function registerPlugins(
|
|
|
728
744
|
)${ts ? ": Promise<void>" : ""} {
|
|
729
745
|
const { config } = deps;
|
|
730
746
|
|
|
747
|
+
// Error handling (CastError → 400, validation → 422, duplicate → 409)
|
|
748
|
+
await app.register(errorHandlerPlugin, {
|
|
749
|
+
includeStack: config.isDev,
|
|
750
|
+
});
|
|
751
|
+
|
|
731
752
|
// API Documentation (Scalar UI)
|
|
732
753
|
// OpenAPI spec: /_docs/openapi.json
|
|
733
754
|
// Scalar UI: /docs
|
|
@@ -735,43 +756,37 @@ export async function registerPlugins(
|
|
|
735
756
|
title: '${config.name} API',
|
|
736
757
|
version: '1.0.0',
|
|
737
758
|
description: 'API documentation for ${config.name}',
|
|
759
|
+
apiPrefix: '/api',
|
|
738
760
|
});
|
|
739
761
|
await app.register(scalarPlugin, {
|
|
740
762
|
routePrefix: '/docs',
|
|
741
763
|
theme: 'default',
|
|
742
764
|
});
|
|
743
|
-
|
|
744
|
-
if (config.tenant === "multi") {
|
|
745
|
-
content += `
|
|
746
|
-
// Multi-tenant org scope
|
|
747
|
-
await app.register(orgScopePlugin, {
|
|
748
|
-
header: config.org?.header || 'x-organization-id',
|
|
749
|
-
bypassRoles: ['superadmin', 'admin'],
|
|
750
|
-
});
|
|
751
|
-
`;
|
|
752
|
-
}
|
|
753
|
-
content += `
|
|
765
|
+
|
|
754
766
|
// Add your custom plugins here:
|
|
755
767
|
// await app.register(myCustomPlugin, { ...options });
|
|
756
768
|
}
|
|
757
769
|
`;
|
|
758
|
-
|
|
770
|
+
return content;
|
|
759
771
|
}
|
|
760
772
|
function resourcesIndexTemplate(config) {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
773
|
+
const ts = config.typescript;
|
|
774
|
+
const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
|
|
775
|
+
const appType = ts ? ": FastifyInstance" : "";
|
|
776
|
+
return `/**
|
|
765
777
|
* Resources Registry
|
|
766
778
|
*
|
|
767
779
|
* Central registry for all API resources.
|
|
768
|
-
*
|
|
780
|
+
* All resources are mounted under /api prefix via Fastify scoping.
|
|
769
781
|
*/
|
|
770
782
|
|
|
771
|
-
${typeImport}
|
|
783
|
+
${typeImport}${config.auth === "jwt" ? `
|
|
772
784
|
// Auth resources (register, login, /users/me)
|
|
773
785
|
import { authResource, userProfileResource } from './auth/auth.resource.js';
|
|
774
|
-
|
|
786
|
+
` : `
|
|
787
|
+
// Auth is handled by Better Auth — routes at /api/auth/*
|
|
788
|
+
// No manual auth resource needed.
|
|
789
|
+
`}
|
|
775
790
|
// App resources
|
|
776
791
|
import exampleResource from './example/example.resource.js';
|
|
777
792
|
|
|
@@ -782,24 +797,27 @@ import exampleResource from './example/example.resource.js';
|
|
|
782
797
|
* All registered resources
|
|
783
798
|
*/
|
|
784
799
|
export const resources = [
|
|
785
|
-
authResource,
|
|
800
|
+
${config.auth === "jwt" ? ` authResource,
|
|
786
801
|
userProfileResource,
|
|
787
|
-
exampleResource,
|
|
802
|
+
` : ` `}exampleResource,
|
|
788
803
|
]${ts ? " as const" : ""};
|
|
789
804
|
|
|
790
805
|
/**
|
|
791
|
-
* Register all resources with the app
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
806
|
+
* Register all resources with the app under a common prefix.
|
|
807
|
+
* Fastify scoping ensures all routes are mounted at /api/*.
|
|
808
|
+
* The apiPrefix option in openApiPlugin keeps OpenAPI docs in sync.
|
|
809
|
+
*/
|
|
810
|
+
export async function registerResources(app${appType}, prefix = '/api')${ts ? ": Promise<void>" : ""} {
|
|
811
|
+
await app.register(async (scope) => {
|
|
812
|
+
for (const resource of resources) {
|
|
813
|
+
await scope.register(resource.toPlugin());
|
|
814
|
+
}
|
|
815
|
+
}, { prefix });
|
|
797
816
|
}
|
|
798
817
|
`;
|
|
799
818
|
}
|
|
800
|
-
function sharedIndexTemplate(
|
|
801
|
-
|
|
802
|
-
return `/**
|
|
819
|
+
function sharedIndexTemplate(_config) {
|
|
820
|
+
return `/**
|
|
803
821
|
* Shared Utilities
|
|
804
822
|
*
|
|
805
823
|
* Central exports for resource definitions.
|
|
@@ -812,19 +830,7 @@ export { createAdapter } from './adapter.js';
|
|
|
812
830
|
// Core Arc exports
|
|
813
831
|
export { createMongooseAdapter, defineResource } from '@classytic/arc';
|
|
814
832
|
|
|
815
|
-
// Permission helpers
|
|
816
|
-
export {
|
|
817
|
-
allowPublic,
|
|
818
|
-
requireAuth,
|
|
819
|
-
requireRoles,
|
|
820
|
-
requireOwnership,
|
|
821
|
-
allOf,
|
|
822
|
-
anyOf,
|
|
823
|
-
denyAll,
|
|
824
|
-
when,${ts ? "\n type PermissionCheck," : ""}
|
|
825
|
-
} from '@classytic/arc/permissions';
|
|
826
|
-
|
|
827
|
-
// Application permissions
|
|
833
|
+
// Permission helpers (core + application-level)
|
|
828
834
|
export * from './permissions.js';
|
|
829
835
|
|
|
830
836
|
// Presets
|
|
@@ -832,8 +838,8 @@ export * from './presets/index.js';
|
|
|
832
838
|
`;
|
|
833
839
|
}
|
|
834
840
|
function createAdapterTemplate(config) {
|
|
835
|
-
|
|
836
|
-
|
|
841
|
+
const ts = config.typescript;
|
|
842
|
+
return `/**
|
|
837
843
|
* MongoKit Adapter Factory
|
|
838
844
|
*
|
|
839
845
|
* Creates Arc adapters using MongoKit repositories.
|
|
@@ -849,10 +855,10 @@ ${ts ? "import type { Model } from 'mongoose';\nimport type { Repository } from
|
|
|
849
855
|
* Note: Query parsing is handled by MongoKit's Repository class.
|
|
850
856
|
* Just pass the model and repository - Arc handles the rest.
|
|
851
857
|
*/
|
|
852
|
-
export function createAdapter${ts ? "<TDoc
|
|
858
|
+
export function createAdapter${ts ? "<TDoc = any>" : ""}(
|
|
853
859
|
model${ts ? ": Model<TDoc>" : ""},
|
|
854
|
-
repository${ts ? ":
|
|
855
|
-
)
|
|
860
|
+
repository${ts ? ": Repository<TDoc>" : ""}
|
|
861
|
+
) {
|
|
856
862
|
return createMongooseAdapter({
|
|
857
863
|
model,
|
|
858
864
|
repository,
|
|
@@ -861,8 +867,8 @@ export function createAdapter${ts ? "<TDoc, TRepo extends Repository<TDoc>>" : "
|
|
|
861
867
|
`;
|
|
862
868
|
}
|
|
863
869
|
function customAdapterTemplate(config) {
|
|
864
|
-
|
|
865
|
-
|
|
870
|
+
const ts = config.typescript;
|
|
871
|
+
return `/**
|
|
866
872
|
* Custom Adapter Factory
|
|
867
873
|
*
|
|
868
874
|
* Implement your own database adapter here.
|
|
@@ -883,7 +889,7 @@ export function createAdapter${ts ? "<TDoc>" : ""}(
|
|
|
883
889
|
model${ts ? ": Model<TDoc>" : ""},
|
|
884
890
|
repository${ts ? ": any" : ""}
|
|
885
891
|
)${ts ? ": ReturnType<typeof createMongooseAdapter>" : ""} {
|
|
886
|
-
//
|
|
892
|
+
// SCAFFOLD: Replace with your custom adapter implementation
|
|
887
893
|
return createMongooseAdapter({
|
|
888
894
|
model,
|
|
889
895
|
repository,
|
|
@@ -892,8 +898,7 @@ export function createAdapter${ts ? "<TDoc>" : ""}(
|
|
|
892
898
|
`;
|
|
893
899
|
}
|
|
894
900
|
function presetsMultiTenantTemplate(config) {
|
|
895
|
-
|
|
896
|
-
return `/**
|
|
901
|
+
return `/**
|
|
897
902
|
* Arc Presets - Multi-Tenant Configuration
|
|
898
903
|
*
|
|
899
904
|
* Pre-configured presets for multi-tenant applications.
|
|
@@ -917,7 +922,6 @@ export { flexibleMultiTenantPreset } from './flexible-multi-tenant.js';
|
|
|
917
922
|
*/
|
|
918
923
|
export const orgScoped = multiTenantPreset({
|
|
919
924
|
tenantField: 'organizationId',
|
|
920
|
-
bypassRoles: ['superadmin', 'admin'],
|
|
921
925
|
});
|
|
922
926
|
|
|
923
927
|
/**
|
|
@@ -955,14 +959,13 @@ export const presets = {
|
|
|
955
959
|
ownedByUser,
|
|
956
960
|
softDelete,
|
|
957
961
|
slugLookup,
|
|
958
|
-
}${
|
|
962
|
+
}${config.typescript ? " as const" : ""};
|
|
959
963
|
|
|
960
964
|
export default presets;
|
|
961
965
|
`;
|
|
962
966
|
}
|
|
963
967
|
function presetsSingleTenantTemplate(config) {
|
|
964
|
-
|
|
965
|
-
return `/**
|
|
968
|
+
return `/**
|
|
966
969
|
* Arc Presets - Single-Tenant Configuration
|
|
967
970
|
*
|
|
968
971
|
* Pre-configured presets for single-tenant applications.
|
|
@@ -1008,18 +1011,31 @@ export const presets = {
|
|
|
1008
1011
|
ownedByUser,
|
|
1009
1012
|
softDelete,
|
|
1010
1013
|
slugLookup,
|
|
1011
|
-
}${
|
|
1014
|
+
}${config.typescript ? " as const" : ""};
|
|
1012
1015
|
|
|
1013
1016
|
export default presets;
|
|
1014
1017
|
`;
|
|
1015
1018
|
}
|
|
1016
1019
|
function flexibleMultiTenantPresetTemplate(config) {
|
|
1017
|
-
|
|
1018
|
-
|
|
1020
|
+
const ts = config.typescript;
|
|
1021
|
+
return `/**
|
|
1022
|
+
* Flexible Multi-Tenant Preset
|
|
1023
|
+
*
|
|
1024
|
+
* Smarter tenant filtering that works with public + authenticated routes.
|
|
1025
|
+
*
|
|
1026
|
+
* Philosophy:
|
|
1027
|
+
* - No org scope → No filtering (public data, all orgs)
|
|
1028
|
+
* - Org scope present → Filter by org
|
|
1029
|
+
* - Elevated scope → No filter (platform admin sees all)
|
|
1030
|
+
*
|
|
1031
|
+
* Uses request.scope (RequestScope) from Arc's scope system.
|
|
1032
|
+
*/
|
|
1033
|
+
${ts ? `
|
|
1034
|
+
import { getOrgId, isElevated, isMember } from '@classytic/arc/scope';
|
|
1035
|
+
import type { RequestScope } from '@classytic/arc/scope';
|
|
1036
|
+
|
|
1019
1037
|
interface FlexibleMultiTenantOptions {
|
|
1020
1038
|
tenantField?: string;
|
|
1021
|
-
bypassRoles?: string[];
|
|
1022
|
-
extractOrganizationId?: (request: any) => string | null;
|
|
1023
1039
|
}
|
|
1024
1040
|
|
|
1025
1041
|
interface PresetMiddlewares {
|
|
@@ -1035,101 +1051,47 @@ interface Preset {
|
|
|
1035
1051
|
name: string;
|
|
1036
1052
|
middlewares: PresetMiddlewares;
|
|
1037
1053
|
}
|
|
1038
|
-
` :
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
*
|
|
1042
|
-
* Smarter tenant filtering that works with public + authenticated routes.
|
|
1043
|
-
*
|
|
1044
|
-
* Philosophy:
|
|
1045
|
-
* - No org header → No filtering (public data, all orgs)
|
|
1046
|
-
* - Org header present → Require auth, filter by org
|
|
1047
|
-
*
|
|
1048
|
-
* This differs from Arc's strict multiTenant which always requires auth.
|
|
1049
|
-
*/
|
|
1050
|
-
${typeAnnotations}
|
|
1054
|
+
` : `
|
|
1055
|
+
const { getOrgId, isElevated, isMember } = require('@classytic/arc/scope');
|
|
1056
|
+
`}
|
|
1051
1057
|
/**
|
|
1052
|
-
*
|
|
1053
|
-
*
|
|
1058
|
+
* Create flexible tenant filter middleware.
|
|
1059
|
+
* Only filters when org context is present.
|
|
1054
1060
|
*/
|
|
1055
|
-
function
|
|
1056
|
-
// Priority 1: Explicit context (set by org-scope plugin)
|
|
1057
|
-
if (request.context?.organizationId) {
|
|
1058
|
-
return String(request.context.organizationId);
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
// Priority 2: User's organizationId field
|
|
1062
|
-
if (request.user?.organizationId) {
|
|
1063
|
-
return String(request.user.organizationId);
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// Priority 3: User's organization object (nested)
|
|
1067
|
-
if (request.user?.organization) {
|
|
1068
|
-
const org = request.user.organization;
|
|
1069
|
-
return String(org._id || org.id || org);
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
return null;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
/**
|
|
1076
|
-
* Create flexible tenant filter middleware
|
|
1077
|
-
* Only filters when org context is present
|
|
1078
|
-
*/
|
|
1079
|
-
function createFlexibleTenantFilter(
|
|
1080
|
-
tenantField${ts ? ": string" : ""},
|
|
1081
|
-
bypassRoles${ts ? ": string[]" : ""},
|
|
1082
|
-
extractOrganizationId${ts ? ": (request: any) => string | null" : ""}
|
|
1083
|
-
) {
|
|
1061
|
+
function createFlexibleTenantFilter(tenantField${ts ? ": string" : ""}) {
|
|
1084
1062
|
return async (request${ts ? ": any" : ""}, reply${ts ? ": any" : ""}) => {
|
|
1085
|
-
const
|
|
1086
|
-
const orgId = extractOrganizationId(request);
|
|
1063
|
+
const scope${ts ? ": RequestScope" : ""} = request.scope ?? { kind: 'public' };
|
|
1087
1064
|
|
|
1088
|
-
//
|
|
1089
|
-
if (
|
|
1090
|
-
request.log?.debug?.({ msg: '
|
|
1065
|
+
// Elevated scope — platform admin sees all, no filter
|
|
1066
|
+
if (isElevated(scope)) {
|
|
1067
|
+
request.log?.debug?.({ msg: 'Elevated scope — no tenant filter' });
|
|
1091
1068
|
return;
|
|
1092
1069
|
}
|
|
1093
1070
|
|
|
1094
|
-
//
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
request.
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
});
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
// Bypass roles skip filter (superadmin sees all)
|
|
1106
|
-
const userRoles = Array.isArray(user.roles) ? user.roles : [];
|
|
1107
|
-
if (bypassRoles.some((r${ts ? ": string" : ""}) => userRoles.includes(r))) {
|
|
1108
|
-
request.log?.debug?.({ msg: 'Bypass role - no tenant filter' });
|
|
1071
|
+
// Member scope — filter by org
|
|
1072
|
+
if (isMember(scope)) {
|
|
1073
|
+
request.query = request.query ?? {};
|
|
1074
|
+
request.query._policyFilters = {
|
|
1075
|
+
...(request.query._policyFilters ?? {}),
|
|
1076
|
+
[tenantField]: scope.organizationId,
|
|
1077
|
+
};
|
|
1078
|
+
request.log?.debug?.({ msg: 'Tenant filter applied', orgId: scope.organizationId, tenantField });
|
|
1109
1079
|
return;
|
|
1110
1080
|
}
|
|
1111
1081
|
|
|
1112
|
-
//
|
|
1113
|
-
request.
|
|
1114
|
-
request.query._policyFilters = {
|
|
1115
|
-
...(request.query._policyFilters ?? {}),
|
|
1116
|
-
[tenantField]: orgId,
|
|
1117
|
-
};
|
|
1118
|
-
|
|
1119
|
-
request.log?.debug?.({ msg: 'Tenant filter applied', orgId, tenantField });
|
|
1082
|
+
// Public / authenticated — no org context, show all data (public routes)
|
|
1083
|
+
request.log?.debug?.({ msg: 'No org context — showing all data' });
|
|
1120
1084
|
};
|
|
1121
1085
|
}
|
|
1122
1086
|
|
|
1123
1087
|
/**
|
|
1124
|
-
* Create tenant injection middleware
|
|
1125
|
-
* Injects tenant ID into request body on create
|
|
1088
|
+
* Create tenant injection middleware.
|
|
1089
|
+
* Injects tenant ID into request body on create.
|
|
1126
1090
|
*/
|
|
1127
|
-
function createTenantInjection(
|
|
1128
|
-
tenantField${ts ? ": string" : ""},
|
|
1129
|
-
extractOrganizationId${ts ? ": (request: any) => string | null" : ""}
|
|
1130
|
-
) {
|
|
1091
|
+
function createTenantInjection(tenantField${ts ? ": string" : ""}) {
|
|
1131
1092
|
return async (request${ts ? ": any" : ""}, reply${ts ? ": any" : ""}) => {
|
|
1132
|
-
const
|
|
1093
|
+
const scope${ts ? ": RequestScope" : ""} = request.scope ?? { kind: 'public' };
|
|
1094
|
+
const orgId = getOrgId(scope);
|
|
1133
1095
|
|
|
1134
1096
|
// Fail-closed: Require orgId for create operations
|
|
1135
1097
|
if (!orgId) {
|
|
@@ -1150,18 +1112,12 @@ function createTenantInjection(
|
|
|
1150
1112
|
* Flexible Multi-Tenant Preset
|
|
1151
1113
|
*
|
|
1152
1114
|
* @param options.tenantField - Field name in database (default: 'organizationId')
|
|
1153
|
-
* @param options.bypassRoles - Roles that bypass tenant isolation (default: ['superadmin'])
|
|
1154
|
-
* @param options.extractOrganizationId - Custom org ID extractor function
|
|
1155
1115
|
*/
|
|
1156
1116
|
export function flexibleMultiTenantPreset(options${ts ? ": FlexibleMultiTenantOptions = {}" : " = {}"})${ts ? ": Preset" : ""} {
|
|
1157
|
-
const {
|
|
1158
|
-
tenantField = 'organizationId',
|
|
1159
|
-
bypassRoles = ['superadmin'],
|
|
1160
|
-
extractOrganizationId = defaultExtractOrganizationId,
|
|
1161
|
-
} = options;
|
|
1117
|
+
const { tenantField = 'organizationId' } = options;
|
|
1162
1118
|
|
|
1163
|
-
const tenantFilter = createFlexibleTenantFilter(tenantField
|
|
1164
|
-
const tenantInjection = createTenantInjection(tenantField
|
|
1119
|
+
const tenantFilter = createFlexibleTenantFilter(tenantField);
|
|
1120
|
+
const tenantInjection = createTenantInjection(tenantField);
|
|
1165
1121
|
|
|
1166
1122
|
return {
|
|
1167
1123
|
name: 'flexibleMultiTenant',
|
|
@@ -1179,10 +1135,10 @@ export default flexibleMultiTenantPreset;
|
|
|
1179
1135
|
`;
|
|
1180
1136
|
}
|
|
1181
1137
|
function permissionsTemplate(config) {
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1138
|
+
const ts = config.typescript;
|
|
1139
|
+
const typeImport = ts ? ",\n type PermissionCheck," : "";
|
|
1140
|
+
const returnType = ts ? ": PermissionCheck" : "";
|
|
1141
|
+
let content = `/**
|
|
1186
1142
|
* Permission Helpers
|
|
1187
1143
|
*
|
|
1188
1144
|
* Clean, type-safe permission definitions for resources.
|
|
@@ -1233,28 +1189,73 @@ export const requireAdmin = ()${returnType} =>
|
|
|
1233
1189
|
export const requireSuperadmin = ()${returnType} =>
|
|
1234
1190
|
requireRoles(['superadmin']);
|
|
1235
1191
|
`;
|
|
1236
|
-
|
|
1237
|
-
|
|
1192
|
+
if (config.tenant === "multi") if (config.auth === "better-auth") content += `
|
|
1193
|
+
// ============================================================================
|
|
1194
|
+
// Better Auth Organization & Team Permission Helpers
|
|
1195
|
+
// ============================================================================
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Organization-level guards (per-org member.role):
|
|
1199
|
+
*
|
|
1200
|
+
* - requireOrgRole(['admin','owner']) — checks member.role in active org
|
|
1201
|
+
* - requireOrgMembership() — just checks if user is in the org (any role)
|
|
1202
|
+
* - requireTeamMembership() — checks if user is in the active team
|
|
1203
|
+
*
|
|
1204
|
+
* These are DIFFERENT from platform-level helpers above (requireRoles checks user.roles).
|
|
1205
|
+
* Platform superadmin automatically bypasses all org role checks.
|
|
1206
|
+
*
|
|
1207
|
+
* IMPORTANT: When using Better Auth's Access Control (ac) with custom roles,
|
|
1208
|
+
* you MUST define ALL roles (owner, admin, member, + any custom) using the
|
|
1209
|
+
* same AC instance. BA's built-in defaults won't cover custom statements.
|
|
1210
|
+
* Omitting any role causes BA's hasPermission to fail silently for that role.
|
|
1211
|
+
*
|
|
1212
|
+
* @see multi-org-betterauth boilerplate (src/shared/access-control.ts) for the recommended pattern.
|
|
1213
|
+
*/
|
|
1214
|
+
import {
|
|
1215
|
+
requireOrgMembership,
|
|
1216
|
+
requireOrgRole,
|
|
1217
|
+
requireTeamMembership,
|
|
1218
|
+
} from '@classytic/arc/permissions';
|
|
1219
|
+
export { requireOrgMembership, requireOrgRole, requireTeamMembership };
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Require organization owner (checks member.role, not user.roles)
|
|
1223
|
+
*/
|
|
1224
|
+
export const requireOrgOwner = ()${returnType} =>
|
|
1225
|
+
requireOrgRole(['owner']);
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Require organization manager or higher (checks member.role, not user.roles)
|
|
1229
|
+
*/
|
|
1230
|
+
export const requireOrgManager = ()${returnType} =>
|
|
1231
|
+
requireOrgRole(['manager', 'admin', 'owner']);
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Require any organization member (any role)
|
|
1235
|
+
*/
|
|
1236
|
+
export const requireOrgStaff = ()${returnType} =>
|
|
1237
|
+
requireOrgMembership();
|
|
1238
|
+
`;
|
|
1239
|
+
else content += `
|
|
1238
1240
|
/**
|
|
1239
|
-
* Require organization owner
|
|
1241
|
+
* Require organization owner (elevated scope auto-bypasses)
|
|
1240
1242
|
*/
|
|
1241
1243
|
export const requireOrgOwner = ()${returnType} =>
|
|
1242
|
-
requireRoles(['owner'
|
|
1244
|
+
requireRoles(['owner', 'admin', 'superadmin']);
|
|
1243
1245
|
|
|
1244
1246
|
/**
|
|
1245
1247
|
* Require organization manager or higher
|
|
1246
1248
|
*/
|
|
1247
1249
|
export const requireOrgManager = ()${returnType} =>
|
|
1248
|
-
requireRoles(['owner', 'manager'
|
|
1250
|
+
requireRoles(['owner', 'manager', 'admin', 'superadmin']);
|
|
1249
1251
|
|
|
1250
1252
|
/**
|
|
1251
1253
|
* Require organization staff (any org member)
|
|
1252
1254
|
*/
|
|
1253
1255
|
export const requireOrgStaff = ()${returnType} =>
|
|
1254
|
-
requireRoles(['owner', 'manager', 'staff'
|
|
1256
|
+
requireRoles(['owner', 'manager', 'staff', 'admin', 'superadmin']);
|
|
1255
1257
|
`;
|
|
1256
|
-
|
|
1257
|
-
content += `
|
|
1258
|
+
content += `
|
|
1258
1259
|
// ============================================================================
|
|
1259
1260
|
// Standard Permission Sets
|
|
1260
1261
|
// ============================================================================
|
|
@@ -1292,8 +1293,8 @@ export const adminPermissions = {
|
|
|
1292
1293
|
delete: requireSuperadmin(),
|
|
1293
1294
|
};
|
|
1294
1295
|
`;
|
|
1295
|
-
|
|
1296
|
-
|
|
1296
|
+
if (config.tenant === "multi") {
|
|
1297
|
+
content += `
|
|
1297
1298
|
/**
|
|
1298
1299
|
* Organization staff permissions
|
|
1299
1300
|
*/
|
|
@@ -1305,24 +1306,45 @@ export const orgStaffPermissions = {
|
|
|
1305
1306
|
delete: requireOrgOwner(),
|
|
1306
1307
|
};
|
|
1307
1308
|
`;
|
|
1308
|
-
|
|
1309
|
-
|
|
1309
|
+
if (config.auth === "better-auth") content += `
|
|
1310
|
+
/**
|
|
1311
|
+
* Team-scoped permissions (requires active team)
|
|
1312
|
+
* Uses Better Auth's team membership — flat groups, no team-level roles.
|
|
1313
|
+
*/
|
|
1314
|
+
export const teamScopedPermissions = {
|
|
1315
|
+
list: requireTeamMembership(),
|
|
1316
|
+
get: requireTeamMembership(),
|
|
1317
|
+
create: requireTeamMembership(),
|
|
1318
|
+
update: requireTeamMembership(),
|
|
1319
|
+
delete: requireOrgOwner(),
|
|
1320
|
+
};
|
|
1321
|
+
`;
|
|
1322
|
+
}
|
|
1323
|
+
return content;
|
|
1310
1324
|
}
|
|
1311
1325
|
function configTemplate(config) {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1326
|
+
const ts = config.typescript;
|
|
1327
|
+
const authTypeBlock = config.auth === "better-auth" ? `
|
|
1328
|
+
betterAuth: {
|
|
1329
|
+
secret: string;
|
|
1330
|
+
};
|
|
1331
|
+
frontend: {
|
|
1332
|
+
url: string;
|
|
1333
|
+
};` : `
|
|
1334
|
+
jwt: {
|
|
1335
|
+
secret: string;
|
|
1336
|
+
expiresIn: string;
|
|
1337
|
+
};`;
|
|
1338
|
+
let typeDefinition = "";
|
|
1339
|
+
if (ts) typeDefinition = `
|
|
1316
1340
|
export interface AppConfig {
|
|
1317
1341
|
env: string;
|
|
1342
|
+
isDev: boolean;
|
|
1343
|
+
isProd: boolean;
|
|
1318
1344
|
server: {
|
|
1319
1345
|
port: number;
|
|
1320
1346
|
host: string;
|
|
1321
|
-
}
|
|
1322
|
-
jwt: {
|
|
1323
|
-
secret: string;
|
|
1324
|
-
expiresIn: string;
|
|
1325
|
-
};
|
|
1347
|
+
};${authTypeBlock}
|
|
1326
1348
|
cors: {
|
|
1327
1349
|
origins: string[] | boolean; // true = allow all ('*')
|
|
1328
1350
|
methods: string[];
|
|
@@ -1332,13 +1354,24 @@ export interface AppConfig {
|
|
|
1332
1354
|
database: {
|
|
1333
1355
|
uri: string;
|
|
1334
1356
|
};` : ""}${config.tenant === "multi" ? `
|
|
1335
|
-
org
|
|
1357
|
+
org: {
|
|
1336
1358
|
header: string;
|
|
1337
1359
|
};` : ""}
|
|
1338
1360
|
}
|
|
1339
1361
|
`;
|
|
1340
|
-
|
|
1341
|
-
|
|
1362
|
+
const authConfigBlock = config.auth === "better-auth" ? `
|
|
1363
|
+
betterAuth: {
|
|
1364
|
+
secret: process.env.BETTER_AUTH_SECRET || 'dev-secret-change-in-production-min-32-chars',
|
|
1365
|
+
},
|
|
1366
|
+
|
|
1367
|
+
frontend: {
|
|
1368
|
+
url: process.env.FRONTEND_URL || 'http://localhost:3000',
|
|
1369
|
+
},` : `
|
|
1370
|
+
jwt: {
|
|
1371
|
+
secret: process.env.JWT_SECRET || 'dev-secret-change-in-production-min-32',
|
|
1372
|
+
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
|
|
1373
|
+
},`;
|
|
1374
|
+
return `/**
|
|
1342
1375
|
* Application Configuration
|
|
1343
1376
|
*
|
|
1344
1377
|
* All config is loaded from environment variables.
|
|
@@ -1347,16 +1380,14 @@ export interface AppConfig {
|
|
|
1347
1380
|
${typeDefinition}
|
|
1348
1381
|
const config${ts ? ": AppConfig" : ""} = {
|
|
1349
1382
|
env: process.env.NODE_ENV || 'development',
|
|
1383
|
+
isDev: (process.env.NODE_ENV || 'development') !== 'production',
|
|
1384
|
+
isProd: process.env.NODE_ENV === 'production',
|
|
1350
1385
|
|
|
1351
1386
|
server: {
|
|
1352
1387
|
port: parseInt(process.env.PORT || '8040', 10),
|
|
1353
1388
|
host: process.env.HOST || '0.0.0.0',
|
|
1354
1389
|
},
|
|
1355
|
-
|
|
1356
|
-
jwt: {
|
|
1357
|
-
secret: process.env.JWT_SECRET || 'dev-secret-change-in-production-min-32',
|
|
1358
|
-
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
|
|
1359
|
-
},
|
|
1390
|
+
${authConfigBlock}
|
|
1360
1391
|
|
|
1361
1392
|
cors: {
|
|
1362
1393
|
// '*' = allow all origins (true), otherwise comma-separated list
|
|
@@ -1382,12 +1413,12 @@ export default config;
|
|
|
1382
1413
|
`;
|
|
1383
1414
|
}
|
|
1384
1415
|
function exampleModelTemplate(config) {
|
|
1385
|
-
|
|
1386
|
-
|
|
1416
|
+
const ts = config.typescript;
|
|
1417
|
+
const typeExport = ts ? `
|
|
1387
1418
|
export type ExampleDocument = mongoose.InferSchemaType<typeof exampleSchema>;
|
|
1388
1419
|
export type ExampleModel = mongoose.Model<ExampleDocument>;
|
|
1389
1420
|
` : "";
|
|
1390
|
-
|
|
1421
|
+
return `/**
|
|
1391
1422
|
* Example Model
|
|
1392
1423
|
* Generated by Arc CLI
|
|
1393
1424
|
*/
|
|
@@ -1419,10 +1450,8 @@ export default Example;
|
|
|
1419
1450
|
`;
|
|
1420
1451
|
}
|
|
1421
1452
|
function exampleRepositoryTemplate(config) {
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
const generic = ts ? "<ExampleDocument>" : "";
|
|
1425
|
-
return `/**
|
|
1453
|
+
const ts = config.typescript;
|
|
1454
|
+
return `/**
|
|
1426
1455
|
* Example Repository
|
|
1427
1456
|
* Generated by Arc CLI
|
|
1428
1457
|
*
|
|
@@ -1436,9 +1465,9 @@ import {
|
|
|
1436
1465
|
softDeletePlugin,
|
|
1437
1466
|
methodRegistryPlugin,
|
|
1438
1467
|
} from '@classytic/mongokit';
|
|
1439
|
-
${
|
|
1468
|
+
${ts ? "import type { ExampleDocument } from './example.model.js';\n" : ""}import Example from './example.model.js';
|
|
1440
1469
|
|
|
1441
|
-
class ExampleRepository extends Repository${
|
|
1470
|
+
class ExampleRepository extends Repository${ts ? "<ExampleDocument>" : ""} {
|
|
1442
1471
|
constructor() {
|
|
1443
1472
|
super(Example, [
|
|
1444
1473
|
methodRegistryPlugin(), // Required for plugin method registration
|
|
@@ -1474,9 +1503,8 @@ export { ExampleRepository };
|
|
|
1474
1503
|
`;
|
|
1475
1504
|
}
|
|
1476
1505
|
function exampleResourceTemplate(config) {
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
return `/**
|
|
1506
|
+
const ts = config.typescript;
|
|
1507
|
+
return `/**
|
|
1480
1508
|
* Example Resource
|
|
1481
1509
|
* Generated by Arc CLI
|
|
1482
1510
|
*
|
|
@@ -1489,12 +1517,12 @@ function exampleResourceTemplate(config) {
|
|
|
1489
1517
|
|
|
1490
1518
|
import { defineResource } from '@classytic/arc';
|
|
1491
1519
|
import { createAdapter } from '#shared/adapter.js';
|
|
1492
|
-
import { publicReadPermissions } from '#shared/permissions.js';
|
|
1493
|
-
${config.tenant === "multi" ? "import { flexibleMultiTenantPreset } from '#shared/presets/flexible-multi-tenant.js';\n" : ""}import Example from './example.model.js';
|
|
1520
|
+
import { ${config.tenant === "multi" ? "orgStaffPermissions" : "publicReadPermissions"} } from '#shared/permissions.js';
|
|
1521
|
+
${config.tenant === "multi" ? "import { flexibleMultiTenantPreset } from '#shared/presets/flexible-multi-tenant.js';\n" : ""}import Example${ts ? ", { type ExampleDocument }" : ""} from './example.model.js';
|
|
1494
1522
|
import exampleRepository from './example.repository.js';
|
|
1495
1523
|
import exampleController from './example.controller.js';
|
|
1496
1524
|
|
|
1497
|
-
const exampleResource = defineResource({
|
|
1525
|
+
const exampleResource = defineResource${ts ? "<ExampleDocument>" : ""}({
|
|
1498
1526
|
name: 'example',
|
|
1499
1527
|
displayName: 'Examples',
|
|
1500
1528
|
prefix: '/examples',
|
|
@@ -1507,7 +1535,7 @@ const exampleResource = defineResource({
|
|
|
1507
1535
|
flexibleMultiTenantPreset({ tenantField: 'organizationId' }),` : ""}
|
|
1508
1536
|
],
|
|
1509
1537
|
|
|
1510
|
-
permissions: publicReadPermissions,
|
|
1538
|
+
permissions: ${config.tenant === "multi" ? "orgStaffPermissions" : "publicReadPermissions"},
|
|
1511
1539
|
|
|
1512
1540
|
// Add custom routes here:
|
|
1513
1541
|
// additionalRoutes: [
|
|
@@ -1524,8 +1552,7 @@ export default exampleResource;
|
|
|
1524
1552
|
`;
|
|
1525
1553
|
}
|
|
1526
1554
|
function exampleControllerTemplate(config) {
|
|
1527
|
-
|
|
1528
|
-
return `/**
|
|
1555
|
+
return `/**
|
|
1529
1556
|
* Example Controller
|
|
1530
1557
|
* Generated by Arc CLI
|
|
1531
1558
|
*
|
|
@@ -1541,7 +1568,11 @@ import { exampleSchemaOptions } from './example.schemas.js';
|
|
|
1541
1568
|
|
|
1542
1569
|
class ExampleController extends BaseController {
|
|
1543
1570
|
constructor() {
|
|
1544
|
-
super(exampleRepository${
|
|
1571
|
+
super(exampleRepository${config.typescript ? " as any" : ""}, {
|
|
1572
|
+
schemaOptions: exampleSchemaOptions,${config.tenant === "multi" ? `
|
|
1573
|
+
tenantField: 'organizationId', // Configurable tenant field for multi-tenant` : `
|
|
1574
|
+
// tenantField: 'organizationId', // For multi-tenant apps`}
|
|
1575
|
+
});
|
|
1545
1576
|
}
|
|
1546
1577
|
|
|
1547
1578
|
// Add custom controller methods here:
|
|
@@ -1555,9 +1586,9 @@ export default exampleController;
|
|
|
1555
1586
|
`;
|
|
1556
1587
|
}
|
|
1557
1588
|
function exampleSchemasTemplate(config) {
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1589
|
+
const ts = config.typescript;
|
|
1590
|
+
const multiTenantFields = config.tenant === "multi";
|
|
1591
|
+
return `/**
|
|
1561
1592
|
* Example Schemas
|
|
1562
1593
|
* Generated by Arc CLI
|
|
1563
1594
|
*
|
|
@@ -1602,8 +1633,8 @@ export default crudSchemas;
|
|
|
1602
1633
|
`;
|
|
1603
1634
|
}
|
|
1604
1635
|
function exampleTestTemplate(config) {
|
|
1605
|
-
|
|
1606
|
-
|
|
1636
|
+
const ts = config.typescript;
|
|
1637
|
+
return `/**
|
|
1607
1638
|
* Example Resource Tests
|
|
1608
1639
|
* Generated by Arc CLI
|
|
1609
1640
|
*
|
|
@@ -1668,13 +1699,13 @@ ${config.adapter === "mongokit" ? " await mongoose.connection.close();" : ""}
|
|
|
1668
1699
|
`;
|
|
1669
1700
|
}
|
|
1670
1701
|
function userModelTemplate(config) {
|
|
1671
|
-
|
|
1672
|
-
|
|
1702
|
+
const ts = config.typescript;
|
|
1703
|
+
const orgRoles = config.tenant === "multi" ? `
|
|
1673
1704
|
// Organization roles (for multi-tenant)
|
|
1674
1705
|
const ORG_ROLES = ['owner', 'manager', 'hr', 'staff', 'contractor'] as const;
|
|
1675
1706
|
type OrgRole = typeof ORG_ROLES[number];
|
|
1676
1707
|
` : "";
|
|
1677
|
-
|
|
1708
|
+
const orgInterface = config.tenant === "multi" ? `
|
|
1678
1709
|
type UserOrganization = {
|
|
1679
1710
|
organizationId: Types.ObjectId;
|
|
1680
1711
|
organizationName: string;
|
|
@@ -1682,7 +1713,7 @@ type UserOrganization = {
|
|
|
1682
1713
|
joinedAt: Date;
|
|
1683
1714
|
};
|
|
1684
1715
|
` : "";
|
|
1685
|
-
|
|
1716
|
+
const orgSchema = config.tenant === "multi" ? `
|
|
1686
1717
|
// Multi-org support
|
|
1687
1718
|
organizations: [{
|
|
1688
1719
|
organizationId: { type: Schema.Types.ObjectId, ref: 'Organization', required: true },
|
|
@@ -1691,7 +1722,7 @@ type UserOrganization = {
|
|
|
1691
1722
|
joinedAt: { type: Date, default: () => new Date() },
|
|
1692
1723
|
}],
|
|
1693
1724
|
` : "";
|
|
1694
|
-
|
|
1725
|
+
const orgMethods = config.tenant === "multi" ? `
|
|
1695
1726
|
// Organization methods
|
|
1696
1727
|
userSchema.methods.getOrgRoles = function(orgId${ts ? ": Types.ObjectId | string" : ""}) {
|
|
1697
1728
|
const org = this.organizations.find(o => o.organizationId.toString() === orgId.toString());
|
|
@@ -1725,7 +1756,7 @@ userSchema.methods.removeOrganization = function(organizationId${ts ? ": Types.O
|
|
|
1725
1756
|
// Index for org queries
|
|
1726
1757
|
userSchema.index({ 'organizations.organizationId': 1 });
|
|
1727
1758
|
` : "";
|
|
1728
|
-
|
|
1759
|
+
const userType = ts ? `
|
|
1729
1760
|
type PlatformRole = 'user' | 'admin' | 'superadmin';
|
|
1730
1761
|
|
|
1731
1762
|
type User = {
|
|
@@ -1749,7 +1780,7 @@ type UserMethods = {
|
|
|
1749
1780
|
export type UserDocument = HydratedDocument<User, UserMethods>;
|
|
1750
1781
|
export type UserModel = Model<User, {}, UserMethods>;
|
|
1751
1782
|
` : "";
|
|
1752
|
-
|
|
1783
|
+
return `/**
|
|
1753
1784
|
* User Model
|
|
1754
1785
|
* Generated by Arc CLI
|
|
1755
1786
|
*/
|
|
@@ -1812,9 +1843,8 @@ export default User;
|
|
|
1812
1843
|
`;
|
|
1813
1844
|
}
|
|
1814
1845
|
function userRepositoryTemplate(config) {
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
return `/**
|
|
1846
|
+
const ts = config.typescript;
|
|
1847
|
+
return `/**
|
|
1818
1848
|
* User Repository
|
|
1819
1849
|
* Generated by Arc CLI
|
|
1820
1850
|
*
|
|
@@ -1826,7 +1856,7 @@ import {
|
|
|
1826
1856
|
methodRegistryPlugin,
|
|
1827
1857
|
mongoOperationsPlugin,
|
|
1828
1858
|
} from '@classytic/mongokit';
|
|
1829
|
-
${
|
|
1859
|
+
${ts ? "import type { UserDocument } from './user.model.js';\nimport type { ClientSession, Types } from 'mongoose';\n" : ""}import User from './user.model.js';
|
|
1830
1860
|
|
|
1831
1861
|
${ts ? "type ID = string | Types.ObjectId;\n" : ""}
|
|
1832
1862
|
class UserRepository extends Repository${ts ? "<UserDocument>" : ""} {
|
|
@@ -1904,8 +1934,7 @@ export { UserRepository };
|
|
|
1904
1934
|
`;
|
|
1905
1935
|
}
|
|
1906
1936
|
function userControllerTemplate(config) {
|
|
1907
|
-
|
|
1908
|
-
return `/**
|
|
1937
|
+
return `/**
|
|
1909
1938
|
* User Controller
|
|
1910
1939
|
* Generated by Arc CLI
|
|
1911
1940
|
*
|
|
@@ -1918,7 +1947,7 @@ import userRepository from './user.repository.js';
|
|
|
1918
1947
|
|
|
1919
1948
|
class UserController extends BaseController {
|
|
1920
1949
|
constructor() {
|
|
1921
|
-
super(userRepository${
|
|
1950
|
+
super(userRepository${config.typescript ? " as any" : ""});
|
|
1922
1951
|
}
|
|
1923
1952
|
|
|
1924
1953
|
// Custom user operations can be added here
|
|
@@ -1929,8 +1958,8 @@ export default userController;
|
|
|
1929
1958
|
`;
|
|
1930
1959
|
}
|
|
1931
1960
|
function authResourceTemplate(config) {
|
|
1932
|
-
|
|
1933
|
-
|
|
1961
|
+
const ts = config.typescript;
|
|
1962
|
+
return `/**
|
|
1934
1963
|
* Auth Resource
|
|
1935
1964
|
* Generated by Arc CLI
|
|
1936
1965
|
*
|
|
@@ -1972,7 +2001,7 @@ export const authResource = defineResource({
|
|
|
1972
2001
|
permissions: allowPublic(),
|
|
1973
2002
|
handler: handlers.register,
|
|
1974
2003
|
wrapHandler: false,
|
|
1975
|
-
schema: { body: schemas.registerBody },
|
|
2004
|
+
schema: { body: schemas.registerBody, response: { 201: schemas.successResponse } },
|
|
1976
2005
|
},
|
|
1977
2006
|
{
|
|
1978
2007
|
method: 'POST',
|
|
@@ -1981,7 +2010,7 @@ export const authResource = defineResource({
|
|
|
1981
2010
|
permissions: allowPublic(),
|
|
1982
2011
|
handler: handlers.login,
|
|
1983
2012
|
wrapHandler: false,
|
|
1984
|
-
schema: { body: schemas.loginBody },
|
|
2013
|
+
schema: { body: schemas.loginBody, response: { 200: schemas.loginResponse } },
|
|
1985
2014
|
},
|
|
1986
2015
|
{
|
|
1987
2016
|
method: 'POST',
|
|
@@ -1990,7 +2019,7 @@ export const authResource = defineResource({
|
|
|
1990
2019
|
permissions: allowPublic(),
|
|
1991
2020
|
handler: handlers.refreshToken,
|
|
1992
2021
|
wrapHandler: false,
|
|
1993
|
-
schema: { body: schemas.refreshBody },
|
|
2022
|
+
schema: { body: schemas.refreshBody, response: { 200: schemas.tokenResponse } },
|
|
1994
2023
|
},
|
|
1995
2024
|
{
|
|
1996
2025
|
method: 'POST',
|
|
@@ -1999,7 +2028,7 @@ export const authResource = defineResource({
|
|
|
1999
2028
|
permissions: allowPublic(),
|
|
2000
2029
|
handler: handlers.forgotPassword,
|
|
2001
2030
|
wrapHandler: false,
|
|
2002
|
-
schema: { body: schemas.forgotBody },
|
|
2031
|
+
schema: { body: schemas.forgotBody, response: { 200: schemas.successResponse } },
|
|
2003
2032
|
},
|
|
2004
2033
|
{
|
|
2005
2034
|
method: 'POST',
|
|
@@ -2008,7 +2037,7 @@ export const authResource = defineResource({
|
|
|
2008
2037
|
permissions: allowPublic(),
|
|
2009
2038
|
handler: handlers.resetPassword,
|
|
2010
2039
|
wrapHandler: false,
|
|
2011
|
-
schema: { body: schemas.resetBody },
|
|
2040
|
+
schema: { body: schemas.resetBody, response: { 200: schemas.successResponse } },
|
|
2012
2041
|
},
|
|
2013
2042
|
],
|
|
2014
2043
|
});
|
|
@@ -2033,6 +2062,7 @@ export const userProfileResource = defineResource({
|
|
|
2033
2062
|
permissions: requireAuth(),
|
|
2034
2063
|
handler: handlers.getUserProfile,
|
|
2035
2064
|
wrapHandler: false,
|
|
2065
|
+
schema: { response: { 200: schemas.userProfileResponse } },
|
|
2036
2066
|
},
|
|
2037
2067
|
{
|
|
2038
2068
|
method: 'PATCH',
|
|
@@ -2041,7 +2071,7 @@ export const userProfileResource = defineResource({
|
|
|
2041
2071
|
permissions: requireAuth(),
|
|
2042
2072
|
handler: handlers.updateUserProfile,
|
|
2043
2073
|
wrapHandler: false,
|
|
2044
|
-
schema: { body: schemas.updateUserBody },
|
|
2074
|
+
schema: { body: schemas.updateUserBody, response: { 200: schemas.userProfileResponse } },
|
|
2045
2075
|
},
|
|
2046
2076
|
],
|
|
2047
2077
|
});
|
|
@@ -2049,26 +2079,128 @@ export const userProfileResource = defineResource({
|
|
|
2049
2079
|
export default authResource;
|
|
2050
2080
|
`;
|
|
2051
2081
|
}
|
|
2082
|
+
function betterAuthSetupTemplate(config) {
|
|
2083
|
+
const ts = config.typescript;
|
|
2084
|
+
const mongoImport = config.adapter === "mongokit" ? `import mongoose from 'mongoose';
|
|
2085
|
+
import { mongodbAdapter } from 'better-auth/adapters/mongodb';` : "";
|
|
2086
|
+
const dbAdapter = config.adapter === "mongokit" ? config.typescript ? `database: mongodbAdapter(mongoose.connection.getClient().db() as any),` : `database: mongodbAdapter(mongoose.connection.getClient().db()),` : `// Configure your database adapter here
|
|
2087
|
+
// See: https://www.better-auth.com/docs/concepts/database`;
|
|
2088
|
+
const orgPlugin = config.tenant === "multi" ? `
|
|
2089
|
+
import { organization } from 'better-auth/plugins/organization';
|
|
2090
|
+
import { bearer } from 'better-auth/plugins/bearer';` : "";
|
|
2091
|
+
const orgPluginUsage = config.tenant === "multi" ? `
|
|
2092
|
+
plugins: [
|
|
2093
|
+
bearer(),
|
|
2094
|
+
organization({
|
|
2095
|
+
allowUserToCreateOrganization: true,
|
|
2096
|
+
creatorRole: 'owner',
|
|
2097
|
+
teams: {
|
|
2098
|
+
enabled: true,
|
|
2099
|
+
},
|
|
2100
|
+
}),
|
|
2101
|
+
],` : "";
|
|
2102
|
+
return `/**
|
|
2103
|
+
* Better Auth Configuration
|
|
2104
|
+
* Generated by Arc CLI
|
|
2105
|
+
*
|
|
2106
|
+
* Authentication is handled entirely by Better Auth.
|
|
2107
|
+
* Routes are registered automatically at /api/auth/*
|
|
2108
|
+
*
|
|
2109
|
+
* Better Auth manages these collections:
|
|
2110
|
+
* - user, session, account${config.tenant === "multi" ? ", organization, member, invitation, team, teamMember" : ""}
|
|
2111
|
+
*
|
|
2112
|
+
* @see https://www.better-auth.com/docs
|
|
2113
|
+
*/
|
|
2114
|
+
|
|
2115
|
+
import { betterAuth } from 'better-auth';
|
|
2116
|
+
${mongoImport}${orgPlugin}
|
|
2117
|
+
import config from '#config/index.js';
|
|
2118
|
+
|
|
2119
|
+
let _auth${ts ? ": ReturnType<typeof betterAuth> | null" : ""} = null;
|
|
2120
|
+
|
|
2121
|
+
/**
|
|
2122
|
+
* Get the Better Auth instance (lazy singleton)
|
|
2123
|
+
*
|
|
2124
|
+
* Must be called AFTER database connection is established.
|
|
2125
|
+
*/
|
|
2126
|
+
export function getAuth()${ts ? ": ReturnType<typeof betterAuth>" : ""} {
|
|
2127
|
+
if (process.env.NODE_ENV === 'production' && !process.env.BETTER_AUTH_SECRET) {
|
|
2128
|
+
throw new Error('BETTER_AUTH_SECRET is required in production (min 32 chars)');
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
if (!_auth) {
|
|
2132
|
+
_auth = betterAuth({
|
|
2133
|
+
secret: config.betterAuth.secret,
|
|
2134
|
+
baseURL: process.env.BETTER_AUTH_URL || \`http://localhost:\${config.server.port}\`,
|
|
2135
|
+
basePath: '/api/auth',
|
|
2136
|
+
|
|
2137
|
+
${dbAdapter}
|
|
2138
|
+
${config.tenant === "multi" ? `
|
|
2139
|
+
user: {
|
|
2140
|
+
additionalFields: {
|
|
2141
|
+
roles: {
|
|
2142
|
+
type: 'string[]',
|
|
2143
|
+
defaultValue: ['user'],
|
|
2144
|
+
required: false,
|
|
2145
|
+
input: false, // Cannot be set during signup
|
|
2146
|
+
},
|
|
2147
|
+
},
|
|
2148
|
+
},
|
|
2149
|
+
` : ""}
|
|
2150
|
+
emailAndPassword: {
|
|
2151
|
+
enabled: true,
|
|
2152
|
+
minPasswordLength: 6,
|
|
2153
|
+
},
|
|
2154
|
+
|
|
2155
|
+
// Google OAuth (enabled when env vars are set)
|
|
2156
|
+
...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
|
|
2157
|
+
? {
|
|
2158
|
+
socialProviders: {
|
|
2159
|
+
google: {
|
|
2160
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
2161
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
2162
|
+
},
|
|
2163
|
+
},
|
|
2164
|
+
}
|
|
2165
|
+
: {}),
|
|
2166
|
+
${orgPluginUsage}
|
|
2167
|
+
session: {
|
|
2168
|
+
cookieCache: {
|
|
2169
|
+
enabled: true,
|
|
2170
|
+
maxAge: 5 * 60, // 5 minutes
|
|
2171
|
+
},
|
|
2172
|
+
},
|
|
2173
|
+
|
|
2174
|
+
trustedOrigins: [config.frontend.url],
|
|
2175
|
+
|
|
2176
|
+
rateLimit: {
|
|
2177
|
+
enabled: process.env.NODE_ENV === 'production',
|
|
2178
|
+
},
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
return _auth;
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
export default getAuth;
|
|
2186
|
+
`;
|
|
2187
|
+
}
|
|
2052
2188
|
function authHandlersTemplate(config) {
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
2056
|
-
` : "";
|
|
2057
|
-
return `/**
|
|
2189
|
+
const ts = config.typescript;
|
|
2190
|
+
return `/**
|
|
2058
2191
|
* Auth Handlers
|
|
2059
2192
|
* Generated by Arc CLI
|
|
2193
|
+
*
|
|
2194
|
+
* Uses Arc's built-in JWT utilities via fastify.auth (provided by @fastify/jwt v10).
|
|
2195
|
+
* No standalone jsonwebtoken dependency needed.
|
|
2060
2196
|
*/
|
|
2061
2197
|
|
|
2062
|
-
import jwt from 'jsonwebtoken';
|
|
2063
|
-
import config from '#config/index.js';
|
|
2064
2198
|
import userRepository from '../user/user.repository.js';
|
|
2065
|
-
${
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
return { accessToken, refreshToken };
|
|
2071
|
-
}
|
|
2199
|
+
${ts ? `
|
|
2200
|
+
import type { FastifyRequest, FastifyReply } from 'fastify';
|
|
2201
|
+
// Load Arc auth type augmentations (adds request.server.auth typings)
|
|
2202
|
+
import '@classytic/arc/auth';
|
|
2203
|
+
` : ""}
|
|
2072
2204
|
|
|
2073
2205
|
/**
|
|
2074
2206
|
* Register new user
|
|
@@ -2104,7 +2236,7 @@ export async function login(request${ts ? ": FastifyRequest" : ""}, reply${ts ?
|
|
|
2104
2236
|
return reply.code(401).send({ success: false, message: 'Invalid credentials' });
|
|
2105
2237
|
}
|
|
2106
2238
|
|
|
2107
|
-
const tokens =
|
|
2239
|
+
const tokens = request.server.auth.issueTokens({ id: user._id.toString(), roles: user.roles });
|
|
2108
2240
|
|
|
2109
2241
|
return reply.send({
|
|
2110
2242
|
success: true,
|
|
@@ -2127,8 +2259,8 @@ export async function refreshToken(request${ts ? ": FastifyRequest" : ""}, reply
|
|
|
2127
2259
|
return reply.code(401).send({ success: false, message: 'Refresh token required' });
|
|
2128
2260
|
}
|
|
2129
2261
|
|
|
2130
|
-
const decoded =
|
|
2131
|
-
const tokens =
|
|
2262
|
+
const decoded = request.server.auth.verifyRefreshToken(token)${ts ? " as { id: string }" : ""};
|
|
2263
|
+
const tokens = request.server.auth.issueTokens({ id: decoded.id });
|
|
2132
2264
|
|
|
2133
2265
|
return reply.send({ success: true, ...tokens });
|
|
2134
2266
|
} catch {
|
|
@@ -2145,11 +2277,12 @@ export async function forgotPassword(request${ts ? ": FastifyRequest" : ""}, rep
|
|
|
2145
2277
|
const user = await userRepository.findByEmail(email);
|
|
2146
2278
|
|
|
2147
2279
|
if (user) {
|
|
2148
|
-
const
|
|
2280
|
+
const { randomBytes } = await import('node:crypto');
|
|
2281
|
+
const token = randomBytes(32).toString('hex');
|
|
2149
2282
|
const expires = new Date(Date.now() + 3600000); // 1 hour
|
|
2150
2283
|
await userRepository.setResetToken(user._id, token, expires);
|
|
2151
|
-
//
|
|
2152
|
-
request.log.info(\`Password reset
|
|
2284
|
+
// SCAFFOLD: Integrate your email provider to send the reset link
|
|
2285
|
+
request.log.info(\`Password reset requested for \${email}\`);
|
|
2153
2286
|
}
|
|
2154
2287
|
|
|
2155
2288
|
// Always return success to prevent email enumeration
|
|
@@ -2221,8 +2354,8 @@ export async function updateUserProfile(request${ts ? ": FastifyRequest" : ""},
|
|
|
2221
2354
|
}
|
|
2222
2355
|
`;
|
|
2223
2356
|
}
|
|
2224
|
-
function authSchemasTemplate(
|
|
2225
|
-
|
|
2357
|
+
function authSchemasTemplate(_config) {
|
|
2358
|
+
return `/**
|
|
2226
2359
|
* Auth Schemas
|
|
2227
2360
|
* Generated by Arc CLI
|
|
2228
2361
|
*/
|
|
@@ -2278,11 +2411,56 @@ export const updateUserBody = {
|
|
|
2278
2411
|
email: { type: 'string', format: 'email' },
|
|
2279
2412
|
},
|
|
2280
2413
|
};
|
|
2414
|
+
|
|
2415
|
+
// Response schemas (enables fast-json-stringify serialization)
|
|
2416
|
+
|
|
2417
|
+
export const successResponse = {
|
|
2418
|
+
type: 'object',
|
|
2419
|
+
properties: {
|
|
2420
|
+
success: { type: 'boolean' },
|
|
2421
|
+
message: { type: 'string' },
|
|
2422
|
+
},
|
|
2423
|
+
};
|
|
2424
|
+
|
|
2425
|
+
export const loginResponse = {
|
|
2426
|
+
type: 'object',
|
|
2427
|
+
properties: {
|
|
2428
|
+
success: { type: 'boolean' },
|
|
2429
|
+
user: {
|
|
2430
|
+
type: 'object',
|
|
2431
|
+
properties: {
|
|
2432
|
+
id: { type: 'string' },
|
|
2433
|
+
name: { type: 'string' },
|
|
2434
|
+
email: { type: 'string' },
|
|
2435
|
+
roles: { type: 'array', items: { type: 'string' } },
|
|
2436
|
+
},
|
|
2437
|
+
},
|
|
2438
|
+
accessToken: { type: 'string' },
|
|
2439
|
+
refreshToken: { type: 'string' },
|
|
2440
|
+
},
|
|
2441
|
+
};
|
|
2442
|
+
|
|
2443
|
+
export const tokenResponse = {
|
|
2444
|
+
type: 'object',
|
|
2445
|
+
properties: {
|
|
2446
|
+
success: { type: 'boolean' },
|
|
2447
|
+
accessToken: { type: 'string' },
|
|
2448
|
+
refreshToken: { type: 'string' },
|
|
2449
|
+
},
|
|
2450
|
+
};
|
|
2451
|
+
|
|
2452
|
+
export const userProfileResponse = {
|
|
2453
|
+
type: 'object',
|
|
2454
|
+
properties: {
|
|
2455
|
+
success: { type: 'boolean' },
|
|
2456
|
+
data: { type: 'object', additionalProperties: true },
|
|
2457
|
+
},
|
|
2458
|
+
};
|
|
2281
2459
|
`;
|
|
2282
2460
|
}
|
|
2283
2461
|
function authTestTemplate(config) {
|
|
2284
|
-
|
|
2285
|
-
|
|
2462
|
+
const ts = config.typescript;
|
|
2463
|
+
return `/**
|
|
2286
2464
|
* Auth Tests
|
|
2287
2465
|
* Generated by Arc CLI
|
|
2288
2466
|
*/
|
|
@@ -2378,18 +2556,32 @@ ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('use
|
|
|
2378
2556
|
`;
|
|
2379
2557
|
}
|
|
2380
2558
|
function printSuccessMessage(config, skipInstall) {
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2559
|
+
const installStep = skipInstall ? ` npm install\n` : "";
|
|
2560
|
+
const ext = config.typescript ? "ts" : "js";
|
|
2561
|
+
const authInfo = config.auth === "better-auth" ? `
|
|
2562
|
+
Auth (Better Auth):
|
|
2563
|
+
|
|
2564
|
+
Auth routes: http://localhost:8040/api/auth/*
|
|
2565
|
+
Better Auth handles: registration, login, sessions, OAuth
|
|
2566
|
+
Config file: src/auth.${ext}
|
|
2567
|
+
` : `
|
|
2568
|
+
Auth (JWT):
|
|
2569
|
+
|
|
2570
|
+
POST /auth/register # Register
|
|
2571
|
+
POST /auth/login # Login (returns JWT)
|
|
2572
|
+
POST /auth/refresh # Refresh token
|
|
2573
|
+
GET /users/me # Current user profile
|
|
2574
|
+
`;
|
|
2575
|
+
console.log(`
|
|
2384
2576
|
╔═══════════════════════════════════════════════════════════════╗
|
|
2385
|
-
║
|
|
2577
|
+
║ Project Created ║
|
|
2386
2578
|
╚═══════════════════════════════════════════════════════════════╝
|
|
2387
2579
|
|
|
2388
2580
|
Next steps:
|
|
2389
2581
|
|
|
2390
2582
|
cd ${config.name}
|
|
2391
2583
|
${installStep} npm run dev # Uses .env.dev automatically
|
|
2392
|
-
|
|
2584
|
+
${authInfo}
|
|
2393
2585
|
API Documentation:
|
|
2394
2586
|
|
|
2395
2587
|
http://localhost:8040/docs # Scalar UI
|
|
@@ -2402,15 +2594,13 @@ Run tests:
|
|
|
2402
2594
|
|
|
2403
2595
|
Add resources:
|
|
2404
2596
|
|
|
2405
|
-
|
|
2406
|
-
2. Add: index.${config.typescript ? "ts" : "js"}, model.${config.typescript ? "ts" : "js"}, repository.${config.typescript ? "ts" : "js"}
|
|
2407
|
-
3. Register in src/resources/index.${config.typescript ? "ts" : "js"}
|
|
2597
|
+
arc generate resource product
|
|
2408
2598
|
|
|
2409
2599
|
Project structure:
|
|
2410
2600
|
|
|
2411
2601
|
src/
|
|
2412
|
-
├── app.${
|
|
2413
|
-
├── index.${config.
|
|
2602
|
+
├── app.${ext} # App factory (for workers/tests)
|
|
2603
|
+
├── index.${ext} # Server entry${config.auth === "better-auth" ? `\n ├── auth.${ext} # Better Auth config` : ""}
|
|
2414
2604
|
├── config/ # Configuration
|
|
2415
2605
|
├── shared/ # Adapters, presets, permissions
|
|
2416
2606
|
├── plugins/ # App plugins (DI pattern)
|
|
@@ -2420,6 +2610,7 @@ Documentation:
|
|
|
2420
2610
|
https://github.com/classytic/arc
|
|
2421
2611
|
`);
|
|
2422
2612
|
}
|
|
2423
|
-
var init_default = init;
|
|
2424
2613
|
|
|
2425
|
-
|
|
2614
|
+
//#endregion
|
|
2615
|
+
export { init as default, init };
|
|
2616
|
+
//# sourceMappingURL=init.mjs.map
|