@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.
Files changed (322) hide show
  1. package/README.md +247 -794
  2. package/bin/arc.js +91 -52
  3. package/dist/EventTransport-BD2U0BTc.d.mts +100 -0
  4. package/dist/EventTransport-BD2U0BTc.d.mts.map +1 -0
  5. package/dist/HookSystem-BsGV-j2l.mjs +405 -0
  6. package/dist/HookSystem-BsGV-j2l.mjs.map +1 -0
  7. package/dist/ResourceRegistry-DsN4KJjV.mjs +250 -0
  8. package/dist/ResourceRegistry-DsN4KJjV.mjs.map +1 -0
  9. package/dist/adapters/index.d.mts +5 -0
  10. package/dist/adapters/index.mjs +3 -0
  11. package/dist/audit/index.d.mts +82 -0
  12. package/dist/audit/index.d.mts.map +1 -0
  13. package/dist/audit/index.mjs +276 -0
  14. package/dist/audit/index.mjs.map +1 -0
  15. package/dist/audit/mongodb.d.mts +5 -0
  16. package/dist/audit/mongodb.mjs +3 -0
  17. package/dist/audited-C3T5DTUx.mjs +141 -0
  18. package/dist/audited-C3T5DTUx.mjs.map +1 -0
  19. package/dist/auth/index.d.mts +189 -0
  20. package/dist/auth/index.d.mts.map +1 -0
  21. package/dist/auth/index.mjs +1102 -0
  22. package/dist/auth/index.mjs.map +1 -0
  23. package/dist/auth/redis-session.d.mts +44 -0
  24. package/dist/auth/redis-session.d.mts.map +1 -0
  25. package/dist/auth/redis-session.mjs +76 -0
  26. package/dist/auth/redis-session.mjs.map +1 -0
  27. package/dist/betterAuthOpenApi-BrHKeSAx.mjs +250 -0
  28. package/dist/betterAuthOpenApi-BrHKeSAx.mjs.map +1 -0
  29. package/dist/cache/index.d.mts +146 -0
  30. package/dist/cache/index.d.mts.map +1 -0
  31. package/dist/cache/index.mjs +92 -0
  32. package/dist/cache/index.mjs.map +1 -0
  33. package/dist/caching-Bl28lYsR.mjs +94 -0
  34. package/dist/caching-Bl28lYsR.mjs.map +1 -0
  35. package/dist/chunk-C7Uep-_p.mjs +20 -0
  36. package/dist/circuitBreaker-DeY4FCjs.mjs +1097 -0
  37. package/dist/circuitBreaker-DeY4FCjs.mjs.map +1 -0
  38. package/dist/cli/commands/describe.d.mts +19 -0
  39. package/dist/cli/commands/describe.d.mts.map +1 -0
  40. package/dist/cli/commands/describe.mjs +239 -0
  41. package/dist/cli/commands/describe.mjs.map +1 -0
  42. package/dist/cli/commands/docs.d.mts +14 -0
  43. package/dist/cli/commands/docs.d.mts.map +1 -0
  44. package/dist/cli/commands/docs.mjs +53 -0
  45. package/dist/cli/commands/docs.mjs.map +1 -0
  46. package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -1
  47. package/dist/cli/commands/generate.d.mts.map +1 -0
  48. package/dist/cli/commands/generate.mjs +358 -0
  49. package/dist/cli/commands/generate.mjs.map +1 -0
  50. package/dist/cli/commands/{init.d.ts → init.d.mts} +12 -8
  51. package/dist/cli/commands/init.d.mts.map +1 -0
  52. package/dist/cli/commands/{init.js → init.mjs} +807 -616
  53. package/dist/cli/commands/init.mjs.map +1 -0
  54. package/dist/cli/commands/introspect.d.mts +11 -0
  55. package/dist/cli/commands/introspect.d.mts.map +1 -0
  56. package/dist/cli/commands/introspect.mjs +76 -0
  57. package/dist/cli/commands/introspect.mjs.map +1 -0
  58. package/dist/cli/index.d.mts +17 -0
  59. package/dist/cli/index.d.mts.map +1 -0
  60. package/dist/cli/index.mjs +157 -0
  61. package/dist/cli/index.mjs.map +1 -0
  62. package/dist/constants-DdXFXQtN.mjs +85 -0
  63. package/dist/constants-DdXFXQtN.mjs.map +1 -0
  64. package/dist/core/index.d.mts +5 -0
  65. package/dist/core/index.mjs +4 -0
  66. package/dist/createApp-CUgNqegw.mjs +560 -0
  67. package/dist/createApp-CUgNqegw.mjs.map +1 -0
  68. package/dist/defineResource-k0_BDn8v.mjs +2197 -0
  69. package/dist/defineResource-k0_BDn8v.mjs.map +1 -0
  70. package/dist/discovery/index.d.mts +47 -0
  71. package/dist/discovery/index.d.mts.map +1 -0
  72. package/dist/discovery/index.mjs +110 -0
  73. package/dist/discovery/index.mjs.map +1 -0
  74. package/dist/docs/index.d.mts +163 -0
  75. package/dist/docs/index.d.mts.map +1 -0
  76. package/dist/docs/index.mjs +73 -0
  77. package/dist/docs/index.mjs.map +1 -0
  78. package/dist/elevation-BRy3yFWT.mjs +113 -0
  79. package/dist/elevation-BRy3yFWT.mjs.map +1 -0
  80. package/dist/elevation-B_2dRLVP.d.mts +88 -0
  81. package/dist/elevation-B_2dRLVP.d.mts.map +1 -0
  82. package/dist/errorHandler-BbcgBmIH.d.mts +73 -0
  83. package/dist/errorHandler-BbcgBmIH.d.mts.map +1 -0
  84. package/dist/errorHandler-C1okiriz.mjs +109 -0
  85. package/dist/errorHandler-C1okiriz.mjs.map +1 -0
  86. package/dist/errors-B9bZok84.mjs +212 -0
  87. package/dist/errors-B9bZok84.mjs.map +1 -0
  88. package/dist/errors-ChKiFz62.d.mts +125 -0
  89. package/dist/errors-ChKiFz62.d.mts.map +1 -0
  90. package/dist/eventPlugin-CTrLH3mt.d.mts +125 -0
  91. package/dist/eventPlugin-CTrLH3mt.d.mts.map +1 -0
  92. package/dist/eventPlugin-DGR_B2on.mjs +230 -0
  93. package/dist/eventPlugin-DGR_B2on.mjs.map +1 -0
  94. package/dist/events/index.d.mts +54 -0
  95. package/dist/events/index.d.mts.map +1 -0
  96. package/dist/events/index.mjs +52 -0
  97. package/dist/events/index.mjs.map +1 -0
  98. package/dist/events/transports/redis-stream-entry.d.mts +2 -0
  99. package/dist/events/transports/redis-stream-entry.mjs +178 -0
  100. package/dist/events/transports/redis-stream-entry.mjs.map +1 -0
  101. package/dist/events/transports/redis.d.mts +77 -0
  102. package/dist/events/transports/redis.d.mts.map +1 -0
  103. package/dist/events/transports/redis.mjs +125 -0
  104. package/dist/events/transports/redis.mjs.map +1 -0
  105. package/dist/externalPaths-DlINfKbP.d.mts +51 -0
  106. package/dist/externalPaths-DlINfKbP.d.mts.map +1 -0
  107. package/dist/factory/index.d.mts +64 -0
  108. package/dist/factory/index.d.mts.map +1 -0
  109. package/dist/factory/index.mjs +3 -0
  110. package/dist/fastifyAdapter-BkrGrlFi.d.mts +217 -0
  111. package/dist/fastifyAdapter-BkrGrlFi.d.mts.map +1 -0
  112. package/dist/fields-DyaDVX4J.d.mts +110 -0
  113. package/dist/fields-DyaDVX4J.d.mts.map +1 -0
  114. package/dist/fields-iagOozy0.mjs +115 -0
  115. package/dist/fields-iagOozy0.mjs.map +1 -0
  116. package/dist/hooks/index.d.mts +4 -0
  117. package/dist/hooks/index.mjs +3 -0
  118. package/dist/idempotency/index.d.mts +97 -0
  119. package/dist/idempotency/index.d.mts.map +1 -0
  120. package/dist/idempotency/index.mjs +320 -0
  121. package/dist/idempotency/index.mjs.map +1 -0
  122. package/dist/idempotency/mongodb.d.mts +2 -0
  123. package/dist/idempotency/mongodb.mjs +115 -0
  124. package/dist/idempotency/mongodb.mjs.map +1 -0
  125. package/dist/idempotency/redis.d.mts +2 -0
  126. package/dist/idempotency/redis.mjs +104 -0
  127. package/dist/idempotency/redis.mjs.map +1 -0
  128. package/dist/index.d.mts +261 -0
  129. package/dist/index.d.mts.map +1 -0
  130. package/dist/index.mjs +105 -0
  131. package/dist/index.mjs.map +1 -0
  132. package/dist/integrations/event-gateway.d.mts +47 -0
  133. package/dist/integrations/event-gateway.d.mts.map +1 -0
  134. package/dist/integrations/event-gateway.mjs +44 -0
  135. package/dist/integrations/event-gateway.mjs.map +1 -0
  136. package/dist/integrations/index.d.mts +5 -0
  137. package/dist/integrations/index.mjs +1 -0
  138. package/dist/integrations/jobs.d.mts +104 -0
  139. package/dist/integrations/jobs.d.mts.map +1 -0
  140. package/dist/integrations/jobs.mjs +124 -0
  141. package/dist/integrations/jobs.mjs.map +1 -0
  142. package/dist/integrations/streamline.d.mts +61 -0
  143. package/dist/integrations/streamline.d.mts.map +1 -0
  144. package/dist/integrations/streamline.mjs +126 -0
  145. package/dist/integrations/streamline.mjs.map +1 -0
  146. package/dist/integrations/websocket.d.mts +83 -0
  147. package/dist/integrations/websocket.d.mts.map +1 -0
  148. package/dist/integrations/websocket.mjs +289 -0
  149. package/dist/integrations/websocket.mjs.map +1 -0
  150. package/dist/interface-B01JvPVc.d.mts +78 -0
  151. package/dist/interface-B01JvPVc.d.mts.map +1 -0
  152. package/dist/interface-CZe8IkMf.d.mts +55 -0
  153. package/dist/interface-CZe8IkMf.d.mts.map +1 -0
  154. package/dist/interface-Ch8HU9uM.d.mts +1098 -0
  155. package/dist/interface-Ch8HU9uM.d.mts.map +1 -0
  156. package/dist/introspectionPlugin-rFdO8ZUa.mjs +54 -0
  157. package/dist/introspectionPlugin-rFdO8ZUa.mjs.map +1 -0
  158. package/dist/keys-BqNejWup.mjs +43 -0
  159. package/dist/keys-BqNejWup.mjs.map +1 -0
  160. package/dist/logger-Df2O2WsW.mjs +79 -0
  161. package/dist/logger-Df2O2WsW.mjs.map +1 -0
  162. package/dist/memory-cQgelFOj.mjs +144 -0
  163. package/dist/memory-cQgelFOj.mjs.map +1 -0
  164. package/dist/migrations/index.d.mts +157 -0
  165. package/dist/migrations/index.d.mts.map +1 -0
  166. package/dist/migrations/index.mjs +261 -0
  167. package/dist/migrations/index.mjs.map +1 -0
  168. package/dist/mongodb-BfJVlUJH.mjs +94 -0
  169. package/dist/mongodb-BfJVlUJH.mjs.map +1 -0
  170. package/dist/mongodb-CGzRbfAK.d.mts +119 -0
  171. package/dist/mongodb-CGzRbfAK.d.mts.map +1 -0
  172. package/dist/mongodb-JN-9JA7K.d.mts +72 -0
  173. package/dist/mongodb-JN-9JA7K.d.mts.map +1 -0
  174. package/dist/openapi-G3Cw7XuM.mjs +524 -0
  175. package/dist/openapi-G3Cw7XuM.mjs.map +1 -0
  176. package/dist/org/index.d.mts +69 -0
  177. package/dist/org/index.d.mts.map +1 -0
  178. package/dist/org/index.mjs +514 -0
  179. package/dist/org/index.mjs.map +1 -0
  180. package/dist/org/types.d.mts +83 -0
  181. package/dist/org/types.d.mts.map +1 -0
  182. package/dist/org/types.mjs +1 -0
  183. package/dist/permissions/index.d.mts +279 -0
  184. package/dist/permissions/index.d.mts.map +1 -0
  185. package/dist/permissions/index.mjs +579 -0
  186. package/dist/permissions/index.mjs.map +1 -0
  187. package/dist/plugins/index.d.mts +173 -0
  188. package/dist/plugins/index.d.mts.map +1 -0
  189. package/dist/plugins/index.mjs +523 -0
  190. package/dist/plugins/index.mjs.map +1 -0
  191. package/dist/plugins/response-cache.d.mts +88 -0
  192. package/dist/plugins/response-cache.d.mts.map +1 -0
  193. package/dist/plugins/response-cache.mjs +284 -0
  194. package/dist/plugins/response-cache.mjs.map +1 -0
  195. package/dist/plugins/tracing-entry.d.mts +2 -0
  196. package/dist/plugins/tracing-entry.mjs +186 -0
  197. package/dist/plugins/tracing-entry.mjs.map +1 -0
  198. package/dist/pluralize-CEweyOEm.mjs +87 -0
  199. package/dist/pluralize-CEweyOEm.mjs.map +1 -0
  200. package/dist/policies/{index.d.ts → index.d.mts} +204 -169
  201. package/dist/policies/index.d.mts.map +1 -0
  202. package/dist/policies/index.mjs +322 -0
  203. package/dist/policies/index.mjs.map +1 -0
  204. package/dist/presets/{index.d.ts → index.d.mts} +63 -131
  205. package/dist/presets/index.d.mts.map +1 -0
  206. package/dist/presets/index.mjs +144 -0
  207. package/dist/presets/index.mjs.map +1 -0
  208. package/dist/presets/multiTenant.d.mts +25 -0
  209. package/dist/presets/multiTenant.d.mts.map +1 -0
  210. package/dist/presets/multiTenant.mjs +114 -0
  211. package/dist/presets/multiTenant.mjs.map +1 -0
  212. package/dist/presets-BITljm96.mjs +120 -0
  213. package/dist/presets-BITljm96.mjs.map +1 -0
  214. package/dist/presets-DzSMwlKj.d.mts +58 -0
  215. package/dist/presets-DzSMwlKj.d.mts.map +1 -0
  216. package/dist/prisma-DJbMt3yf.mjs +628 -0
  217. package/dist/prisma-DJbMt3yf.mjs.map +1 -0
  218. package/dist/prisma-Dg9GoVdj.d.mts +275 -0
  219. package/dist/prisma-Dg9GoVdj.d.mts.map +1 -0
  220. package/dist/queryCachePlugin-7THaI5mt.d.mts +72 -0
  221. package/dist/queryCachePlugin-7THaI5mt.d.mts.map +1 -0
  222. package/dist/queryCachePlugin-DMBnp2Q0.mjs +139 -0
  223. package/dist/queryCachePlugin-DMBnp2Q0.mjs.map +1 -0
  224. package/dist/redis-D-JAeLtm.d.mts +50 -0
  225. package/dist/redis-D-JAeLtm.d.mts.map +1 -0
  226. package/dist/redis-stream-Bdh_vUU8.d.mts +104 -0
  227. package/dist/redis-stream-Bdh_vUU8.d.mts.map +1 -0
  228. package/dist/registry/index.d.mts +12 -0
  229. package/dist/registry/index.d.mts.map +1 -0
  230. package/dist/registry/index.mjs +4 -0
  231. package/dist/requestContext-QQD6ROJc.mjs +56 -0
  232. package/dist/requestContext-QQD6ROJc.mjs.map +1 -0
  233. package/dist/schemaConverter-BwrmWroW.mjs +99 -0
  234. package/dist/schemaConverter-BwrmWroW.mjs.map +1 -0
  235. package/dist/schemas/index.d.mts +64 -0
  236. package/dist/schemas/index.d.mts.map +1 -0
  237. package/dist/schemas/index.mjs +83 -0
  238. package/dist/schemas/index.mjs.map +1 -0
  239. package/dist/scope/index.d.mts +22 -0
  240. package/dist/scope/index.d.mts.map +1 -0
  241. package/dist/scope/index.mjs +66 -0
  242. package/dist/scope/index.mjs.map +1 -0
  243. package/dist/sessionManager-jPKLbHE0.d.mts +187 -0
  244. package/dist/sessionManager-jPKLbHE0.d.mts.map +1 -0
  245. package/dist/sse-B3c3_yZp.mjs +124 -0
  246. package/dist/sse-B3c3_yZp.mjs.map +1 -0
  247. package/dist/testing/index.d.mts +908 -0
  248. package/dist/testing/index.d.mts.map +1 -0
  249. package/dist/testing/index.mjs +1977 -0
  250. package/dist/testing/index.mjs.map +1 -0
  251. package/dist/tracing-Cc7vVQPp.d.mts +71 -0
  252. package/dist/tracing-Cc7vVQPp.d.mts.map +1 -0
  253. package/dist/typeGuards-DhMNLuvU.mjs +10 -0
  254. package/dist/typeGuards-DhMNLuvU.mjs.map +1 -0
  255. package/dist/types/index.d.mts +947 -0
  256. package/dist/types/index.d.mts.map +1 -0
  257. package/dist/types/index.mjs +15 -0
  258. package/dist/types/index.mjs.map +1 -0
  259. package/dist/types-Beqn1Un7.mjs +39 -0
  260. package/dist/types-Beqn1Un7.mjs.map +1 -0
  261. package/dist/types-CIgB7UUl.d.mts +446 -0
  262. package/dist/types-CIgB7UUl.d.mts.map +1 -0
  263. package/dist/types-aYB4V7uN.d.mts +87 -0
  264. package/dist/types-aYB4V7uN.d.mts.map +1 -0
  265. package/dist/utils/index.d.mts +748 -0
  266. package/dist/utils/index.d.mts.map +1 -0
  267. package/dist/utils/index.mjs +6 -0
  268. package/package.json +194 -68
  269. package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
  270. package/dist/adapters/index.d.ts +0 -237
  271. package/dist/adapters/index.js +0 -668
  272. package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
  273. package/dist/audit/index.d.ts +0 -195
  274. package/dist/audit/index.js +0 -319
  275. package/dist/auth/index.d.ts +0 -47
  276. package/dist/auth/index.js +0 -174
  277. package/dist/cli/commands/docs.d.ts +0 -11
  278. package/dist/cli/commands/docs.js +0 -474
  279. package/dist/cli/commands/generate.js +0 -334
  280. package/dist/cli/commands/introspect.d.ts +0 -8
  281. package/dist/cli/commands/introspect.js +0 -338
  282. package/dist/cli/index.d.ts +0 -4
  283. package/dist/cli/index.js +0 -3269
  284. package/dist/core/index.d.ts +0 -220
  285. package/dist/core/index.js +0 -2786
  286. package/dist/createApp-Ce9wl8W9.d.ts +0 -77
  287. package/dist/docs/index.d.ts +0 -166
  288. package/dist/docs/index.js +0 -658
  289. package/dist/errors-8WIxGS_6.d.ts +0 -122
  290. package/dist/events/index.d.ts +0 -117
  291. package/dist/events/index.js +0 -89
  292. package/dist/factory/index.d.ts +0 -38
  293. package/dist/factory/index.js +0 -1652
  294. package/dist/hooks/index.d.ts +0 -4
  295. package/dist/hooks/index.js +0 -199
  296. package/dist/idempotency/index.d.ts +0 -323
  297. package/dist/idempotency/index.js +0 -500
  298. package/dist/index-B4t03KQ0.d.ts +0 -1366
  299. package/dist/index.d.ts +0 -135
  300. package/dist/index.js +0 -4756
  301. package/dist/migrations/index.d.ts +0 -185
  302. package/dist/migrations/index.js +0 -274
  303. package/dist/org/index.d.ts +0 -129
  304. package/dist/org/index.js +0 -220
  305. package/dist/permissions/index.d.ts +0 -144
  306. package/dist/permissions/index.js +0 -103
  307. package/dist/plugins/index.d.ts +0 -46
  308. package/dist/plugins/index.js +0 -1069
  309. package/dist/policies/index.js +0 -196
  310. package/dist/presets/index.js +0 -384
  311. package/dist/presets/multiTenant.d.ts +0 -39
  312. package/dist/presets/multiTenant.js +0 -112
  313. package/dist/registry/index.d.ts +0 -16
  314. package/dist/registry/index.js +0 -253
  315. package/dist/testing/index.d.ts +0 -618
  316. package/dist/testing/index.js +0 -48020
  317. package/dist/types/index.d.ts +0 -4
  318. package/dist/types/index.js +0 -8
  319. package/dist/types-B99TBmFV.d.ts +0 -76
  320. package/dist/types-BvckRbs2.d.ts +0 -143
  321. package/dist/utils/index.d.ts +0 -679
  322. package/dist/utils/index.js +0 -931
package/dist/cli/index.js DELETED
@@ -1,3269 +0,0 @@
1
- import fp from 'fastify-plugin';
2
- import { existsSync, mkdirSync, writeFileSync } from 'fs';
3
- import * as path from 'path';
4
- import { join } from 'path';
5
- import * as fs from 'fs/promises';
6
- import * as readline from 'readline';
7
- import { execSync, spawn } from 'child_process';
8
-
9
- var __defProp = Object.defineProperty;
10
- var __getOwnPropNames = Object.getOwnPropertyNames;
11
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
12
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
13
- }) : x)(function(x) {
14
- if (typeof require !== "undefined") return require.apply(this, arguments);
15
- throw Error('Dynamic require of "' + x + '" is not supported');
16
- });
17
- var __esm = (fn, res) => function __init() {
18
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
19
- };
20
- var __export = (target, all) => {
21
- for (var name in all)
22
- __defProp(target, name, { get: all[name], enumerable: true });
23
- };
24
-
25
- // src/registry/ResourceRegistry.ts
26
- var ResourceRegistry, registryKey, globalScope, resourceRegistry;
27
- var init_ResourceRegistry = __esm({
28
- "src/registry/ResourceRegistry.ts"() {
29
- ResourceRegistry = class {
30
- _resources;
31
- _frozen;
32
- constructor() {
33
- this._resources = /* @__PURE__ */ new Map();
34
- this._frozen = false;
35
- }
36
- /**
37
- * Register a resource
38
- */
39
- register(resource, options = {}) {
40
- if (this._frozen) {
41
- throw new Error(
42
- `Registry frozen. Cannot register '${resource.name}' after startup.`
43
- );
44
- }
45
- if (this._resources.has(resource.name)) {
46
- throw new Error(`Resource '${resource.name}' already registered.`);
47
- }
48
- const entry = {
49
- name: resource.name,
50
- displayName: resource.displayName,
51
- tag: resource.tag,
52
- prefix: resource.prefix,
53
- module: options.module ?? void 0,
54
- adapter: resource.adapter ? {
55
- type: resource.adapter.type,
56
- name: resource.adapter.name
57
- } : null,
58
- permissions: resource.permissions,
59
- presets: resource._appliedPresets ?? [],
60
- routes: [],
61
- // Populated later by getIntrospection()
62
- additionalRoutes: resource.additionalRoutes.map((r) => ({
63
- method: r.method,
64
- path: r.path,
65
- handler: typeof r.handler === "string" ? r.handler : r.handler.name || "anonymous",
66
- summary: r.summary,
67
- description: r.description,
68
- permissions: r.permissions,
69
- wrapHandler: r.wrapHandler,
70
- schema: r.schema
71
- // Include schema for OpenAPI docs
72
- })),
73
- events: Object.keys(resource.events ?? {}),
74
- registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
75
- disableDefaultRoutes: resource.disableDefaultRoutes,
76
- openApiSchemas: options.openApiSchemas,
77
- plugin: resource.toPlugin()
78
- // Store plugin factory
79
- };
80
- this._resources.set(resource.name, entry);
81
- return this;
82
- }
83
- /**
84
- * Get resource by name
85
- */
86
- get(name) {
87
- return this._resources.get(name);
88
- }
89
- /**
90
- * Get all resources
91
- */
92
- getAll() {
93
- return Array.from(this._resources.values());
94
- }
95
- /**
96
- * Get resources by module
97
- */
98
- getByModule(moduleName) {
99
- return this.getAll().filter((r) => r.module === moduleName);
100
- }
101
- /**
102
- * Get resources by preset
103
- */
104
- getByPreset(presetName) {
105
- return this.getAll().filter((r) => r.presets.includes(presetName));
106
- }
107
- /**
108
- * Check if resource exists
109
- */
110
- has(name) {
111
- return this._resources.has(name);
112
- }
113
- /**
114
- * Get registry statistics
115
- */
116
- getStats() {
117
- const resources = this.getAll();
118
- const presetCounts = {};
119
- for (const r of resources) {
120
- for (const preset of r.presets) {
121
- presetCounts[preset] = (presetCounts[preset] ?? 0) + 1;
122
- }
123
- }
124
- return {
125
- totalResources: resources.length,
126
- byModule: this._groupBy(resources, "module"),
127
- presetUsage: presetCounts,
128
- totalRoutes: resources.reduce((sum, r) => {
129
- const defaultRouteCount = r.disableDefaultRoutes ? 0 : 5;
130
- return sum + (r.additionalRoutes?.length ?? 0) + defaultRouteCount;
131
- }, 0),
132
- totalEvents: resources.reduce((sum, r) => sum + (r.events?.length ?? 0), 0)
133
- };
134
- }
135
- /**
136
- * Get full introspection data
137
- */
138
- getIntrospection() {
139
- return {
140
- resources: this.getAll().map((r) => {
141
- const defaultRoutes = r.disableDefaultRoutes ? [] : [
142
- { method: "GET", path: r.prefix, operation: "list" },
143
- { method: "GET", path: `${r.prefix}/:id`, operation: "get" },
144
- { method: "POST", path: r.prefix, operation: "create" },
145
- { method: "PATCH", path: `${r.prefix}/:id`, operation: "update" },
146
- { method: "DELETE", path: `${r.prefix}/:id`, operation: "delete" }
147
- ];
148
- return {
149
- name: r.name,
150
- displayName: r.displayName,
151
- prefix: r.prefix,
152
- module: r.module,
153
- presets: r.presets,
154
- permissions: r.permissions,
155
- routes: [
156
- ...defaultRoutes,
157
- ...r.additionalRoutes?.map((ar) => ({
158
- method: ar.method,
159
- path: `${r.prefix}${ar.path}`,
160
- operation: typeof ar.handler === "string" ? ar.handler : "custom",
161
- handler: typeof ar.handler === "string" ? ar.handler : void 0,
162
- summary: ar.summary
163
- })) ?? []
164
- ],
165
- events: r.events
166
- };
167
- }),
168
- stats: this.getStats(),
169
- generatedAt: (/* @__PURE__ */ new Date()).toISOString()
170
- };
171
- }
172
- /**
173
- * Freeze registry (prevent further registrations)
174
- */
175
- freeze() {
176
- this._frozen = true;
177
- }
178
- /**
179
- * Check if frozen
180
- */
181
- isFrozen() {
182
- return this._frozen;
183
- }
184
- /**
185
- * Unfreeze registry (for testing)
186
- */
187
- _unfreeze() {
188
- this._frozen = false;
189
- }
190
- /**
191
- * Clear all resources (for testing)
192
- */
193
- _clear() {
194
- this._resources.clear();
195
- this._frozen = false;
196
- }
197
- /**
198
- * Group by key
199
- */
200
- _groupBy(arr, key) {
201
- const result = {};
202
- for (const item of arr) {
203
- const k = String(item[key] ?? "uncategorized");
204
- result[k] = (result[k] ?? 0) + 1;
205
- }
206
- return result;
207
- }
208
- };
209
- registryKey = /* @__PURE__ */ Symbol.for("arc.resourceRegistry");
210
- globalScope = globalThis;
211
- resourceRegistry = globalScope[registryKey] ?? new ResourceRegistry();
212
- if (!globalScope[registryKey]) {
213
- globalScope[registryKey] = resourceRegistry;
214
- }
215
- }
216
- });
217
- var introspectionPlugin, introspectionPlugin_default;
218
- var init_introspectionPlugin = __esm({
219
- "src/registry/introspectionPlugin.ts"() {
220
- init_ResourceRegistry();
221
- introspectionPlugin = async (fastify, opts = {}) => {
222
- const {
223
- prefix = "/_resources",
224
- authRoles = ["superadmin"],
225
- enabled = process.env.NODE_ENV !== "production" || process.env.ENABLE_INTROSPECTION === "true"
226
- } = opts;
227
- if (!enabled) {
228
- fastify.log?.info?.("Introspection plugin disabled");
229
- return;
230
- }
231
- const typedFastify = fastify;
232
- const authMiddleware = authRoles.length > 0 && typedFastify.authenticate ? [
233
- typedFastify.authenticate,
234
- typedFastify.authorize?.(...authRoles)
235
- ].filter(Boolean) : [];
236
- await fastify.register(async (instance) => {
237
- instance.get(
238
- "/",
239
- {
240
- preHandler: authMiddleware
241
- },
242
- async (_req, _reply) => {
243
- return resourceRegistry.getIntrospection();
244
- }
245
- );
246
- instance.get(
247
- "/stats",
248
- {
249
- preHandler: authMiddleware
250
- },
251
- async (_req, _reply) => {
252
- return resourceRegistry.getStats();
253
- }
254
- );
255
- instance.get(
256
- "/:name",
257
- {
258
- schema: {
259
- params: {
260
- type: "object",
261
- properties: {
262
- name: { type: "string" }
263
- },
264
- required: ["name"]
265
- }
266
- },
267
- preHandler: authMiddleware
268
- },
269
- async (req, reply) => {
270
- const resource = resourceRegistry.get(req.params.name);
271
- if (!resource) {
272
- return reply.code(404).send({
273
- error: `Resource '${req.params.name}' not found`
274
- });
275
- }
276
- return resource;
277
- }
278
- );
279
- }, { prefix });
280
- fastify.log?.info?.(`Introspection API at ${prefix}`);
281
- };
282
- introspectionPlugin_default = fp(introspectionPlugin, { name: "arc-introspection" });
283
- }
284
- });
285
-
286
- // src/registry/index.ts
287
- var registry_exports = {};
288
- __export(registry_exports, {
289
- ResourceRegistry: () => ResourceRegistry,
290
- introspectionPlugin: () => introspectionPlugin_default,
291
- introspectionPluginFn: () => introspectionPlugin,
292
- resourceRegistry: () => resourceRegistry
293
- });
294
- var init_registry = __esm({
295
- "src/registry/index.ts"() {
296
- init_ResourceRegistry();
297
- init_introspectionPlugin();
298
- }
299
- });
300
- function isTypeScriptProject() {
301
- return existsSync(join(process.cwd(), "tsconfig.json"));
302
- }
303
- function getTemplates(ts) {
304
- return {
305
- model: (name) => `/**
306
- * ${name} Model
307
- * Generated by Arc CLI
308
- */
309
-
310
- import mongoose${ts ? ", { type HydratedDocument }" : ""} from 'mongoose';
311
-
312
- const { Schema } = mongoose;
313
- ${ts ? `
314
- type ${name} = {
315
- name: string;
316
- description?: string;
317
- isActive: boolean;
318
- };
319
-
320
- export type ${name}Document = HydratedDocument<${name}>;
321
- ` : ""}
322
- const ${name.toLowerCase()}Schema = new Schema${ts ? `<${name}>` : ""}(
323
- {
324
- name: { type: String, required: true, trim: true },
325
- description: { type: String, trim: true },
326
- isActive: { type: Boolean, default: true },
327
- },
328
- { timestamps: true }
329
- );
330
-
331
- // Indexes
332
- ${name.toLowerCase()}Schema.index({ name: 1 });
333
- ${name.toLowerCase()}Schema.index({ isActive: 1 });
334
-
335
- const ${name} = mongoose.models.${name}${ts ? ` as mongoose.Model<${name}>` : ""} || mongoose.model${ts ? `<${name}>` : ""}('${name}', ${name.toLowerCase()}Schema);
336
- export default ${name};
337
- `,
338
- repository: (name) => `/**
339
- * ${name} Repository
340
- * Generated by Arc CLI
341
- */
342
-
343
- import {
344
- Repository,
345
- methodRegistryPlugin,
346
- softDeletePlugin,
347
- mongoOperationsPlugin,
348
- } from '@classytic/mongokit';
349
- ${ts ? `import type { ${name}Document } from './${name.toLowerCase()}.model.js';` : ""}
350
- import ${name} from './${name.toLowerCase()}.model.js';
351
-
352
- class ${name}Repository extends Repository${ts ? `<${name}Document>` : ""} {
353
- constructor() {
354
- super(${name}${ts ? " as any" : ""}, [
355
- methodRegistryPlugin(),
356
- softDeletePlugin(),
357
- mongoOperationsPlugin(),
358
- ]);
359
- }
360
-
361
- /**
362
- * Find all active records
363
- */
364
- async findActive() {
365
- return this.Model.find({ isActive: true, deletedAt: null }).lean();
366
- }
367
-
368
- // Add custom repository methods here
369
- }
370
-
371
- const ${name.toLowerCase()}Repository = new ${name}Repository();
372
- export default ${name.toLowerCase()}Repository;
373
- export { ${name}Repository };
374
- `,
375
- controller: (name) => `/**
376
- * ${name} Controller
377
- * Generated by Arc CLI
378
- */
379
-
380
- import { BaseController } from '@classytic/arc';
381
- import ${name.toLowerCase()}Repository from './${name.toLowerCase()}.repository.js';
382
- import { ${name.toLowerCase()}SchemaOptions } from './${name.toLowerCase()}.schemas.js';
383
-
384
- class ${name}Controller extends BaseController {
385
- constructor() {
386
- super(${name.toLowerCase()}Repository${ts ? " as any" : ""}, { schemaOptions: ${name.toLowerCase()}SchemaOptions });
387
- }
388
-
389
- // Add custom controller methods here
390
- }
391
-
392
- const ${name.toLowerCase()}Controller = new ${name}Controller();
393
- export default ${name.toLowerCase()}Controller;
394
- `,
395
- schemas: (name) => `/**
396
- * ${name} Schemas
397
- * Generated by Arc CLI
398
- */
399
-
400
- import ${name} from './${name.toLowerCase()}.model.js';
401
- import { buildCrudSchemasFromModel } from '@classytic/mongokit/utils';
402
-
403
- /**
404
- * CRUD Schemas with Field Rules
405
- */
406
- const crudSchemas = buildCrudSchemasFromModel(${name}, {
407
- strictAdditionalProperties: true,
408
- fieldRules: {
409
- // Mark fields as system-managed (excluded from create/update)
410
- // deletedAt: { systemManaged: true },
411
- },
412
- query: {
413
- filterableFields: {
414
- isActive: 'boolean',
415
- createdAt: 'date',
416
- },
417
- },
418
- });
419
-
420
- // Schema options for controller
421
- export const ${name.toLowerCase()}SchemaOptions${ts ? ": any" : ""} = {
422
- query: {
423
- filterableFields: {
424
- isActive: 'boolean',
425
- createdAt: 'date',
426
- },
427
- },
428
- };
429
-
430
- export default crudSchemas;
431
- `,
432
- resource: (name) => `/**
433
- * ${name} Resource
434
- * Generated by Arc CLI
435
- */
436
-
437
- import { defineResource } from '@classytic/arc';
438
- import { createAdapter } from '#shared/adapter.js';
439
- import { publicReadPermissions } from '#shared/permissions.js';
440
- import ${name} from './${name.toLowerCase()}.model.js';
441
- import ${name.toLowerCase()}Repository from './${name.toLowerCase()}.repository.js';
442
- import ${name.toLowerCase()}Controller from './${name.toLowerCase()}.controller.js';
443
-
444
- const ${name.toLowerCase()}Resource = defineResource({
445
- name: '${name.toLowerCase()}',
446
- displayName: '${name}s',
447
- prefix: '/${name.toLowerCase()}s',
448
-
449
- adapter: createAdapter(${name}, ${name.toLowerCase()}Repository),
450
- controller: ${name.toLowerCase()}Controller,
451
-
452
- presets: ['softDelete'],
453
-
454
- permissions: publicReadPermissions,
455
-
456
- // Add custom routes here:
457
- // additionalRoutes: [
458
- // {
459
- // method: 'GET',
460
- // path: '/custom',
461
- // summary: 'Custom endpoint',
462
- // handler: async (request, reply) => { ... },
463
- // },
464
- // ],
465
- });
466
-
467
- export default ${name.toLowerCase()}Resource;
468
- `,
469
- test: (name) => `/**
470
- * ${name} Tests
471
- * Generated by Arc CLI
472
- */
473
-
474
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
475
- import mongoose from 'mongoose';
476
- import { createAppInstance } from '../src/app.js';
477
- ${ts ? "import type { FastifyInstance } from 'fastify';\n" : ""}
478
- describe('${name} Resource', () => {
479
- let app${ts ? ": FastifyInstance" : ""};
480
-
481
- beforeAll(async () => {
482
- const testDbUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/test-${name.toLowerCase()}';
483
- await mongoose.connect(testDbUri);
484
- app = await createAppInstance();
485
- await app.ready();
486
- });
487
-
488
- afterAll(async () => {
489
- await app.close();
490
- await mongoose.connection.close();
491
- });
492
-
493
- describe('GET /${name.toLowerCase()}s', () => {
494
- it('should return a list', async () => {
495
- const response = await app.inject({
496
- method: 'GET',
497
- url: '/${name.toLowerCase()}s',
498
- });
499
-
500
- expect(response.statusCode).toBe(200);
501
- const body = JSON.parse(response.body);
502
- expect(body).toHaveProperty('docs');
503
- });
504
- });
505
- });
506
- `
507
- };
508
- }
509
- async function generate(type, args) {
510
- if (!type) {
511
- console.error("Error: Missing type argument");
512
- console.log("Usage: arc generate <resource|controller|model|repository|schemas> <name>");
513
- process.exit(1);
514
- }
515
- const [name] = args;
516
- if (!name) {
517
- console.error("Error: Missing name argument");
518
- console.log("Usage: arc generate <type> <name>");
519
- console.log("Example: arc generate resource product");
520
- process.exit(1);
521
- }
522
- const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1);
523
- const lowerName = name.toLowerCase();
524
- const ts = isTypeScriptProject();
525
- const ext = ts ? "ts" : "js";
526
- const templates = getTemplates(ts);
527
- const resourcePath = join(process.cwd(), "src", "resources", lowerName);
528
- switch (type) {
529
- case "resource":
530
- case "r":
531
- await generateResource(capitalizedName, lowerName, resourcePath, templates, ext);
532
- break;
533
- case "controller":
534
- case "c":
535
- await generateFile(capitalizedName, lowerName, resourcePath, "controller", templates.controller, ext);
536
- break;
537
- case "model":
538
- case "m":
539
- await generateFile(capitalizedName, lowerName, resourcePath, "model", templates.model, ext);
540
- break;
541
- case "repository":
542
- case "repo":
543
- await generateFile(capitalizedName, lowerName, resourcePath, "repository", templates.repository, ext);
544
- break;
545
- case "schemas":
546
- case "s":
547
- await generateFile(capitalizedName, lowerName, resourcePath, "schemas", templates.schemas, ext);
548
- break;
549
- default:
550
- console.error(`Unknown type: ${type}`);
551
- console.log("Available types: resource, controller, model, repository, schemas");
552
- process.exit(1);
553
- }
554
- }
555
- async function generateResource(name, lowerName, resourcePath, templates, ext) {
556
- console.log(`
557
- 📦 Generating resource: ${name}...
558
- `);
559
- if (!existsSync(resourcePath)) {
560
- mkdirSync(resourcePath, { recursive: true });
561
- console.log(` 📁 Created: src/resources/${lowerName}/`);
562
- }
563
- const files = {
564
- [`${lowerName}.model.${ext}`]: templates.model(name),
565
- [`${lowerName}.repository.${ext}`]: templates.repository(name),
566
- [`${lowerName}.controller.${ext}`]: templates.controller(name),
567
- [`${lowerName}.schemas.${ext}`]: templates.schemas(name),
568
- [`${lowerName}.resource.${ext}`]: templates.resource(name)
569
- };
570
- for (const [filename, content] of Object.entries(files)) {
571
- const filepath = join(resourcePath, filename);
572
- if (existsSync(filepath)) {
573
- console.warn(` ⚠ Skipped: ${filename} (already exists)`);
574
- } else {
575
- writeFileSync(filepath, content);
576
- console.log(` ✅ Created: ${filename}`);
577
- }
578
- }
579
- const testsDir = join(process.cwd(), "tests");
580
- if (!existsSync(testsDir)) {
581
- mkdirSync(testsDir, { recursive: true });
582
- }
583
- const testPath = join(testsDir, `${lowerName}.test.${ext}`);
584
- if (!existsSync(testPath)) {
585
- writeFileSync(testPath, templates.test(name));
586
- console.log(` ✅ Created: tests/${lowerName}.test.${ext}`);
587
- }
588
- console.log(`
589
- ╔═══════════════════════════════════════════════════════════════╗
590
- ║ ✅ Resource Generated! ║
591
- ╚═══════════════════════════════════════════════════════════════╝
592
-
593
- Next steps:
594
-
595
- 1. Register in src/resources/index.${ext}:
596
- import ${lowerName}Resource from './${lowerName}/${lowerName}.resource.js';
597
-
598
- export const resources = [
599
- // ... existing resources
600
- ${lowerName}Resource,
601
- ];
602
-
603
- 2. Customize the model schema in:
604
- src/resources/${lowerName}/${lowerName}.model.${ext}
605
-
606
- 3. Run tests:
607
- npm test
608
- `);
609
- }
610
- async function generateFile(name, lowerName, resourcePath, fileType, template, ext) {
611
- console.log(`
612
- 📦 Generating ${fileType}: ${name}...
613
- `);
614
- if (!existsSync(resourcePath)) {
615
- mkdirSync(resourcePath, { recursive: true });
616
- console.log(` 📁 Created: src/resources/${lowerName}/`);
617
- }
618
- const filename = `${lowerName}.${fileType}.${ext}`;
619
- const filepath = join(resourcePath, filename);
620
- if (existsSync(filepath)) {
621
- console.error(` ❌ Error: ${filename} already exists`);
622
- process.exit(1);
623
- }
624
- writeFileSync(filepath, template(name));
625
- console.log(` ✅ Created: ${filename}`);
626
- }
627
- async function init(options = {}) {
628
- console.log(`
629
- ╔═══════════════════════════════════════════════════════════════╗
630
- ║ 🔥 Arc Project Setup ║
631
- ║ Resource-Oriented Backend Framework ║
632
- ╚═══════════════════════════════════════════════════════════════╝
633
- `);
634
- const config = await gatherConfig(options);
635
- console.log(`
636
- 📦 Creating project: ${config.name}`);
637
- console.log(` Adapter: ${config.adapter === "mongokit" ? "MongoKit (MongoDB)" : "Custom"}`);
638
- console.log(` Tenant: ${config.tenant === "multi" ? "Multi-tenant" : "Single-tenant"}`);
639
- console.log(` Language: ${config.typescript ? "TypeScript" : "JavaScript"}
640
- `);
641
- const projectPath = path.join(process.cwd(), config.name);
642
- try {
643
- await fs.access(projectPath);
644
- if (!options.force) {
645
- console.error(`❌ Directory "${config.name}" already exists. Use --force to overwrite.`);
646
- process.exit(1);
647
- }
648
- } catch {
649
- }
650
- const packageManager = detectPackageManager();
651
- console.log(`📦 Using package manager: ${packageManager}
652
- `);
653
- await createProjectStructure(projectPath, config);
654
- if (!options.skipInstall) {
655
- console.log("\n📥 Installing dependencies...\n");
656
- await installDependencies(projectPath, config, packageManager);
657
- }
658
- printSuccessMessage(config, options.skipInstall);
659
- }
660
- function detectPackageManager() {
661
- try {
662
- const cwd = process.cwd();
663
- if (existsSync2(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
664
- if (existsSync2(path.join(cwd, "yarn.lock"))) return "yarn";
665
- if (existsSync2(path.join(cwd, "bun.lockb"))) return "bun";
666
- if (existsSync2(path.join(cwd, "package-lock.json"))) return "npm";
667
- } catch {
668
- }
669
- if (isCommandAvailable("pnpm")) return "pnpm";
670
- if (isCommandAvailable("yarn")) return "yarn";
671
- if (isCommandAvailable("bun")) return "bun";
672
- return "npm";
673
- }
674
- function isCommandAvailable(command) {
675
- try {
676
- execSync(`${command} --version`, { stdio: "ignore" });
677
- return true;
678
- } catch {
679
- return false;
680
- }
681
- }
682
- function existsSync2(filePath) {
683
- try {
684
- __require("fs").accessSync(filePath);
685
- return true;
686
- } catch {
687
- return false;
688
- }
689
- }
690
- async function installDependencies(projectPath, config, pm) {
691
- const deps = [
692
- "@classytic/arc@latest",
693
- "fastify@latest",
694
- "@fastify/cors@latest",
695
- "@fastify/helmet@latest",
696
- "@fastify/jwt@latest",
697
- "@fastify/rate-limit@latest",
698
- "@fastify/sensible@latest",
699
- "@fastify/under-pressure@latest",
700
- "bcryptjs@latest",
701
- "dotenv@latest",
702
- "jsonwebtoken@latest"
703
- ];
704
- if (config.adapter === "mongokit") {
705
- deps.push("@classytic/mongokit@latest", "mongoose@latest");
706
- }
707
- const devDeps = [
708
- "vitest@latest",
709
- "pino-pretty@latest"
710
- ];
711
- if (config.typescript) {
712
- devDeps.push(
713
- "typescript@latest",
714
- "@types/node@latest",
715
- "@types/jsonwebtoken@latest",
716
- "tsx@latest"
717
- );
718
- }
719
- const installCmd = getInstallCommand(pm, deps, false);
720
- const installDevCmd = getInstallCommand(pm, devDeps, true);
721
- console.log(` Installing dependencies...`);
722
- await runCommand(installCmd, projectPath);
723
- console.log(` Installing dev dependencies...`);
724
- await runCommand(installDevCmd, projectPath);
725
- console.log(`
726
- ✅ Dependencies installed successfully!`);
727
- }
728
- function getInstallCommand(pm, packages, isDev) {
729
- const pkgList = packages.join(" ");
730
- switch (pm) {
731
- case "pnpm":
732
- return `pnpm add ${isDev ? "-D" : ""} ${pkgList}`;
733
- case "yarn":
734
- return `yarn add ${isDev ? "-D" : ""} ${pkgList}`;
735
- case "bun":
736
- return `bun add ${isDev ? "-d" : ""} ${pkgList}`;
737
- case "npm":
738
- default:
739
- return `npm install ${isDev ? "--save-dev" : ""} ${pkgList}`;
740
- }
741
- }
742
- function runCommand(command, cwd) {
743
- return new Promise((resolve, reject) => {
744
- const isWindows = process.platform === "win32";
745
- const shell = isWindows ? "cmd" : "/bin/sh";
746
- const shellFlag = isWindows ? "/c" : "-c";
747
- const child = spawn(shell, [shellFlag, command], {
748
- cwd,
749
- stdio: "inherit",
750
- env: { ...process.env, FORCE_COLOR: "1" }
751
- });
752
- child.on("close", (code) => {
753
- if (code === 0) {
754
- resolve();
755
- } else {
756
- reject(new Error(`Command failed with exit code ${code}`));
757
- }
758
- });
759
- child.on("error", reject);
760
- });
761
- }
762
- async function gatherConfig(options) {
763
- const rl = readline.createInterface({
764
- input: process.stdin,
765
- output: process.stdout
766
- });
767
- const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
768
- try {
769
- const name = options.name || await question("📁 Project name: ") || "my-arc-app";
770
- let adapter = options.adapter || "mongokit";
771
- if (!options.adapter) {
772
- const adapterChoice = await question("🗄️ Database adapter [1=MongoKit (recommended), 2=Custom]: ");
773
- adapter = adapterChoice === "2" ? "custom" : "mongokit";
774
- }
775
- let tenant = options.tenant || "single";
776
- if (!options.tenant) {
777
- const tenantChoice = await question("🏢 Tenant mode [1=Single-tenant, 2=Multi-tenant]: ");
778
- tenant = tenantChoice === "2" ? "multi" : "single";
779
- }
780
- let typescript = options.typescript ?? true;
781
- if (options.typescript === void 0) {
782
- const tsChoice = await question("�� Language [1=TypeScript (recommended), 2=JavaScript]: ");
783
- typescript = tsChoice !== "2";
784
- }
785
- return { name, adapter, tenant, typescript };
786
- } finally {
787
- rl.close();
788
- }
789
- }
790
- async function createProjectStructure(projectPath, config) {
791
- const ext = config.typescript ? "ts" : "js";
792
- const dirs = [
793
- "",
794
- "src",
795
- "src/config",
796
- // Config & env loading (import first!)
797
- "src/shared",
798
- // Shared utilities (adapters, presets, permissions)
799
- "src/shared/presets",
800
- // Preset definitions
801
- "src/plugins",
802
- // App-specific plugins
803
- "src/resources",
804
- // Resource definitions
805
- "src/resources/user",
806
- // User resource (user.model, user.repository, etc.)
807
- "src/resources/auth",
808
- // Auth resource (auth.resource, auth.handlers, etc.)
809
- "src/resources/example",
810
- // Example resource
811
- "tests"
812
- ];
813
- for (const dir of dirs) {
814
- await fs.mkdir(path.join(projectPath, dir), { recursive: true });
815
- console.log(` 📁 Created: ${dir || "/"}`);
816
- }
817
- const files = {
818
- "package.json": packageJsonTemplate(config),
819
- ".gitignore": gitignoreTemplate(),
820
- ".env.example": envExampleTemplate(config),
821
- ".env.dev": envDevTemplate(config),
822
- "README.md": readmeTemplate(config)
823
- };
824
- if (config.typescript) {
825
- files["tsconfig.json"] = tsconfigTemplate();
826
- }
827
- files["vitest.config.ts"] = vitestConfigTemplate(config);
828
- files[`src/config/env.${ext}`] = envLoaderTemplate(config);
829
- files[`src/config/index.${ext}`] = configTemplate(config);
830
- files[`src/app.${ext}`] = appTemplate(config);
831
- files[`src/index.${ext}`] = indexTemplate(config);
832
- files[`src/shared/index.${ext}`] = sharedIndexTemplate(config);
833
- files[`src/shared/adapter.${ext}`] = config.adapter === "mongokit" ? createAdapterTemplate(config) : customAdapterTemplate(config);
834
- files[`src/shared/permissions.${ext}`] = permissionsTemplate(config);
835
- if (config.tenant === "multi") {
836
- files[`src/shared/presets/index.${ext}`] = presetsMultiTenantTemplate(config);
837
- files[`src/shared/presets/flexible-multi-tenant.${ext}`] = flexibleMultiTenantPresetTemplate(config);
838
- } else {
839
- files[`src/shared/presets/index.${ext}`] = presetsSingleTenantTemplate(config);
840
- }
841
- files[`src/plugins/index.${ext}`] = pluginsIndexTemplate(config);
842
- files[`src/resources/index.${ext}`] = resourcesIndexTemplate(config);
843
- files[`src/resources/user/user.model.${ext}`] = userModelTemplate(config);
844
- files[`src/resources/user/user.repository.${ext}`] = userRepositoryTemplate(config);
845
- files[`src/resources/user/user.controller.${ext}`] = userControllerTemplate(config);
846
- files[`src/resources/auth/auth.resource.${ext}`] = authResourceTemplate(config);
847
- files[`src/resources/auth/auth.handlers.${ext}`] = authHandlersTemplate(config);
848
- files[`src/resources/auth/auth.schemas.${ext}`] = authSchemasTemplate();
849
- files[`src/resources/example/example.model.${ext}`] = exampleModelTemplate(config);
850
- files[`src/resources/example/example.repository.${ext}`] = exampleRepositoryTemplate(config);
851
- files[`src/resources/example/example.resource.${ext}`] = exampleResourceTemplate(config);
852
- files[`src/resources/example/example.controller.${ext}`] = exampleControllerTemplate(config);
853
- files[`src/resources/example/example.schemas.${ext}`] = exampleSchemasTemplate(config);
854
- files[`tests/example.test.${ext}`] = exampleTestTemplate(config);
855
- files[`tests/auth.test.${ext}`] = authTestTemplate(config);
856
- for (const [filePath, content] of Object.entries(files)) {
857
- const fullPath = path.join(projectPath, filePath);
858
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
859
- await fs.writeFile(fullPath, content);
860
- console.log(` ✅ Created: ${filePath}`);
861
- }
862
- }
863
- function packageJsonTemplate(config) {
864
- const scripts = config.typescript ? {
865
- dev: "tsx watch src/index.ts",
866
- build: "tsc",
867
- start: "node dist/index.js",
868
- test: "vitest run",
869
- "test:watch": "vitest"
870
- } : {
871
- dev: "node --watch src/index.js",
872
- start: "node src/index.js",
873
- test: "vitest run",
874
- "test:watch": "vitest"
875
- };
876
- const imports = config.typescript ? {
877
- "#config/*": "./dist/config/*",
878
- "#shared/*": "./dist/shared/*",
879
- "#resources/*": "./dist/resources/*",
880
- "#plugins/*": "./dist/plugins/*"
881
- } : {
882
- "#config/*": "./src/config/*",
883
- "#shared/*": "./src/shared/*",
884
- "#resources/*": "./src/resources/*",
885
- "#plugins/*": "./src/plugins/*"
886
- };
887
- return JSON.stringify(
888
- {
889
- name: config.name,
890
- version: "1.0.0",
891
- type: "module",
892
- main: config.typescript ? "dist/index.js" : "src/index.js",
893
- imports,
894
- scripts,
895
- engines: {
896
- node: ">=20"
897
- }
898
- },
899
- null,
900
- 2
901
- );
902
- }
903
- function tsconfigTemplate() {
904
- return JSON.stringify(
905
- {
906
- compilerOptions: {
907
- target: "ES2022",
908
- module: "NodeNext",
909
- moduleResolution: "NodeNext",
910
- lib: ["ES2022"],
911
- outDir: "./dist",
912
- rootDir: "./src",
913
- strict: true,
914
- esModuleInterop: true,
915
- skipLibCheck: true,
916
- forceConsistentCasingInFileNames: true,
917
- declaration: true,
918
- declarationMap: true,
919
- sourceMap: true,
920
- resolveJsonModule: true,
921
- paths: {
922
- "#shared/*": ["./src/shared/*"],
923
- "#resources/*": ["./src/resources/*"],
924
- "#config/*": ["./src/config/*"],
925
- "#plugins/*": ["./src/plugins/*"]
926
- }
927
- },
928
- include: ["src/**/*"],
929
- exclude: ["node_modules", "dist"]
930
- },
931
- null,
932
- 2
933
- );
934
- }
935
- function vitestConfigTemplate(config) {
936
- const srcDir = config.typescript ? "./src" : "./src";
937
- return `import { defineConfig } from 'vitest/config';
938
- import { resolve } from 'path';
939
-
940
- export default defineConfig({
941
- test: {
942
- globals: true,
943
- environment: 'node',
944
- },
945
- resolve: {
946
- alias: {
947
- '#config': resolve(__dirname, '${srcDir}/config'),
948
- '#shared': resolve(__dirname, '${srcDir}/shared'),
949
- '#resources': resolve(__dirname, '${srcDir}/resources'),
950
- '#plugins': resolve(__dirname, '${srcDir}/plugins'),
951
- },
952
- },
953
- });
954
- `;
955
- }
956
- function gitignoreTemplate() {
957
- return `# Dependencies
958
- node_modules/
959
-
960
- # Build
961
- dist/
962
- *.js.map
963
-
964
- # Environment
965
- .env
966
- .env.local
967
- .env.*.local
968
-
969
- # IDE
970
- .vscode/
971
- .idea/
972
- *.swp
973
- *.swo
974
-
975
- # OS
976
- .DS_Store
977
- Thumbs.db
978
-
979
- # Logs
980
- *.log
981
- npm-debug.log*
982
-
983
- # Test coverage
984
- coverage/
985
- `;
986
- }
987
- function envExampleTemplate(config) {
988
- let content = `# Server
989
- PORT=8040
990
- HOST=0.0.0.0
991
- NODE_ENV=development
992
-
993
- # JWT
994
- JWT_SECRET=your-32-character-minimum-secret-here
995
- `;
996
- if (config.adapter === "mongokit") {
997
- content += `
998
- # MongoDB
999
- MONGODB_URI=mongodb://localhost:27017/${config.name}
1000
- `;
1001
- }
1002
- if (config.tenant === "multi") {
1003
- content += `
1004
- # Multi-tenant
1005
- DEFAULT_ORG_ID=
1006
- `;
1007
- }
1008
- return content;
1009
- }
1010
- function readmeTemplate(config) {
1011
- const ext = config.typescript ? "ts" : "js";
1012
- return `# ${config.name}
1013
-
1014
- Built with [Arc](https://github.com/classytic/arc) - Resource-Oriented Backend Framework
1015
-
1016
- ## Quick Start
1017
-
1018
- \`\`\`bash
1019
- # Install dependencies
1020
- npm install
1021
-
1022
- # Start development server (uses .env.dev)
1023
- npm run dev
1024
-
1025
- # Run tests
1026
- npm test
1027
- \`\`\`
1028
-
1029
- ## Project Structure
1030
-
1031
- \`\`\`
1032
- src/
1033
- ├── config/ # Configuration (loaded first)
1034
- │ ├── env.${ext} # Env loader (import first!)
1035
- │ └── index.${ext} # App config
1036
- ├── shared/ # Shared utilities
1037
- │ ├── adapter.${ext} # ${config.adapter === "mongokit" ? "MongoKit adapter factory" : "Custom adapter"}
1038
- │ ├── permissions.${ext} # Permission helpers
1039
- │ └── presets/ # ${config.tenant === "multi" ? "Multi-tenant presets" : "Standard presets"}
1040
- ├── plugins/ # App-specific plugins
1041
- │ └── index.${ext} # Plugin registry
1042
- ├── resources/ # API Resources
1043
- │ ├── index.${ext} # Resource registry
1044
- │ └── example/ # Example resource
1045
- │ ├── index.${ext} # Resource definition
1046
- │ ├── model.${ext} # Mongoose schema
1047
- │ └── repository.${ext} # MongoKit repository
1048
- ├── app.${ext} # App factory (reusable)
1049
- └── index.${ext} # Server entry point
1050
- tests/
1051
- └── example.test.${ext} # Example tests
1052
- \`\`\`
1053
-
1054
- ## Architecture
1055
-
1056
- ### Entry Points
1057
-
1058
- - **\`src/index.${ext}\`** - HTTP server entry point
1059
- - **\`src/app.${ext}\`** - App factory (import for workers/tests)
1060
-
1061
- \`\`\`${config.typescript ? "typescript" : "javascript"}
1062
- // For workers or custom entry points:
1063
- import { createAppInstance } from './app.js';
1064
-
1065
- const app = await createAppInstance();
1066
- // Use app for your worker logic
1067
- \`\`\`
1068
-
1069
- ### Adding Resources
1070
-
1071
- 1. Create a new folder in \`src/resources/\`:
1072
-
1073
- \`\`\`
1074
- src/resources/product/
1075
- ├── index.${ext} # Resource definition
1076
- ├── model.${ext} # Mongoose schema
1077
- └── repository.${ext} # MongoKit repository
1078
- \`\`\`
1079
-
1080
- 2. Register in \`src/resources/index.${ext}\`:
1081
-
1082
- \`\`\`${config.typescript ? "typescript" : "javascript"}
1083
- import productResource from './product/index.js';
1084
-
1085
- export const resources = [
1086
- exampleResource,
1087
- productResource, // Add here
1088
- ];
1089
- \`\`\`
1090
-
1091
- ### Adding Plugins
1092
-
1093
- Add custom plugins in \`src/plugins/index.${ext}\`:
1094
-
1095
- \`\`\`${config.typescript ? "typescript" : "javascript"}
1096
- export async function registerPlugins(app, deps) {
1097
- const { config } = deps; // Explicit dependency injection
1098
-
1099
- await app.register(myCustomPlugin, { ...options });
1100
- }
1101
- \`\`\`
1102
-
1103
- ## CLI Commands
1104
-
1105
- \`\`\`bash
1106
- # Generate a new resource
1107
- arc generate resource product
1108
-
1109
- # Introspect existing schema
1110
- arc introspect
1111
-
1112
- # Generate API docs
1113
- arc docs
1114
- \`\`\`
1115
-
1116
- ## Environment Files
1117
-
1118
- - \`.env.dev\` - Development (default)
1119
- - \`.env.test\` - Testing
1120
- - \`.env.prod\` - Production
1121
- - \`.env\` - Fallback
1122
-
1123
- ## API Documentation
1124
-
1125
- API documentation is available via Scalar UI:
1126
-
1127
- - **Interactive UI**: [http://localhost:8040/docs](http://localhost:8040/docs)
1128
- - **OpenAPI Spec**: [http://localhost:8040/_docs/openapi.json](http://localhost:8040/_docs/openapi.json)
1129
-
1130
- ## API Endpoints
1131
-
1132
- | Method | Endpoint | Description |
1133
- |--------|----------|-------------|
1134
- | GET | /docs | API documentation (Scalar UI) |
1135
- | GET | /_docs/openapi.json | OpenAPI 3.0 spec |
1136
- | GET | /examples | List all |
1137
- | GET | /examples/:id | Get by ID |
1138
- | POST | /examples | Create |
1139
- | PATCH | /examples/:id | Update |
1140
- | DELETE | /examples/:id | Delete |
1141
- `;
1142
- }
1143
- function indexTemplate(config) {
1144
- const ts = config.typescript;
1145
- return `/**
1146
- * ${config.name} - Server Entry Point
1147
- * Generated by Arc CLI
1148
- *
1149
- * This file starts the HTTP server.
1150
- * For workers or other entry points, import createAppInstance from './app.js'
1151
- */
1152
-
1153
- // Load environment FIRST (before any other imports)
1154
- import '#config/env.js';
1155
-
1156
- import config from '#config/index.js';
1157
- ${config.adapter === "mongokit" ? "import mongoose from 'mongoose';" : ""}
1158
- import { createAppInstance } from './app.js';
1159
-
1160
- async function main()${ts ? ": Promise<void>" : ""} {
1161
- console.log(\`🔧 Environment: \${config.env}\`);
1162
- ${config.adapter === "mongokit" ? `
1163
- // Connect to MongoDB
1164
- await mongoose.connect(config.database.uri);
1165
- console.log('📦 Connected to MongoDB');
1166
- ` : ""}
1167
- // Create and configure app
1168
- const app = await createAppInstance();
1169
-
1170
- // Start server
1171
- await app.listen({ port: config.server.port, host: config.server.host });
1172
- console.log(\`🚀 Server running at http://\${config.server.host}:\${config.server.port}\`);
1173
- }
1174
-
1175
- main().catch((err) => {
1176
- console.error('❌ Failed to start server:', err);
1177
- process.exit(1);
1178
- });
1179
- `;
1180
- }
1181
- function appTemplate(config) {
1182
- const ts = config.typescript;
1183
- const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
1184
- return `/**
1185
- * ${config.name} - App Factory
1186
- * Generated by Arc CLI
1187
- *
1188
- * Creates and configures the Fastify app instance.
1189
- * Can be imported by:
1190
- * - index.ts (HTTP server)
1191
- * - worker.ts (background workers)
1192
- * - tests (integration tests)
1193
- */
1194
-
1195
- ${typeImport}import config from '#config/index.js';
1196
- import { createApp } from '@classytic/arc/factory';
1197
-
1198
- // App-specific plugins
1199
- import { registerPlugins } from '#plugins/index.js';
1200
-
1201
- // Resource registry
1202
- import { registerResources } from '#resources/index.js';
1203
-
1204
- /**
1205
- * Create a fully configured app instance
1206
- *
1207
- * @returns Configured Fastify instance ready to use
1208
- */
1209
- export async function createAppInstance()${ts ? ": Promise<FastifyInstance>" : ""} {
1210
- // Create Arc app with base configuration
1211
- const app = await createApp({
1212
- preset: config.env === 'production' ? 'production' : 'development',
1213
- auth: {
1214
- jwt: { secret: config.jwt.secret },
1215
- },
1216
- cors: {
1217
- origin: config.cors.origins,
1218
- methods: config.cors.methods,
1219
- allowedHeaders: config.cors.allowedHeaders,
1220
- credentials: config.cors.credentials,
1221
- },
1222
- });
1223
-
1224
- // Register app-specific plugins (explicit dependency injection)
1225
- await registerPlugins(app, { config });
1226
-
1227
- // Register all resources
1228
- await registerResources(app);
1229
-
1230
- return app;
1231
- }
1232
-
1233
- export default createAppInstance;
1234
- `;
1235
- }
1236
- function envLoaderTemplate(config) {
1237
- const ts = config.typescript;
1238
- return `/**
1239
- * Environment Loader
1240
- *
1241
- * MUST be imported FIRST before any other imports.
1242
- * Loads .env files based on NODE_ENV.
1243
- *
1244
- * Usage:
1245
- * import './config/env.js'; // First line of entry point
1246
- */
1247
-
1248
- import dotenv from 'dotenv';
1249
- import { existsSync } from 'node:fs';
1250
- import { resolve } from 'node:path';
1251
-
1252
- /**
1253
- * Normalize environment string to short form
1254
- */
1255
- function normalizeEnv(env${ts ? ": string | undefined" : ""})${ts ? ": string" : ""} {
1256
- const normalized = (env || '').toLowerCase();
1257
- if (normalized === 'production' || normalized === 'prod') return 'prod';
1258
- if (normalized === 'test' || normalized === 'qa') return 'test';
1259
- return 'dev';
1260
- }
1261
-
1262
- // Determine environment
1263
- const env = normalizeEnv(process.env.NODE_ENV);
1264
-
1265
- // Load environment-specific .env file
1266
- const envFile = resolve(process.cwd(), \`.env.\${env}\`);
1267
- const defaultEnvFile = resolve(process.cwd(), '.env');
1268
-
1269
- if (existsSync(envFile)) {
1270
- dotenv.config({ path: envFile });
1271
- console.log(\`📄 Loaded: .env.\${env}\`);
1272
- } else if (existsSync(defaultEnvFile)) {
1273
- dotenv.config({ path: defaultEnvFile });
1274
- console.log('📄 Loaded: .env');
1275
- } else {
1276
- console.warn('⚠️ No .env file found');
1277
- }
1278
-
1279
- // Export for reference
1280
- export const ENV = env;
1281
- `;
1282
- }
1283
- function envDevTemplate(config) {
1284
- let content = `# Development Environment
1285
- NODE_ENV=development
1286
-
1287
- # Server
1288
- PORT=8040
1289
- HOST=0.0.0.0
1290
-
1291
- # JWT
1292
- JWT_SECRET=dev-secret-change-in-production-min-32-chars
1293
- JWT_EXPIRES_IN=7d
1294
-
1295
- # CORS - Allowed origins
1296
- # Options:
1297
- # * = allow all origins (not recommended for production)
1298
- # Comma-separated list = specific origins only
1299
- CORS_ORIGINS=http://localhost:3000,http://localhost:5173
1300
- `;
1301
- if (config.adapter === "mongokit") {
1302
- content += `
1303
- # MongoDB
1304
- MONGODB_URI=mongodb://localhost:27017/${config.name}
1305
- `;
1306
- }
1307
- if (config.tenant === "multi") {
1308
- content += `
1309
- # Multi-tenant
1310
- ORG_HEADER=x-organization-id
1311
- `;
1312
- }
1313
- return content;
1314
- }
1315
- function pluginsIndexTemplate(config) {
1316
- const ts = config.typescript;
1317
- const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
1318
- const configType = ts ? ": { config: AppConfig }" : "";
1319
- const appType = ts ? ": FastifyInstance" : "";
1320
- let content = `/**
1321
- * App Plugins Registry
1322
- *
1323
- * Register your app-specific plugins here.
1324
- * Dependencies are passed explicitly (no shims, no magic).
1325
- */
1326
-
1327
- ${typeImport}${ts ? "import type { AppConfig } from '../config/index.js';\n" : ""}import { openApiPlugin, scalarPlugin } from '@classytic/arc/docs';
1328
- `;
1329
- if (config.tenant === "multi") {
1330
- content += `import { orgScopePlugin } from '@classytic/arc/org';
1331
- `;
1332
- }
1333
- content += `
1334
- /**
1335
- * Register all app-specific plugins
1336
- *
1337
- * @param app - Fastify instance
1338
- * @param deps - Explicit dependencies (config, services, etc.)
1339
- */
1340
- export async function registerPlugins(
1341
- app${appType},
1342
- deps${configType}
1343
- )${ts ? ": Promise<void>" : ""} {
1344
- const { config } = deps;
1345
-
1346
- // API Documentation (Scalar UI)
1347
- // OpenAPI spec: /_docs/openapi.json
1348
- // Scalar UI: /docs
1349
- await app.register(openApiPlugin, {
1350
- title: '${config.name} API',
1351
- version: '1.0.0',
1352
- description: 'API documentation for ${config.name}',
1353
- });
1354
- await app.register(scalarPlugin, {
1355
- routePrefix: '/docs',
1356
- theme: 'default',
1357
- });
1358
- `;
1359
- if (config.tenant === "multi") {
1360
- content += `
1361
- // Multi-tenant org scope
1362
- await app.register(orgScopePlugin, {
1363
- header: config.org?.header || 'x-organization-id',
1364
- bypassRoles: ['superadmin', 'admin'],
1365
- });
1366
- `;
1367
- }
1368
- content += `
1369
- // Add your custom plugins here:
1370
- // await app.register(myCustomPlugin, { ...options });
1371
- }
1372
- `;
1373
- return content;
1374
- }
1375
- function resourcesIndexTemplate(config) {
1376
- const ts = config.typescript;
1377
- const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
1378
- const appType = ts ? ": FastifyInstance" : "";
1379
- return `/**
1380
- * Resources Registry
1381
- *
1382
- * Central registry for all API resources.
1383
- * Flat structure - no barrels, direct imports.
1384
- */
1385
-
1386
- ${typeImport}
1387
- // Auth resources (register, login, /users/me)
1388
- import { authResource, userProfileResource } from './auth/auth.resource.js';
1389
-
1390
- // App resources
1391
- import exampleResource from './example/example.resource.js';
1392
-
1393
- // Add more resources here:
1394
- // import productResource from './product/product.resource.js';
1395
-
1396
- /**
1397
- * All registered resources
1398
- */
1399
- export const resources = [
1400
- authResource,
1401
- userProfileResource,
1402
- exampleResource,
1403
- ]${ts ? " as const" : ""};
1404
-
1405
- /**
1406
- * Register all resources with the app
1407
- */
1408
- export async function registerResources(app${appType})${ts ? ": Promise<void>" : ""} {
1409
- for (const resource of resources) {
1410
- await app.register(resource.toPlugin());
1411
- }
1412
- }
1413
- `;
1414
- }
1415
- function sharedIndexTemplate(config) {
1416
- const ts = config.typescript;
1417
- return `/**
1418
- * Shared Utilities
1419
- *
1420
- * Central exports for resource definitions.
1421
- * Import from here for clean, consistent code.
1422
- */
1423
-
1424
- // Adapter factory
1425
- export { createAdapter } from './adapter.js';
1426
-
1427
- // Core Arc exports
1428
- export { createMongooseAdapter, defineResource } from '@classytic/arc';
1429
-
1430
- // Permission helpers
1431
- export {
1432
- allowPublic,
1433
- requireAuth,
1434
- requireRoles,
1435
- requireOwnership,
1436
- allOf,
1437
- anyOf,
1438
- denyAll,
1439
- when,${ts ? "\n type PermissionCheck," : ""}
1440
- } from '@classytic/arc/permissions';
1441
-
1442
- // Application permissions
1443
- export * from './permissions.js';
1444
-
1445
- // Presets
1446
- export * from './presets/index.js';
1447
- `;
1448
- }
1449
- function createAdapterTemplate(config) {
1450
- const ts = config.typescript;
1451
- return `/**
1452
- * MongoKit Adapter Factory
1453
- *
1454
- * Creates Arc adapters using MongoKit repositories.
1455
- * The repository handles query parsing via MongoKit's built-in QueryParser.
1456
- */
1457
-
1458
- import { createMongooseAdapter } from '@classytic/arc';
1459
- ${ts ? "import type { Model } from 'mongoose';\nimport type { Repository } from '@classytic/mongokit';" : ""}
1460
-
1461
- /**
1462
- * Create a MongoKit-powered adapter for a resource
1463
- *
1464
- * Note: Query parsing is handled by MongoKit's Repository class.
1465
- * Just pass the model and repository - Arc handles the rest.
1466
- */
1467
- export function createAdapter${ts ? "<TDoc, TRepo extends Repository<TDoc>>" : ""}(
1468
- model${ts ? ": Model<TDoc>" : ""},
1469
- repository${ts ? ": TRepo" : ""}
1470
- )${ts ? ": ReturnType<typeof createMongooseAdapter>" : ""} {
1471
- return createMongooseAdapter({
1472
- model,
1473
- repository,
1474
- });
1475
- }
1476
- `;
1477
- }
1478
- function customAdapterTemplate(config) {
1479
- const ts = config.typescript;
1480
- return `/**
1481
- * Custom Adapter Factory
1482
- *
1483
- * Implement your own database adapter here.
1484
- */
1485
-
1486
- import { createMongooseAdapter } from '@classytic/arc';
1487
- ${ts ? "import type { Model } from 'mongoose';" : ""}
1488
-
1489
- /**
1490
- * Create a custom adapter for a resource
1491
- *
1492
- * Implement this based on your database choice:
1493
- * - Prisma: Use @classytic/prismakit (coming soon)
1494
- * - Drizzle: Create custom adapter
1495
- * - Raw SQL: Create custom adapter
1496
- */
1497
- export function createAdapter${ts ? "<TDoc>" : ""}(
1498
- model${ts ? ": Model<TDoc>" : ""},
1499
- repository${ts ? ": any" : ""}
1500
- )${ts ? ": ReturnType<typeof createMongooseAdapter>" : ""} {
1501
- // TODO: Implement your custom adapter
1502
- return createMongooseAdapter({
1503
- model,
1504
- repository,
1505
- });
1506
- }
1507
- `;
1508
- }
1509
- function presetsMultiTenantTemplate(config) {
1510
- const ts = config.typescript;
1511
- return `/**
1512
- * Arc Presets - Multi-Tenant Configuration
1513
- *
1514
- * Pre-configured presets for multi-tenant applications.
1515
- * Includes both strict and flexible tenant isolation options.
1516
- */
1517
-
1518
- import {
1519
- multiTenantPreset,
1520
- ownedByUserPreset,
1521
- softDeletePreset,
1522
- slugLookupPreset,
1523
- } from '@classytic/arc/presets';
1524
-
1525
- // Flexible preset for mixed public/private routes
1526
- export { flexibleMultiTenantPreset } from './flexible-multi-tenant.js';
1527
-
1528
- /**
1529
- * Organization-scoped preset (STRICT)
1530
- * Always requires auth, always filters by organizationId.
1531
- * Use for admin-only resources.
1532
- */
1533
- export const orgScoped = multiTenantPreset({
1534
- tenantField: 'organizationId',
1535
- bypassRoles: ['superadmin', 'admin'],
1536
- });
1537
-
1538
- /**
1539
- * Owned by creator preset
1540
- * Filters queries by createdBy field.
1541
- */
1542
- export const ownedByCreator = ownedByUserPreset({
1543
- ownerField: 'createdBy',
1544
- });
1545
-
1546
- /**
1547
- * Owned by user preset
1548
- * For resources where userId references the owner.
1549
- */
1550
- export const ownedByUser = ownedByUserPreset({
1551
- ownerField: 'userId',
1552
- });
1553
-
1554
- /**
1555
- * Soft delete preset
1556
- * Adds deletedAt filtering and restore endpoint.
1557
- */
1558
- export const softDelete = softDeletePreset();
1559
-
1560
- /**
1561
- * Slug lookup preset
1562
- * Enables GET by slug in addition to ID.
1563
- */
1564
- export const slugLookup = slugLookupPreset();
1565
-
1566
- // Export all presets
1567
- export const presets = {
1568
- orgScoped,
1569
- ownedByCreator,
1570
- ownedByUser,
1571
- softDelete,
1572
- slugLookup,
1573
- }${ts ? " as const" : ""};
1574
-
1575
- export default presets;
1576
- `;
1577
- }
1578
- function presetsSingleTenantTemplate(config) {
1579
- const ts = config.typescript;
1580
- return `/**
1581
- * Arc Presets - Single-Tenant Configuration
1582
- *
1583
- * Pre-configured presets for single-tenant applications.
1584
- */
1585
-
1586
- import {
1587
- ownedByUserPreset,
1588
- softDeletePreset,
1589
- slugLookupPreset,
1590
- } from '@classytic/arc/presets';
1591
-
1592
- /**
1593
- * Owned by creator preset
1594
- * Filters queries by createdBy field.
1595
- */
1596
- export const ownedByCreator = ownedByUserPreset({
1597
- ownerField: 'createdBy',
1598
- });
1599
-
1600
- /**
1601
- * Owned by user preset
1602
- * For resources where userId references the owner.
1603
- */
1604
- export const ownedByUser = ownedByUserPreset({
1605
- ownerField: 'userId',
1606
- });
1607
-
1608
- /**
1609
- * Soft delete preset
1610
- * Adds deletedAt filtering and restore endpoint.
1611
- */
1612
- export const softDelete = softDeletePreset();
1613
-
1614
- /**
1615
- * Slug lookup preset
1616
- * Enables GET by slug in addition to ID.
1617
- */
1618
- export const slugLookup = slugLookupPreset();
1619
-
1620
- // Export all presets
1621
- export const presets = {
1622
- ownedByCreator,
1623
- ownedByUser,
1624
- softDelete,
1625
- slugLookup,
1626
- }${ts ? " as const" : ""};
1627
-
1628
- export default presets;
1629
- `;
1630
- }
1631
- function flexibleMultiTenantPresetTemplate(config) {
1632
- const ts = config.typescript;
1633
- const typeAnnotations = ts ? `
1634
- interface FlexibleMultiTenantOptions {
1635
- tenantField?: string;
1636
- bypassRoles?: string[];
1637
- extractOrganizationId?: (request: any) => string | null;
1638
- }
1639
-
1640
- interface PresetMiddlewares {
1641
- list: ((request: any, reply: any) => Promise<void>)[];
1642
- get: ((request: any, reply: any) => Promise<void>)[];
1643
- create: ((request: any, reply: any) => Promise<void>)[];
1644
- update: ((request: any, reply: any) => Promise<void>)[];
1645
- delete: ((request: any, reply: any) => Promise<void>)[];
1646
- }
1647
-
1648
- interface Preset {
1649
- [key: string]: unknown;
1650
- name: string;
1651
- middlewares: PresetMiddlewares;
1652
- }
1653
- ` : "";
1654
- return `/**
1655
- * Flexible Multi-Tenant Preset
1656
- *
1657
- * Smarter tenant filtering that works with public + authenticated routes.
1658
- *
1659
- * Philosophy:
1660
- * - No org header → No filtering (public data, all orgs)
1661
- * - Org header present → Require auth, filter by org
1662
- *
1663
- * This differs from Arc's strict multiTenant which always requires auth.
1664
- */
1665
- ${typeAnnotations}
1666
- /**
1667
- * Default organization ID extractor
1668
- * Tries multiple sources in order of priority
1669
- */
1670
- function defaultExtractOrganizationId(request${ts ? ": any" : ""})${ts ? ": string | null" : ""} {
1671
- // Priority 1: Explicit context (set by org-scope plugin)
1672
- if (request.context?.organizationId) {
1673
- return String(request.context.organizationId);
1674
- }
1675
-
1676
- // Priority 2: User's organizationId field
1677
- if (request.user?.organizationId) {
1678
- return String(request.user.organizationId);
1679
- }
1680
-
1681
- // Priority 3: User's organization object (nested)
1682
- if (request.user?.organization) {
1683
- const org = request.user.organization;
1684
- return String(org._id || org.id || org);
1685
- }
1686
-
1687
- return null;
1688
- }
1689
-
1690
- /**
1691
- * Create flexible tenant filter middleware
1692
- * Only filters when org context is present
1693
- */
1694
- function createFlexibleTenantFilter(
1695
- tenantField${ts ? ": string" : ""},
1696
- bypassRoles${ts ? ": string[]" : ""},
1697
- extractOrganizationId${ts ? ": (request: any) => string | null" : ""}
1698
- ) {
1699
- return async (request${ts ? ": any" : ""}, reply${ts ? ": any" : ""}) => {
1700
- const user = request.user;
1701
- const orgId = extractOrganizationId(request);
1702
-
1703
- // No org context - allow through (public data, no filtering)
1704
- if (!orgId) {
1705
- request.log?.debug?.({ msg: 'No org context - showing all data' });
1706
- return;
1707
- }
1708
-
1709
- // Org context present - auth should already be handled by org-scope plugin
1710
- // But double-check for safety
1711
- if (!user) {
1712
- request.log?.warn?.({ msg: 'Org context present but no user - should not happen' });
1713
- return reply.code(401).send({
1714
- success: false,
1715
- error: 'Unauthorized',
1716
- message: 'Authentication required for organization-scoped data',
1717
- });
1718
- }
1719
-
1720
- // Bypass roles skip filter (superadmin sees all)
1721
- const userRoles = Array.isArray(user.roles) ? user.roles : [];
1722
- if (bypassRoles.some((r${ts ? ": string" : ""}) => userRoles.includes(r))) {
1723
- request.log?.debug?.({ msg: 'Bypass role - no tenant filter' });
1724
- return;
1725
- }
1726
-
1727
- // Apply tenant filter to query
1728
- request.query = request.query ?? {};
1729
- request.query._policyFilters = {
1730
- ...(request.query._policyFilters ?? {}),
1731
- [tenantField]: orgId,
1732
- };
1733
-
1734
- request.log?.debug?.({ msg: 'Tenant filter applied', orgId, tenantField });
1735
- };
1736
- }
1737
-
1738
- /**
1739
- * Create tenant injection middleware
1740
- * Injects tenant ID into request body on create
1741
- */
1742
- function createTenantInjection(
1743
- tenantField${ts ? ": string" : ""},
1744
- extractOrganizationId${ts ? ": (request: any) => string | null" : ""}
1745
- ) {
1746
- return async (request${ts ? ": any" : ""}, reply${ts ? ": any" : ""}) => {
1747
- const orgId = extractOrganizationId(request);
1748
-
1749
- // Fail-closed: Require orgId for create operations
1750
- if (!orgId) {
1751
- return reply.code(403).send({
1752
- success: false,
1753
- error: 'Forbidden',
1754
- message: 'Organization context required to create resources',
1755
- });
1756
- }
1757
-
1758
- if (request.body) {
1759
- request.body[tenantField] = orgId;
1760
- }
1761
- };
1762
- }
1763
-
1764
- /**
1765
- * Flexible Multi-Tenant Preset
1766
- *
1767
- * @param options.tenantField - Field name in database (default: 'organizationId')
1768
- * @param options.bypassRoles - Roles that bypass tenant isolation (default: ['superadmin'])
1769
- * @param options.extractOrganizationId - Custom org ID extractor function
1770
- */
1771
- export function flexibleMultiTenantPreset(options${ts ? ": FlexibleMultiTenantOptions = {}" : " = {}"})${ts ? ": Preset" : ""} {
1772
- const {
1773
- tenantField = 'organizationId',
1774
- bypassRoles = ['superadmin'],
1775
- extractOrganizationId = defaultExtractOrganizationId,
1776
- } = options;
1777
-
1778
- const tenantFilter = createFlexibleTenantFilter(tenantField, bypassRoles, extractOrganizationId);
1779
- const tenantInjection = createTenantInjection(tenantField, extractOrganizationId);
1780
-
1781
- return {
1782
- name: 'flexibleMultiTenant',
1783
- middlewares: {
1784
- list: [tenantFilter],
1785
- get: [tenantFilter],
1786
- create: [tenantInjection],
1787
- update: [tenantFilter],
1788
- delete: [tenantFilter],
1789
- },
1790
- };
1791
- }
1792
-
1793
- export default flexibleMultiTenantPreset;
1794
- `;
1795
- }
1796
- function permissionsTemplate(config) {
1797
- const ts = config.typescript;
1798
- const typeImport = ts ? ",\n type PermissionCheck," : "";
1799
- const returnType = ts ? ": PermissionCheck" : "";
1800
- let content = `/**
1801
- * Permission Helpers
1802
- *
1803
- * Clean, type-safe permission definitions for resources.
1804
- */
1805
-
1806
- import {
1807
- requireAuth,
1808
- requireRoles,
1809
- requireOwnership,
1810
- allowPublic,
1811
- anyOf,
1812
- allOf,
1813
- denyAll,
1814
- when${typeImport}
1815
- } from '@classytic/arc/permissions';
1816
-
1817
- // Re-export core helpers
1818
- export {
1819
- allowPublic,
1820
- requireAuth,
1821
- requireRoles,
1822
- requireOwnership,
1823
- allOf,
1824
- anyOf,
1825
- denyAll,
1826
- when,
1827
- };
1828
-
1829
- // ============================================================================
1830
- // Permission Helpers
1831
- // ============================================================================
1832
-
1833
- /**
1834
- * Require any authenticated user
1835
- */
1836
- export const requireAuthenticated = ()${returnType} =>
1837
- requireRoles(['user', 'admin', 'superadmin']);
1838
-
1839
- /**
1840
- * Require admin or superadmin
1841
- */
1842
- export const requireAdmin = ()${returnType} =>
1843
- requireRoles(['admin', 'superadmin']);
1844
-
1845
- /**
1846
- * Require superadmin only
1847
- */
1848
- export const requireSuperadmin = ()${returnType} =>
1849
- requireRoles(['superadmin']);
1850
- `;
1851
- if (config.tenant === "multi") {
1852
- content += `
1853
- /**
1854
- * Require organization owner
1855
- */
1856
- export const requireOrgOwner = ()${returnType} =>
1857
- requireRoles(['owner'], { bypassRoles: ['admin', 'superadmin'] });
1858
-
1859
- /**
1860
- * Require organization manager or higher
1861
- */
1862
- export const requireOrgManager = ()${returnType} =>
1863
- requireRoles(['owner', 'manager'], { bypassRoles: ['admin', 'superadmin'] });
1864
-
1865
- /**
1866
- * Require organization staff (any org member)
1867
- */
1868
- export const requireOrgStaff = ()${returnType} =>
1869
- requireRoles(['owner', 'manager', 'staff'], { bypassRoles: ['admin', 'superadmin'] });
1870
- `;
1871
- }
1872
- content += `
1873
- // ============================================================================
1874
- // Standard Permission Sets
1875
- // ============================================================================
1876
-
1877
- /**
1878
- * Public read, authenticated write (default for most resources)
1879
- */
1880
- export const publicReadPermissions = {
1881
- list: allowPublic(),
1882
- get: allowPublic(),
1883
- create: requireAuthenticated(),
1884
- update: requireAuthenticated(),
1885
- delete: requireAuthenticated(),
1886
- };
1887
-
1888
- /**
1889
- * All operations require authentication
1890
- */
1891
- export const authenticatedPermissions = {
1892
- list: requireAuth(),
1893
- get: requireAuth(),
1894
- create: requireAuth(),
1895
- update: requireAuth(),
1896
- delete: requireAuth(),
1897
- };
1898
-
1899
- /**
1900
- * Admin only permissions
1901
- */
1902
- export const adminPermissions = {
1903
- list: requireAdmin(),
1904
- get: requireAdmin(),
1905
- create: requireSuperadmin(),
1906
- update: requireSuperadmin(),
1907
- delete: requireSuperadmin(),
1908
- };
1909
- `;
1910
- if (config.tenant === "multi") {
1911
- content += `
1912
- /**
1913
- * Organization staff permissions
1914
- */
1915
- export const orgStaffPermissions = {
1916
- list: requireOrgStaff(),
1917
- get: requireOrgStaff(),
1918
- create: requireOrgManager(),
1919
- update: requireOrgManager(),
1920
- delete: requireOrgOwner(),
1921
- };
1922
- `;
1923
- }
1924
- return content;
1925
- }
1926
- function configTemplate(config) {
1927
- const ts = config.typescript;
1928
- let typeDefinition = "";
1929
- if (ts) {
1930
- typeDefinition = `
1931
- export interface AppConfig {
1932
- env: string;
1933
- server: {
1934
- port: number;
1935
- host: string;
1936
- };
1937
- jwt: {
1938
- secret: string;
1939
- expiresIn: string;
1940
- };
1941
- cors: {
1942
- origins: string[] | boolean; // true = allow all ('*')
1943
- methods: string[];
1944
- allowedHeaders: string[];
1945
- credentials: boolean;
1946
- };${config.adapter === "mongokit" ? `
1947
- database: {
1948
- uri: string;
1949
- };` : ""}${config.tenant === "multi" ? `
1950
- org?: {
1951
- header: string;
1952
- };` : ""}
1953
- }
1954
- `;
1955
- }
1956
- return `/**
1957
- * Application Configuration
1958
- *
1959
- * All config is loaded from environment variables.
1960
- * ENV file is loaded by config/env.ts (imported first in entry points).
1961
- */
1962
- ${typeDefinition}
1963
- const config${ts ? ": AppConfig" : ""} = {
1964
- env: process.env.NODE_ENV || 'development',
1965
-
1966
- server: {
1967
- port: parseInt(process.env.PORT || '8040', 10),
1968
- host: process.env.HOST || '0.0.0.0',
1969
- },
1970
-
1971
- jwt: {
1972
- secret: process.env.JWT_SECRET || 'dev-secret-change-in-production-min-32',
1973
- expiresIn: process.env.JWT_EXPIRES_IN || '7d',
1974
- },
1975
-
1976
- cors: {
1977
- // '*' = allow all origins (true), otherwise comma-separated list
1978
- origins:
1979
- process.env.CORS_ORIGINS === '*'
1980
- ? true
1981
- : (process.env.CORS_ORIGINS || 'http://localhost:3000').split(','),
1982
- methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
1983
- allowedHeaders: ['Content-Type', 'Authorization', 'x-organization-id', 'x-request-id'],
1984
- credentials: true,
1985
- },
1986
- ${config.adapter === "mongokit" ? `
1987
- database: {
1988
- uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/${config.name}',
1989
- },
1990
- ` : ""}${config.tenant === "multi" ? `
1991
- org: {
1992
- header: process.env.ORG_HEADER || 'x-organization-id',
1993
- },
1994
- ` : ""}};
1995
-
1996
- export default config;
1997
- `;
1998
- }
1999
- function exampleModelTemplate(config) {
2000
- const ts = config.typescript;
2001
- const typeExport = ts ? `
2002
- export type ExampleDocument = mongoose.InferSchemaType<typeof exampleSchema>;
2003
- export type ExampleModel = mongoose.Model<ExampleDocument>;
2004
- ` : "";
2005
- return `/**
2006
- * Example Model
2007
- * Generated by Arc CLI
2008
- */
2009
-
2010
- import mongoose from 'mongoose';
2011
-
2012
- const exampleSchema = new mongoose.Schema(
2013
- {
2014
- name: { type: String, required: true, trim: true },
2015
- description: { type: String, trim: true },
2016
- isActive: { type: Boolean, default: true, index: true },
2017
- ${config.tenant === "multi" ? " organizationId: { type: mongoose.Schema.Types.ObjectId, ref: 'Organization', required: true, index: true },\n" : ""} createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User', index: true },
2018
- deletedAt: { type: Date, default: null, index: true },
2019
- },
2020
- {
2021
- timestamps: true,
2022
- toJSON: { virtuals: true },
2023
- toObject: { virtuals: true },
2024
- }
2025
- );
2026
-
2027
- // Indexes for common queries
2028
- exampleSchema.index({ name: 1 });
2029
- exampleSchema.index({ deletedAt: 1, isActive: 1 });
2030
- ${config.tenant === "multi" ? "exampleSchema.index({ organizationId: 1, deletedAt: 1 });\n" : ""}${typeExport}
2031
- const Example = mongoose.model${ts ? "<ExampleDocument>" : ""}('Example', exampleSchema);
2032
-
2033
- export default Example;
2034
- `;
2035
- }
2036
- function exampleRepositoryTemplate(config) {
2037
- const ts = config.typescript;
2038
- const typeImport = ts ? "import type { ExampleDocument } from './example.model.js';\n" : "";
2039
- const generic = ts ? "<ExampleDocument>" : "";
2040
- return `/**
2041
- * Example Repository
2042
- * Generated by Arc CLI
2043
- *
2044
- * MongoKit repository with plugins for:
2045
- * - Soft delete (deletedAt filtering)
2046
- * - Custom business logic methods
2047
- */
2048
-
2049
- import {
2050
- Repository,
2051
- softDeletePlugin,
2052
- methodRegistryPlugin,
2053
- } from '@classytic/mongokit';
2054
- ${typeImport}import Example from './example.model.js';
2055
-
2056
- class ExampleRepository extends Repository${generic} {
2057
- constructor() {
2058
- super(Example, [
2059
- methodRegistryPlugin(), // Required for plugin method registration
2060
- softDeletePlugin(), // Soft delete support
2061
- ]);
2062
- }
2063
-
2064
- /**
2065
- * Find all active (non-deleted) records
2066
- */
2067
- async findActive() {
2068
- return this.Model.find({ isActive: true, deletedAt: null }).lean();
2069
- }
2070
- ${config.tenant === "multi" ? `
2071
- /**
2072
- * Find active records for an organization
2073
- */
2074
- async findActiveByOrg(organizationId${ts ? ": string" : ""}) {
2075
- return this.Model.find({
2076
- organizationId,
2077
- isActive: true,
2078
- deletedAt: null,
2079
- }).lean();
2080
- }
2081
- ` : ""}
2082
- // Note: softDeletePlugin provides restore() and getDeleted() methods automatically
2083
- }
2084
-
2085
- const exampleRepository = new ExampleRepository();
2086
-
2087
- export default exampleRepository;
2088
- export { ExampleRepository };
2089
- `;
2090
- }
2091
- function exampleResourceTemplate(config) {
2092
- config.typescript;
2093
- config.tenant === "multi" ? "['softDelete', 'flexibleMultiTenant']" : "['softDelete']";
2094
- return `/**
2095
- * Example Resource
2096
- * Generated by Arc CLI
2097
- *
2098
- * A complete resource with:
2099
- * - Model (Mongoose schema)
2100
- * - Repository (MongoKit with plugins)
2101
- * - Permissions (role-based access)
2102
- * - Presets (soft delete${config.tenant === "multi" ? ", multi-tenant" : ""})
2103
- */
2104
-
2105
- import { defineResource } from '@classytic/arc';
2106
- import { createAdapter } from '#shared/adapter.js';
2107
- import { publicReadPermissions } from '#shared/permissions.js';
2108
- ${config.tenant === "multi" ? "import { flexibleMultiTenantPreset } from '#shared/presets/flexible-multi-tenant.js';\n" : ""}import Example from './example.model.js';
2109
- import exampleRepository from './example.repository.js';
2110
- import exampleController from './example.controller.js';
2111
-
2112
- const exampleResource = defineResource({
2113
- name: 'example',
2114
- displayName: 'Examples',
2115
- prefix: '/examples',
2116
-
2117
- adapter: createAdapter(Example, exampleRepository),
2118
- controller: exampleController,
2119
-
2120
- presets: [
2121
- 'softDelete',${config.tenant === "multi" ? `
2122
- flexibleMultiTenantPreset({ tenantField: 'organizationId' }),` : ""}
2123
- ],
2124
-
2125
- permissions: publicReadPermissions,
2126
-
2127
- // Add custom routes here:
2128
- // additionalRoutes: [
2129
- // {
2130
- // method: 'GET',
2131
- // path: '/custom',
2132
- // summary: 'Custom endpoint',
2133
- // handler: async (request, reply) => { ... },
2134
- // },
2135
- // ],
2136
- });
2137
-
2138
- export default exampleResource;
2139
- `;
2140
- }
2141
- function exampleControllerTemplate(config) {
2142
- const ts = config.typescript;
2143
- return `/**
2144
- * Example Controller
2145
- * Generated by Arc CLI
2146
- *
2147
- * BaseController provides CRUD operations with:
2148
- * - Automatic pagination
2149
- * - Query parsing
2150
- * - Validation
2151
- */
2152
-
2153
- import { BaseController } from '@classytic/arc';
2154
- import exampleRepository from './example.repository.js';
2155
- import { exampleSchemaOptions } from './example.schemas.js';
2156
-
2157
- class ExampleController extends BaseController {
2158
- constructor() {
2159
- super(exampleRepository${ts ? " as any" : ""}, { schemaOptions: exampleSchemaOptions });
2160
- }
2161
-
2162
- // Add custom controller methods here:
2163
- // async customAction(request, reply) {
2164
- // // Custom logic
2165
- // }
2166
- }
2167
-
2168
- const exampleController = new ExampleController();
2169
- export default exampleController;
2170
- `;
2171
- }
2172
- function exampleSchemasTemplate(config) {
2173
- const ts = config.typescript;
2174
- const multiTenantFields = config.tenant === "multi";
2175
- return `/**
2176
- * Example Schemas
2177
- * Generated by Arc CLI
2178
- *
2179
- * Schema options for controller validation and query parsing
2180
- */
2181
-
2182
- import Example from './example.model.js';
2183
- import { buildCrudSchemasFromModel } from '@classytic/mongokit/utils';
2184
-
2185
- /**
2186
- * CRUD Schemas with Field Rules
2187
- * Auto-generated from Mongoose model
2188
- */
2189
- const crudSchemas = buildCrudSchemasFromModel(Example, {
2190
- strictAdditionalProperties: true,
2191
- fieldRules: {
2192
- // Mark fields as system-managed (excluded from create/update)
2193
- // deletedAt: { systemManaged: true },
2194
- },
2195
- query: {
2196
- filterableFields: {
2197
- isActive: 'boolean',${multiTenantFields ? `
2198
- organizationId: 'ObjectId',` : ""}
2199
- createdAt: 'date',
2200
- },
2201
- },
2202
- });
2203
-
2204
- // Schema options for controller
2205
- export const exampleSchemaOptions${ts ? ": any" : ""} = {
2206
- query: {${multiTenantFields ? `
2207
- allowedPopulate: ['organizationId'],` : ""}
2208
- filterableFields: {
2209
- isActive: 'boolean',${multiTenantFields ? `
2210
- organizationId: 'ObjectId',` : ""}
2211
- createdAt: 'date',
2212
- },
2213
- },
2214
- };
2215
-
2216
- export default crudSchemas;
2217
- `;
2218
- }
2219
- function exampleTestTemplate(config) {
2220
- const ts = config.typescript;
2221
- return `/**
2222
- * Example Resource Tests
2223
- * Generated by Arc CLI
2224
- *
2225
- * Run tests: npm test
2226
- * Watch mode: npm run test:watch
2227
- */
2228
-
2229
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2230
- ${config.adapter === "mongokit" ? "import mongoose from 'mongoose';\n" : ""}import { createAppInstance } from '../src/app.js';
2231
- ${ts ? "import type { FastifyInstance } from 'fastify';\n" : ""}
2232
- describe('Example Resource', () => {
2233
- let app${ts ? ": FastifyInstance" : ""};
2234
-
2235
- beforeAll(async () => {
2236
- ${config.adapter === "mongokit" ? ` // Connect to test database
2237
- const testDbUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/${config.name}-test';
2238
- await mongoose.connect(testDbUri);
2239
- ` : ""}
2240
- // Create app instance
2241
- app = await createAppInstance();
2242
- await app.ready();
2243
- });
2244
-
2245
- afterAll(async () => {
2246
- await app.close();
2247
- ${config.adapter === "mongokit" ? " await mongoose.connection.close();" : ""}
2248
- });
2249
-
2250
- describe('GET /examples', () => {
2251
- it('should return a list of examples', async () => {
2252
- const response = await app.inject({
2253
- method: 'GET',
2254
- url: '/examples',
2255
- });
2256
-
2257
- expect(response.statusCode).toBe(200);
2258
- const body = JSON.parse(response.body);
2259
- expect(body).toHaveProperty('docs');
2260
- expect(Array.isArray(body.docs)).toBe(true);
2261
- });
2262
- });
2263
-
2264
- describe('POST /examples', () => {
2265
- it('should require authentication', async () => {
2266
- const response = await app.inject({
2267
- method: 'POST',
2268
- url: '/examples',
2269
- payload: { name: 'Test Example' },
2270
- });
2271
-
2272
- // Should fail without auth token
2273
- expect(response.statusCode).toBe(401);
2274
- });
2275
- });
2276
-
2277
- // Add more tests as needed:
2278
- // - GET /examples/:id
2279
- // - PATCH /examples/:id
2280
- // - DELETE /examples/:id
2281
- // - Custom endpoints
2282
- });
2283
- `;
2284
- }
2285
- function userModelTemplate(config) {
2286
- const ts = config.typescript;
2287
- const orgRoles = config.tenant === "multi" ? `
2288
- // Organization roles (for multi-tenant)
2289
- const ORG_ROLES = ['owner', 'manager', 'hr', 'staff', 'contractor'] as const;
2290
- type OrgRole = typeof ORG_ROLES[number];
2291
- ` : "";
2292
- const orgInterface = config.tenant === "multi" ? `
2293
- type UserOrganization = {
2294
- organizationId: Types.ObjectId;
2295
- organizationName: string;
2296
- roles: OrgRole[];
2297
- joinedAt: Date;
2298
- };
2299
- ` : "";
2300
- const orgSchema = config.tenant === "multi" ? `
2301
- // Multi-org support
2302
- organizations: [{
2303
- organizationId: { type: Schema.Types.ObjectId, ref: 'Organization', required: true },
2304
- organizationName: { type: String, required: true },
2305
- roles: { type: [String], enum: ORG_ROLES, default: [] },
2306
- joinedAt: { type: Date, default: () => new Date() },
2307
- }],
2308
- ` : "";
2309
- const orgMethods = config.tenant === "multi" ? `
2310
- // Organization methods
2311
- userSchema.methods.getOrgRoles = function(orgId${ts ? ": Types.ObjectId | string" : ""}) {
2312
- const org = this.organizations.find(o => o.organizationId.toString() === orgId.toString());
2313
- return org?.roles || [];
2314
- };
2315
-
2316
- userSchema.methods.hasOrgAccess = function(orgId${ts ? ": Types.ObjectId | string" : ""}) {
2317
- return this.organizations.some(o => o.organizationId.toString() === orgId.toString());
2318
- };
2319
-
2320
- userSchema.methods.addOrganization = function(
2321
- organizationId${ts ? ": Types.ObjectId" : ""},
2322
- organizationName${ts ? ": string" : ""},
2323
- roles${ts ? ": OrgRole[]" : ""} = []
2324
- ) {
2325
- const existing = this.organizations.find(o => o.organizationId.toString() === organizationId.toString());
2326
- if (existing) {
2327
- existing.organizationName = organizationName;
2328
- existing.roles = [...new Set([...existing.roles, ...roles])];
2329
- } else {
2330
- this.organizations.push({ organizationId, organizationName, roles, joinedAt: new Date() });
2331
- }
2332
- return this;
2333
- };
2334
-
2335
- userSchema.methods.removeOrganization = function(organizationId${ts ? ": Types.ObjectId" : ""}) {
2336
- this.organizations = this.organizations.filter(o => o.organizationId.toString() !== organizationId.toString());
2337
- return this;
2338
- };
2339
-
2340
- // Index for org queries
2341
- userSchema.index({ 'organizations.organizationId': 1 });
2342
- ` : "";
2343
- const userType = ts ? `
2344
- type PlatformRole = 'user' | 'admin' | 'superadmin';
2345
-
2346
- type User = {
2347
- name: string;
2348
- email: string;
2349
- password: string;
2350
- roles: PlatformRole[];${config.tenant === "multi" ? `
2351
- organizations: UserOrganization[];` : ""}
2352
- resetPasswordToken?: string;
2353
- resetPasswordExpires?: Date;
2354
- };
2355
-
2356
- type UserMethods = {
2357
- matchPassword: (enteredPassword: string) => Promise<boolean>;${config.tenant === "multi" ? `
2358
- getOrgRoles: (orgId: Types.ObjectId | string) => OrgRole[];
2359
- hasOrgAccess: (orgId: Types.ObjectId | string) => boolean;
2360
- addOrganization: (orgId: Types.ObjectId, name: string, roles?: OrgRole[]) => UserDocument;
2361
- removeOrganization: (orgId: Types.ObjectId) => UserDocument;` : ""}
2362
- };
2363
-
2364
- export type UserDocument = HydratedDocument<User, UserMethods>;
2365
- export type UserModel = Model<User, {}, UserMethods>;
2366
- ` : "";
2367
- return `/**
2368
- * User Model
2369
- * Generated by Arc CLI
2370
- */
2371
-
2372
- import bcrypt from 'bcryptjs';
2373
- import mongoose${ts ? ", { type HydratedDocument, type Model, type Types }" : ""} from 'mongoose';
2374
- ${orgRoles}
2375
- const { Schema } = mongoose;
2376
- ${orgInterface}${userType}
2377
- const userSchema = new Schema${ts ? "<User, UserModel, UserMethods>" : ""}(
2378
- {
2379
- name: { type: String, required: true, trim: true },
2380
- email: {
2381
- type: String,
2382
- required: true,
2383
- unique: true,
2384
- lowercase: true,
2385
- trim: true,
2386
- },
2387
- password: { type: String, required: true },
2388
-
2389
- // Platform roles
2390
- roles: {
2391
- type: [String],
2392
- enum: ['user', 'admin', 'superadmin'],
2393
- default: ['user'],
2394
- },
2395
- ${orgSchema}
2396
- // Password reset
2397
- resetPasswordToken: String,
2398
- resetPasswordExpires: Date,
2399
- },
2400
- { timestamps: true }
2401
- );
2402
-
2403
- // Password hashing
2404
- userSchema.pre('save', async function() {
2405
- if (!this.isModified('password')) return;
2406
- const salt = await bcrypt.genSalt(10);
2407
- this.password = await bcrypt.hash(this.password, salt);
2408
- });
2409
-
2410
- // Password comparison
2411
- userSchema.methods.matchPassword = async function(enteredPassword${ts ? ": string" : ""}) {
2412
- return bcrypt.compare(enteredPassword, this.password);
2413
- };
2414
- ${orgMethods}
2415
- // Exclude password in JSON
2416
- userSchema.set('toJSON', {
2417
- transform: (_doc, ret${ts ? ": any" : ""}) => {
2418
- delete ret.password;
2419
- delete ret.resetPasswordToken;
2420
- delete ret.resetPasswordExpires;
2421
- return ret;
2422
- },
2423
- });
2424
-
2425
- const User = mongoose.models.User${ts ? " as UserModel" : ""} || mongoose.model${ts ? "<User, UserModel>" : ""}('User', userSchema);
2426
- export default User;
2427
- `;
2428
- }
2429
- function userRepositoryTemplate(config) {
2430
- const ts = config.typescript;
2431
- const typeImport = ts ? "import type { UserDocument } from './user.model.js';\nimport type { ClientSession, Types } from 'mongoose';\n" : "";
2432
- return `/**
2433
- * User Repository
2434
- * Generated by Arc CLI
2435
- *
2436
- * MongoKit repository with plugins for common operations
2437
- */
2438
-
2439
- import {
2440
- Repository,
2441
- methodRegistryPlugin,
2442
- mongoOperationsPlugin,
2443
- } from '@classytic/mongokit';
2444
- ${typeImport}import User from './user.model.js';
2445
-
2446
- ${ts ? "type ID = string | Types.ObjectId;\n" : ""}
2447
- class UserRepository extends Repository${ts ? "<UserDocument>" : ""} {
2448
- constructor() {
2449
- super(User${ts ? " as any" : ""}, [
2450
- methodRegistryPlugin(),
2451
- mongoOperationsPlugin(),
2452
- ]);
2453
- }
2454
-
2455
- /**
2456
- * Find user by email
2457
- */
2458
- async findByEmail(email${ts ? ": string" : ""}) {
2459
- return this.Model.findOne({ email: email.toLowerCase().trim() });
2460
- }
2461
-
2462
- /**
2463
- * Find user by reset token
2464
- */
2465
- async findByResetToken(token${ts ? ": string" : ""}) {
2466
- return this.Model.findOne({
2467
- resetPasswordToken: token,
2468
- resetPasswordExpires: { $gt: Date.now() },
2469
- });
2470
- }
2471
-
2472
- /**
2473
- * Check if email exists
2474
- */
2475
- async emailExists(email${ts ? ": string" : ""})${ts ? ": Promise<boolean>" : ""} {
2476
- const result = await this.Model.exists({ email: email.toLowerCase().trim() });
2477
- return !!result;
2478
- }
2479
-
2480
- /**
2481
- * Update user password (triggers hash middleware)
2482
- */
2483
- async updatePassword(userId${ts ? ": ID" : ""}, newPassword${ts ? ": string" : ""}, options${ts ? ": { session?: ClientSession }" : ""} = {}) {
2484
- const user = await this.Model.findById(userId).session(options.session ?? null);
2485
- if (!user) throw new Error('User not found');
2486
-
2487
- user.password = newPassword;
2488
- user.resetPasswordToken = undefined;
2489
- user.resetPasswordExpires = undefined;
2490
- await user.save({ session: options.session ?? undefined });
2491
- return user;
2492
- }
2493
-
2494
- /**
2495
- * Set reset token
2496
- */
2497
- async setResetToken(userId${ts ? ": ID" : ""}, token${ts ? ": string" : ""}, expiresAt${ts ? ": Date" : ""}) {
2498
- return this.Model.findByIdAndUpdate(
2499
- userId,
2500
- { resetPasswordToken: token, resetPasswordExpires: expiresAt },
2501
- { new: true }
2502
- );
2503
- }
2504
- ${config.tenant === "multi" ? `
2505
- /**
2506
- * Find users by organization
2507
- */
2508
- async findByOrganization(organizationId${ts ? ": ID" : ""}) {
2509
- return this.Model.find({ 'organizations.organizationId': organizationId })
2510
- .select('-password -resetPasswordToken -resetPasswordExpires')
2511
- .lean();
2512
- }
2513
- ` : ""}
2514
- }
2515
-
2516
- const userRepository = new UserRepository();
2517
- export default userRepository;
2518
- export { UserRepository };
2519
- `;
2520
- }
2521
- function userControllerTemplate(config) {
2522
- const ts = config.typescript;
2523
- return `/**
2524
- * User Controller
2525
- * Generated by Arc CLI
2526
- *
2527
- * BaseController for user management operations.
2528
- * Used by auth resource for /users/me endpoints.
2529
- */
2530
-
2531
- import { BaseController } from '@classytic/arc';
2532
- import userRepository from './user.repository.js';
2533
-
2534
- class UserController extends BaseController {
2535
- constructor() {
2536
- super(userRepository${ts ? " as any" : ""});
2537
- }
2538
-
2539
- // Custom user operations can be added here
2540
- }
2541
-
2542
- const userController = new UserController();
2543
- export default userController;
2544
- `;
2545
- }
2546
- function authResourceTemplate(config) {
2547
- const ts = config.typescript;
2548
- return `/**
2549
- * Auth Resource
2550
- * Generated by Arc CLI
2551
- *
2552
- * Combined auth + user profile endpoints:
2553
- * - POST /auth/register
2554
- * - POST /auth/login
2555
- * - POST /auth/refresh
2556
- * - POST /auth/forgot-password
2557
- * - POST /auth/reset-password
2558
- * - GET /users/me
2559
- * - PATCH /users/me
2560
- */
2561
-
2562
- import { defineResource } from '@classytic/arc';
2563
- import { allowPublic, requireAuth } from '@classytic/arc/permissions';
2564
- import { createAdapter } from '#shared/adapter.js';
2565
- import User from '../user/user.model.js';
2566
- import userRepository from '../user/user.repository.js';
2567
- import * as handlers from './auth.handlers.js';
2568
- import * as schemas from './auth.schemas.js';
2569
-
2570
- /**
2571
- * Auth Resource - handles authentication
2572
- */
2573
- export const authResource = defineResource({
2574
- name: 'auth',
2575
- displayName: 'Authentication',
2576
- tag: 'Authentication',
2577
- prefix: '/auth',
2578
-
2579
- adapter: createAdapter(User${ts ? " as any" : ""}, userRepository${ts ? " as any" : ""}),
2580
- disableDefaultRoutes: true,
2581
-
2582
- additionalRoutes: [
2583
- {
2584
- method: 'POST',
2585
- path: '/register',
2586
- summary: 'Register new user',
2587
- permissions: allowPublic(),
2588
- handler: handlers.register,
2589
- wrapHandler: false,
2590
- schema: { body: schemas.registerBody },
2591
- },
2592
- {
2593
- method: 'POST',
2594
- path: '/login',
2595
- summary: 'User login',
2596
- permissions: allowPublic(),
2597
- handler: handlers.login,
2598
- wrapHandler: false,
2599
- schema: { body: schemas.loginBody },
2600
- },
2601
- {
2602
- method: 'POST',
2603
- path: '/refresh',
2604
- summary: 'Refresh access token',
2605
- permissions: allowPublic(),
2606
- handler: handlers.refreshToken,
2607
- wrapHandler: false,
2608
- schema: { body: schemas.refreshBody },
2609
- },
2610
- {
2611
- method: 'POST',
2612
- path: '/forgot-password',
2613
- summary: 'Request password reset',
2614
- permissions: allowPublic(),
2615
- handler: handlers.forgotPassword,
2616
- wrapHandler: false,
2617
- schema: { body: schemas.forgotBody },
2618
- },
2619
- {
2620
- method: 'POST',
2621
- path: '/reset-password',
2622
- summary: 'Reset password with token',
2623
- permissions: allowPublic(),
2624
- handler: handlers.resetPassword,
2625
- wrapHandler: false,
2626
- schema: { body: schemas.resetBody },
2627
- },
2628
- ],
2629
- });
2630
-
2631
- /**
2632
- * User Profile Resource - handles /users/me
2633
- */
2634
- export const userProfileResource = defineResource({
2635
- name: 'user-profile',
2636
- displayName: 'User Profile',
2637
- tag: 'User Profile',
2638
- prefix: '/users',
2639
-
2640
- adapter: createAdapter(User${ts ? " as any" : ""}, userRepository${ts ? " as any" : ""}),
2641
- disableDefaultRoutes: true,
2642
-
2643
- additionalRoutes: [
2644
- {
2645
- method: 'GET',
2646
- path: '/me',
2647
- summary: 'Get current user profile',
2648
- permissions: requireAuth(),
2649
- handler: handlers.getUserProfile,
2650
- wrapHandler: false,
2651
- },
2652
- {
2653
- method: 'PATCH',
2654
- path: '/me',
2655
- summary: 'Update current user profile',
2656
- permissions: requireAuth(),
2657
- handler: handlers.updateUserProfile,
2658
- wrapHandler: false,
2659
- schema: { body: schemas.updateUserBody },
2660
- },
2661
- ],
2662
- });
2663
-
2664
- export default authResource;
2665
- `;
2666
- }
2667
- function authHandlersTemplate(config) {
2668
- const ts = config.typescript;
2669
- const typeAnnotations = ts ? `
2670
- import type { FastifyRequest, FastifyReply } from 'fastify';
2671
- ` : "";
2672
- return `/**
2673
- * Auth Handlers
2674
- * Generated by Arc CLI
2675
- */
2676
-
2677
- import jwt from 'jsonwebtoken';
2678
- import config from '#config/index.js';
2679
- import userRepository from '../user/user.repository.js';
2680
- ${typeAnnotations}
2681
- // Token helpers
2682
- function generateTokens(userId${ts ? ": string" : ""}) {
2683
- const accessToken = jwt.sign({ id: userId }, config.jwt.secret, { expiresIn: '15m' });
2684
- const refreshToken = jwt.sign({ id: userId }, config.jwt.secret, { expiresIn: '7d' });
2685
- return { accessToken, refreshToken };
2686
- }
2687
-
2688
- /**
2689
- * Register new user
2690
- */
2691
- export async function register(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2692
- try {
2693
- const { name, email, password } = request.body${ts ? " as any" : ""};
2694
-
2695
- // Check if email exists
2696
- if (await userRepository.emailExists(email)) {
2697
- return reply.code(400).send({ success: false, message: 'Email already registered' });
2698
- }
2699
-
2700
- // Create user
2701
- await userRepository.create({ name, email, password, roles: ['user'] });
2702
-
2703
- return reply.code(201).send({ success: true, message: 'User registered successfully' });
2704
- } catch (error) {
2705
- request.log.error({ err: error }, 'Register error');
2706
- return reply.code(500).send({ success: false, message: 'Registration failed' });
2707
- }
2708
- }
2709
-
2710
- /**
2711
- * Login user
2712
- */
2713
- export async function login(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2714
- try {
2715
- const { email, password } = request.body${ts ? " as any" : ""};
2716
-
2717
- const user = await userRepository.findByEmail(email);
2718
- if (!user || !(await user.matchPassword(password))) {
2719
- return reply.code(401).send({ success: false, message: 'Invalid credentials' });
2720
- }
2721
-
2722
- const tokens = generateTokens(user._id.toString());
2723
-
2724
- return reply.send({
2725
- success: true,
2726
- user: { id: user._id, name: user.name, email: user.email, roles: user.roles },
2727
- ...tokens,
2728
- });
2729
- } catch (error) {
2730
- request.log.error({ err: error }, 'Login error');
2731
- return reply.code(500).send({ success: false, message: 'Login failed' });
2732
- }
2733
- }
2734
-
2735
- /**
2736
- * Refresh access token
2737
- */
2738
- export async function refreshToken(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2739
- try {
2740
- const { token } = request.body${ts ? " as any" : ""};
2741
- if (!token) {
2742
- return reply.code(401).send({ success: false, message: 'Refresh token required' });
2743
- }
2744
-
2745
- const decoded = jwt.verify(token, config.jwt.secret)${ts ? " as { id: string }" : ""};
2746
- const tokens = generateTokens(decoded.id);
2747
-
2748
- return reply.send({ success: true, ...tokens });
2749
- } catch {
2750
- return reply.code(401).send({ success: false, message: 'Invalid refresh token' });
2751
- }
2752
- }
2753
-
2754
- /**
2755
- * Forgot password
2756
- */
2757
- export async function forgotPassword(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2758
- try {
2759
- const { email } = request.body${ts ? " as any" : ""};
2760
- const user = await userRepository.findByEmail(email);
2761
-
2762
- if (user) {
2763
- const token = Math.random().toString(36).slice(2) + Date.now().toString(36);
2764
- const expires = new Date(Date.now() + 3600000); // 1 hour
2765
- await userRepository.setResetToken(user._id, token, expires);
2766
- // TODO: Send email with reset link
2767
- request.log.info(\`Password reset token for \${email}: \${token}\`);
2768
- }
2769
-
2770
- // Always return success to prevent email enumeration
2771
- return reply.send({ success: true, message: 'If email exists, reset link sent' });
2772
- } catch (error) {
2773
- request.log.error({ err: error }, 'Forgot password error');
2774
- return reply.code(500).send({ success: false, message: 'Failed to process request' });
2775
- }
2776
- }
2777
-
2778
- /**
2779
- * Reset password
2780
- */
2781
- export async function resetPassword(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2782
- try {
2783
- const { token, newPassword } = request.body${ts ? " as any" : ""};
2784
- const user = await userRepository.findByResetToken(token);
2785
-
2786
- if (!user) {
2787
- return reply.code(400).send({ success: false, message: 'Invalid or expired token' });
2788
- }
2789
-
2790
- await userRepository.updatePassword(user._id, newPassword);
2791
- return reply.send({ success: true, message: 'Password has been reset' });
2792
- } catch (error) {
2793
- request.log.error({ err: error }, 'Reset password error');
2794
- return reply.code(500).send({ success: false, message: 'Failed to reset password' });
2795
- }
2796
- }
2797
-
2798
- /**
2799
- * Get current user profile
2800
- */
2801
- export async function getUserProfile(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2802
- try {
2803
- const userId = (request${ts ? " as any" : ""}).user?._id || (request${ts ? " as any" : ""}).user?.id;
2804
- const user = await userRepository.getById(userId);
2805
-
2806
- if (!user) {
2807
- return reply.code(404).send({ success: false, message: 'User not found' });
2808
- }
2809
-
2810
- return reply.send({ success: true, data: user });
2811
- } catch (error) {
2812
- request.log.error({ err: error }, 'Get profile error');
2813
- return reply.code(500).send({ success: false, message: 'Failed to get profile' });
2814
- }
2815
- }
2816
-
2817
- /**
2818
- * Update current user profile
2819
- */
2820
- export async function updateUserProfile(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2821
- try {
2822
- const userId = (request${ts ? " as any" : ""}).user?._id || (request${ts ? " as any" : ""}).user?.id;
2823
- const updates = { ...request.body${ts ? " as any" : ""} };
2824
-
2825
- // Prevent updating protected fields
2826
- if ('password' in updates) delete updates.password;
2827
- if ('roles' in updates) delete updates.roles;
2828
- if ('organizations' in updates) delete updates.organizations;
2829
-
2830
- const user = await userRepository.Model.findByIdAndUpdate(userId, updates, { new: true });
2831
- return reply.send({ success: true, data: user });
2832
- } catch (error) {
2833
- request.log.error({ err: error }, 'Update profile error');
2834
- return reply.code(500).send({ success: false, message: 'Failed to update profile' });
2835
- }
2836
- }
2837
- `;
2838
- }
2839
- function authSchemasTemplate(config) {
2840
- return `/**
2841
- * Auth Schemas
2842
- * Generated by Arc CLI
2843
- */
2844
-
2845
- export const registerBody = {
2846
- type: 'object',
2847
- required: ['name', 'email', 'password'],
2848
- properties: {
2849
- name: { type: 'string', minLength: 2 },
2850
- email: { type: 'string', format: 'email' },
2851
- password: { type: 'string', minLength: 6 },
2852
- },
2853
- };
2854
-
2855
- export const loginBody = {
2856
- type: 'object',
2857
- required: ['email', 'password'],
2858
- properties: {
2859
- email: { type: 'string', format: 'email' },
2860
- password: { type: 'string' },
2861
- },
2862
- };
2863
-
2864
- export const refreshBody = {
2865
- type: 'object',
2866
- required: ['token'],
2867
- properties: {
2868
- token: { type: 'string' },
2869
- },
2870
- };
2871
-
2872
- export const forgotBody = {
2873
- type: 'object',
2874
- required: ['email'],
2875
- properties: {
2876
- email: { type: 'string', format: 'email' },
2877
- },
2878
- };
2879
-
2880
- export const resetBody = {
2881
- type: 'object',
2882
- required: ['token', 'newPassword'],
2883
- properties: {
2884
- token: { type: 'string' },
2885
- newPassword: { type: 'string', minLength: 6 },
2886
- },
2887
- };
2888
-
2889
- export const updateUserBody = {
2890
- type: 'object',
2891
- properties: {
2892
- name: { type: 'string', minLength: 2 },
2893
- email: { type: 'string', format: 'email' },
2894
- },
2895
- };
2896
- `;
2897
- }
2898
- function authTestTemplate(config) {
2899
- const ts = config.typescript;
2900
- return `/**
2901
- * Auth Tests
2902
- * Generated by Arc CLI
2903
- */
2904
-
2905
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2906
- ${config.adapter === "mongokit" ? "import mongoose from 'mongoose';\n" : ""}import { createAppInstance } from '../src/app.js';
2907
- ${ts ? "import type { FastifyInstance } from 'fastify';\n" : ""}
2908
- describe('Auth', () => {
2909
- let app${ts ? ": FastifyInstance" : ""};
2910
- const testUser = {
2911
- name: 'Test User',
2912
- email: 'test@example.com',
2913
- password: 'password123',
2914
- };
2915
-
2916
- beforeAll(async () => {
2917
- ${config.adapter === "mongokit" ? ` const testDbUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/${config.name}-test';
2918
- await mongoose.connect(testDbUri);
2919
- // Clean up test data
2920
- await mongoose.connection.collection('users').deleteMany({ email: testUser.email });
2921
- ` : ""}
2922
- app = await createAppInstance();
2923
- await app.ready();
2924
- });
2925
-
2926
- afterAll(async () => {
2927
- ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('users').deleteMany({ email: testUser.email });
2928
- await mongoose.connection.close();
2929
- ` : ""} await app.close();
2930
- });
2931
-
2932
- describe('POST /auth/register', () => {
2933
- it('should register a new user', async () => {
2934
- const response = await app.inject({
2935
- method: 'POST',
2936
- url: '/auth/register',
2937
- payload: testUser,
2938
- });
2939
-
2940
- expect(response.statusCode).toBe(201);
2941
- const body = JSON.parse(response.body);
2942
- expect(body.success).toBe(true);
2943
- });
2944
-
2945
- it('should reject duplicate email', async () => {
2946
- const response = await app.inject({
2947
- method: 'POST',
2948
- url: '/auth/register',
2949
- payload: testUser,
2950
- });
2951
-
2952
- expect(response.statusCode).toBe(400);
2953
- });
2954
- });
2955
-
2956
- describe('POST /auth/login', () => {
2957
- it('should login with valid credentials', async () => {
2958
- const response = await app.inject({
2959
- method: 'POST',
2960
- url: '/auth/login',
2961
- payload: { email: testUser.email, password: testUser.password },
2962
- });
2963
-
2964
- expect(response.statusCode).toBe(200);
2965
- const body = JSON.parse(response.body);
2966
- expect(body.success).toBe(true);
2967
- expect(body.accessToken).toBeDefined();
2968
- expect(body.refreshToken).toBeDefined();
2969
- });
2970
-
2971
- it('should reject invalid credentials', async () => {
2972
- const response = await app.inject({
2973
- method: 'POST',
2974
- url: '/auth/login',
2975
- payload: { email: testUser.email, password: 'wrongpassword' },
2976
- });
2977
-
2978
- expect(response.statusCode).toBe(401);
2979
- });
2980
- });
2981
-
2982
- describe('GET /users/me', () => {
2983
- it('should require authentication', async () => {
2984
- const response = await app.inject({
2985
- method: 'GET',
2986
- url: '/users/me',
2987
- });
2988
-
2989
- expect(response.statusCode).toBe(401);
2990
- });
2991
- });
2992
- });
2993
- `;
2994
- }
2995
- function printSuccessMessage(config, skipInstall) {
2996
- const installStep = skipInstall ? ` npm install
2997
- ` : "";
2998
- console.log(`
2999
- ╔═══════════════════════════════════════════════════════════════╗
3000
- ║ ✅ Project Created! ║
3001
- ╚═══════════════════════════════════════════════════════════════╝
3002
-
3003
- Next steps:
3004
-
3005
- cd ${config.name}
3006
- ${installStep} npm run dev # Uses .env.dev automatically
3007
-
3008
- API Documentation:
3009
-
3010
- http://localhost:8040/docs # Scalar UI
3011
- http://localhost:8040/_docs/openapi.json # OpenAPI spec
3012
-
3013
- Run tests:
3014
-
3015
- npm test # Run once
3016
- npm run test:watch # Watch mode
3017
-
3018
- Add resources:
3019
-
3020
- 1. Create folder: src/resources/product/
3021
- 2. Add: index.${config.typescript ? "ts" : "js"}, model.${config.typescript ? "ts" : "js"}, repository.${config.typescript ? "ts" : "js"}
3022
- 3. Register in src/resources/index.${config.typescript ? "ts" : "js"}
3023
-
3024
- Project structure:
3025
-
3026
- src/
3027
- ├── app.${config.typescript ? "ts" : "js"} # App factory (for workers/tests)
3028
- ├── index.${config.typescript ? "ts" : "js"} # Server entry
3029
- ├── config/ # Configuration
3030
- ├── shared/ # Adapters, presets, permissions
3031
- ├── plugins/ # App plugins (DI pattern)
3032
- └── resources/ # API resources
3033
-
3034
- Documentation:
3035
- https://github.com/classytic/arc
3036
- `);
3037
- }
3038
-
3039
- // src/cli/commands/introspect.ts
3040
- async function introspect(args) {
3041
- console.log("Introspecting Arc resources...\n");
3042
- try {
3043
- const { resourceRegistry: resourceRegistry2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
3044
- const resources = resourceRegistry2.getAll();
3045
- if (resources.length === 0) {
3046
- console.log("⚠️ No resources registered.");
3047
- console.log("\nTo introspect resources, you need to load them first:");
3048
- console.log(" arc introspect --entry ./index.js");
3049
- console.log("\nWhere index.js imports all your resource definitions.");
3050
- return;
3051
- }
3052
- console.log(`Found ${resources.length} resource(s):
3053
- `);
3054
- resources.forEach((resource, index) => {
3055
- console.log(`${index + 1}. ${resource.name}`);
3056
- console.log(` Display Name: ${resource.displayName}`);
3057
- console.log(` Prefix: ${resource.prefix}`);
3058
- console.log(` Module: ${resource.module || "none"}`);
3059
- if (resource.permissions) {
3060
- console.log(` Permissions:`);
3061
- Object.entries(resource.permissions).forEach(([op, roles]) => {
3062
- console.log(` ${op}: [${roles.join(", ")}]`);
3063
- });
3064
- }
3065
- if (resource.presets && resource.presets.length > 0) {
3066
- console.log(` Presets: ${resource.presets.join(", ")}`);
3067
- }
3068
- if (resource.additionalRoutes && resource.additionalRoutes.length > 0) {
3069
- console.log(` Additional Routes: ${resource.additionalRoutes.length}`);
3070
- }
3071
- console.log("");
3072
- });
3073
- const stats = resourceRegistry2.getStats();
3074
- console.log("Summary:");
3075
- console.log(` Total Resources: ${stats.totalResources}`);
3076
- console.log(` With Presets: ${resources.filter((r) => r.presets?.length > 0).length}`);
3077
- console.log(
3078
- ` With Custom Routes: ${resources.filter((r) => r.additionalRoutes && r.additionalRoutes.length > 0).length}`
3079
- );
3080
- } catch (error) {
3081
- console.error("Error:", error.message);
3082
- console.log("\nTip: Run this command after starting your application.");
3083
- process.exit(1);
3084
- }
3085
- }
3086
- async function exportDocs(args) {
3087
- const [outputPath = "./openapi.json"] = args;
3088
- console.log("Exporting OpenAPI specification...\n");
3089
- try {
3090
- const { resourceRegistry: resourceRegistry2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
3091
- const resources = resourceRegistry2.getAll();
3092
- if (resources.length === 0) {
3093
- console.warn("⚠️ No resources registered.");
3094
- console.log("\nTo export docs, you need to load your resources first:");
3095
- console.log(" arc docs ./openapi.json --entry ./index.js");
3096
- console.log("\nWhere index.js imports all your resource definitions.");
3097
- process.exit(1);
3098
- }
3099
- const spec = {
3100
- openapi: "3.0.0",
3101
- info: {
3102
- title: "Arc API",
3103
- version: "1.0.0",
3104
- description: "Auto-generated from Arc resources"
3105
- },
3106
- servers: [
3107
- {
3108
- url: "http://localhost:8040/api/v1",
3109
- description: "Development server"
3110
- }
3111
- ],
3112
- paths: {},
3113
- components: {
3114
- schemas: {},
3115
- securitySchemes: {
3116
- bearerAuth: {
3117
- type: "http",
3118
- scheme: "bearer",
3119
- bearerFormat: "JWT"
3120
- }
3121
- }
3122
- }
3123
- };
3124
- resources.forEach((resource) => {
3125
- const basePath = resource.prefix || `/${resource.name}s`;
3126
- spec.paths[basePath] = {
3127
- get: {
3128
- tags: [resource.name],
3129
- summary: `List ${resource.name}s`,
3130
- security: resource.permissions?.list ? [{ bearerAuth: [] }] : [],
3131
- parameters: [
3132
- {
3133
- name: "page",
3134
- in: "query",
3135
- schema: { type: "integer", default: 1 }
3136
- },
3137
- {
3138
- name: "limit",
3139
- in: "query",
3140
- schema: { type: "integer", default: 20 }
3141
- }
3142
- ],
3143
- responses: {
3144
- 200: {
3145
- description: "Successful response",
3146
- content: {
3147
- "application/json": {
3148
- schema: {
3149
- type: "object",
3150
- properties: {
3151
- success: { type: "boolean" },
3152
- data: {
3153
- type: "array",
3154
- items: { $ref: `#/components/schemas/${resource.name}` }
3155
- },
3156
- total: { type: "integer" },
3157
- page: { type: "integer" }
3158
- }
3159
- }
3160
- }
3161
- }
3162
- }
3163
- }
3164
- },
3165
- post: {
3166
- tags: [resource.name],
3167
- summary: `Create ${resource.name}`,
3168
- security: resource.permissions?.create ? [{ bearerAuth: [] }] : [],
3169
- requestBody: {
3170
- required: true,
3171
- content: {
3172
- "application/json": {
3173
- schema: { $ref: `#/components/schemas/${resource.name}` }
3174
- }
3175
- }
3176
- },
3177
- responses: {
3178
- 201: {
3179
- description: "Created successfully"
3180
- }
3181
- }
3182
- }
3183
- };
3184
- spec.paths[`${basePath}/{id}`] = {
3185
- get: {
3186
- tags: [resource.name],
3187
- summary: `Get ${resource.name} by ID`,
3188
- security: resource.permissions?.get ? [{ bearerAuth: [] }] : [],
3189
- parameters: [
3190
- {
3191
- name: "id",
3192
- in: "path",
3193
- required: true,
3194
- schema: { type: "string" }
3195
- }
3196
- ],
3197
- responses: {
3198
- 200: {
3199
- description: "Successful response"
3200
- }
3201
- }
3202
- },
3203
- patch: {
3204
- tags: [resource.name],
3205
- summary: `Update ${resource.name}`,
3206
- security: resource.permissions?.update ? [{ bearerAuth: [] }] : [],
3207
- parameters: [
3208
- {
3209
- name: "id",
3210
- in: "path",
3211
- required: true,
3212
- schema: { type: "string" }
3213
- }
3214
- ],
3215
- requestBody: {
3216
- required: true,
3217
- content: {
3218
- "application/json": {
3219
- schema: { $ref: `#/components/schemas/${resource.name}` }
3220
- }
3221
- }
3222
- },
3223
- responses: {
3224
- 200: {
3225
- description: "Updated successfully"
3226
- }
3227
- }
3228
- },
3229
- delete: {
3230
- tags: [resource.name],
3231
- summary: `Delete ${resource.name}`,
3232
- security: resource.permissions?.delete ? [{ bearerAuth: [] }] : [],
3233
- parameters: [
3234
- {
3235
- name: "id",
3236
- in: "path",
3237
- required: true,
3238
- schema: { type: "string" }
3239
- }
3240
- ],
3241
- responses: {
3242
- 200: {
3243
- description: "Deleted successfully"
3244
- }
3245
- }
3246
- }
3247
- };
3248
- spec.components.schemas[resource.name] = {
3249
- type: "object",
3250
- properties: {
3251
- _id: { type: "string" },
3252
- createdAt: { type: "string", format: "date-time" },
3253
- updatedAt: { type: "string", format: "date-time" }
3254
- }
3255
- };
3256
- });
3257
- const fullPath = join(process.cwd(), outputPath);
3258
- writeFileSync(fullPath, JSON.stringify(spec, null, 2));
3259
- console.log(`✅ OpenAPI spec exported to: ${fullPath}`);
3260
- console.log(`
3261
- Resources included: ${resources.length}`);
3262
- console.log(`Total endpoints: ${Object.keys(spec.paths).length}`);
3263
- } catch (error) {
3264
- console.error("Error:", error.message);
3265
- process.exit(1);
3266
- }
3267
- }
3268
-
3269
- export { exportDocs, generate, init, introspect };