@classytic/arc 1.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -794
- package/bin/arc.js +91 -52
- package/dist/EventTransport-BD2U0BTc.d.mts +100 -0
- package/dist/EventTransport-BD2U0BTc.d.mts.map +1 -0
- package/dist/HookSystem-BsGV-j2l.mjs +405 -0
- package/dist/HookSystem-BsGV-j2l.mjs.map +1 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs +250 -0
- package/dist/ResourceRegistry-DsN4KJjV.mjs.map +1 -0
- package/dist/adapters/index.d.mts +5 -0
- package/dist/adapters/index.mjs +3 -0
- package/dist/audit/index.d.mts +82 -0
- package/dist/audit/index.d.mts.map +1 -0
- package/dist/audit/index.mjs +276 -0
- package/dist/audit/index.mjs.map +1 -0
- package/dist/audit/mongodb.d.mts +5 -0
- package/dist/audit/mongodb.mjs +3 -0
- package/dist/audited-C3T5DTUx.mjs +141 -0
- package/dist/audited-C3T5DTUx.mjs.map +1 -0
- package/dist/auth/index.d.mts +189 -0
- package/dist/auth/index.d.mts.map +1 -0
- package/dist/auth/index.mjs +1102 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/auth/redis-session.d.mts +44 -0
- package/dist/auth/redis-session.d.mts.map +1 -0
- package/dist/auth/redis-session.mjs +76 -0
- package/dist/auth/redis-session.mjs.map +1 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs +250 -0
- package/dist/betterAuthOpenApi-BrHKeSAx.mjs.map +1 -0
- package/dist/cache/index.d.mts +146 -0
- package/dist/cache/index.d.mts.map +1 -0
- package/dist/cache/index.mjs +92 -0
- package/dist/cache/index.mjs.map +1 -0
- package/dist/caching-Bl28lYsR.mjs +94 -0
- package/dist/caching-Bl28lYsR.mjs.map +1 -0
- package/dist/chunk-C7Uep-_p.mjs +20 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs +1097 -0
- package/dist/circuitBreaker-DeY4FCjs.mjs.map +1 -0
- package/dist/cli/commands/describe.d.mts +19 -0
- package/dist/cli/commands/describe.d.mts.map +1 -0
- package/dist/cli/commands/describe.mjs +239 -0
- package/dist/cli/commands/describe.mjs.map +1 -0
- package/dist/cli/commands/docs.d.mts +14 -0
- package/dist/cli/commands/docs.d.mts.map +1 -0
- package/dist/cli/commands/docs.mjs +53 -0
- package/dist/cli/commands/docs.mjs.map +1 -0
- package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -1
- package/dist/cli/commands/generate.d.mts.map +1 -0
- package/dist/cli/commands/generate.mjs +358 -0
- package/dist/cli/commands/generate.mjs.map +1 -0
- package/dist/cli/commands/{init.d.ts → init.d.mts} +12 -8
- package/dist/cli/commands/init.d.mts.map +1 -0
- package/dist/cli/commands/{init.js → init.mjs} +807 -616
- package/dist/cli/commands/init.mjs.map +1 -0
- package/dist/cli/commands/introspect.d.mts +11 -0
- package/dist/cli/commands/introspect.d.mts.map +1 -0
- package/dist/cli/commands/introspect.mjs +76 -0
- package/dist/cli/commands/introspect.mjs.map +1 -0
- package/dist/cli/index.d.mts +17 -0
- package/dist/cli/index.d.mts.map +1 -0
- package/dist/cli/index.mjs +157 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/constants-DdXFXQtN.mjs +85 -0
- package/dist/constants-DdXFXQtN.mjs.map +1 -0
- package/dist/core/index.d.mts +5 -0
- package/dist/core/index.mjs +4 -0
- package/dist/createApp-CUgNqegw.mjs +560 -0
- package/dist/createApp-CUgNqegw.mjs.map +1 -0
- package/dist/defineResource-k0_BDn8v.mjs +2197 -0
- package/dist/defineResource-k0_BDn8v.mjs.map +1 -0
- package/dist/discovery/index.d.mts +47 -0
- package/dist/discovery/index.d.mts.map +1 -0
- package/dist/discovery/index.mjs +110 -0
- package/dist/discovery/index.mjs.map +1 -0
- package/dist/docs/index.d.mts +163 -0
- package/dist/docs/index.d.mts.map +1 -0
- package/dist/docs/index.mjs +73 -0
- package/dist/docs/index.mjs.map +1 -0
- package/dist/elevation-BRy3yFWT.mjs +113 -0
- package/dist/elevation-BRy3yFWT.mjs.map +1 -0
- package/dist/elevation-B_2dRLVP.d.mts +88 -0
- package/dist/elevation-B_2dRLVP.d.mts.map +1 -0
- package/dist/errorHandler-BbcgBmIH.d.mts +73 -0
- package/dist/errorHandler-BbcgBmIH.d.mts.map +1 -0
- package/dist/errorHandler-C1okiriz.mjs +109 -0
- package/dist/errorHandler-C1okiriz.mjs.map +1 -0
- package/dist/errors-B9bZok84.mjs +212 -0
- package/dist/errors-B9bZok84.mjs.map +1 -0
- package/dist/errors-ChKiFz62.d.mts +125 -0
- package/dist/errors-ChKiFz62.d.mts.map +1 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts +125 -0
- package/dist/eventPlugin-CTrLH3mt.d.mts.map +1 -0
- package/dist/eventPlugin-DGR_B2on.mjs +230 -0
- package/dist/eventPlugin-DGR_B2on.mjs.map +1 -0
- package/dist/events/index.d.mts +54 -0
- package/dist/events/index.d.mts.map +1 -0
- package/dist/events/index.mjs +52 -0
- package/dist/events/index.mjs.map +1 -0
- package/dist/events/transports/redis-stream-entry.d.mts +2 -0
- package/dist/events/transports/redis-stream-entry.mjs +178 -0
- package/dist/events/transports/redis-stream-entry.mjs.map +1 -0
- package/dist/events/transports/redis.d.mts +77 -0
- package/dist/events/transports/redis.d.mts.map +1 -0
- package/dist/events/transports/redis.mjs +125 -0
- package/dist/events/transports/redis.mjs.map +1 -0
- package/dist/externalPaths-DlINfKbP.d.mts +51 -0
- package/dist/externalPaths-DlINfKbP.d.mts.map +1 -0
- package/dist/factory/index.d.mts +64 -0
- package/dist/factory/index.d.mts.map +1 -0
- package/dist/factory/index.mjs +3 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts +217 -0
- package/dist/fastifyAdapter-BkrGrlFi.d.mts.map +1 -0
- package/dist/fields-DyaDVX4J.d.mts +110 -0
- package/dist/fields-DyaDVX4J.d.mts.map +1 -0
- package/dist/fields-iagOozy0.mjs +115 -0
- package/dist/fields-iagOozy0.mjs.map +1 -0
- package/dist/hooks/index.d.mts +4 -0
- package/dist/hooks/index.mjs +3 -0
- package/dist/idempotency/index.d.mts +97 -0
- package/dist/idempotency/index.d.mts.map +1 -0
- package/dist/idempotency/index.mjs +320 -0
- package/dist/idempotency/index.mjs.map +1 -0
- package/dist/idempotency/mongodb.d.mts +2 -0
- package/dist/idempotency/mongodb.mjs +115 -0
- package/dist/idempotency/mongodb.mjs.map +1 -0
- package/dist/idempotency/redis.d.mts +2 -0
- package/dist/idempotency/redis.mjs +104 -0
- package/dist/idempotency/redis.mjs.map +1 -0
- package/dist/index.d.mts +261 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +105 -0
- package/dist/index.mjs.map +1 -0
- package/dist/integrations/event-gateway.d.mts +47 -0
- package/dist/integrations/event-gateway.d.mts.map +1 -0
- package/dist/integrations/event-gateway.mjs +44 -0
- package/dist/integrations/event-gateway.mjs.map +1 -0
- package/dist/integrations/index.d.mts +5 -0
- package/dist/integrations/index.mjs +1 -0
- package/dist/integrations/jobs.d.mts +104 -0
- package/dist/integrations/jobs.d.mts.map +1 -0
- package/dist/integrations/jobs.mjs +124 -0
- package/dist/integrations/jobs.mjs.map +1 -0
- package/dist/integrations/streamline.d.mts +61 -0
- package/dist/integrations/streamline.d.mts.map +1 -0
- package/dist/integrations/streamline.mjs +126 -0
- package/dist/integrations/streamline.mjs.map +1 -0
- package/dist/integrations/websocket.d.mts +83 -0
- package/dist/integrations/websocket.d.mts.map +1 -0
- package/dist/integrations/websocket.mjs +289 -0
- package/dist/integrations/websocket.mjs.map +1 -0
- package/dist/interface-B01JvPVc.d.mts +78 -0
- package/dist/interface-B01JvPVc.d.mts.map +1 -0
- package/dist/interface-CZe8IkMf.d.mts +55 -0
- package/dist/interface-CZe8IkMf.d.mts.map +1 -0
- package/dist/interface-Ch8HU9uM.d.mts +1098 -0
- package/dist/interface-Ch8HU9uM.d.mts.map +1 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs +54 -0
- package/dist/introspectionPlugin-rFdO8ZUa.mjs.map +1 -0
- package/dist/keys-BqNejWup.mjs +43 -0
- package/dist/keys-BqNejWup.mjs.map +1 -0
- package/dist/logger-Df2O2WsW.mjs +79 -0
- package/dist/logger-Df2O2WsW.mjs.map +1 -0
- package/dist/memory-cQgelFOj.mjs +144 -0
- package/dist/memory-cQgelFOj.mjs.map +1 -0
- package/dist/migrations/index.d.mts +157 -0
- package/dist/migrations/index.d.mts.map +1 -0
- package/dist/migrations/index.mjs +261 -0
- package/dist/migrations/index.mjs.map +1 -0
- package/dist/mongodb-BfJVlUJH.mjs +94 -0
- package/dist/mongodb-BfJVlUJH.mjs.map +1 -0
- package/dist/mongodb-CGzRbfAK.d.mts +119 -0
- package/dist/mongodb-CGzRbfAK.d.mts.map +1 -0
- package/dist/mongodb-JN-9JA7K.d.mts +72 -0
- package/dist/mongodb-JN-9JA7K.d.mts.map +1 -0
- package/dist/openapi-G3Cw7XuM.mjs +524 -0
- package/dist/openapi-G3Cw7XuM.mjs.map +1 -0
- package/dist/org/index.d.mts +69 -0
- package/dist/org/index.d.mts.map +1 -0
- package/dist/org/index.mjs +514 -0
- package/dist/org/index.mjs.map +1 -0
- package/dist/org/types.d.mts +83 -0
- package/dist/org/types.d.mts.map +1 -0
- package/dist/org/types.mjs +1 -0
- package/dist/permissions/index.d.mts +279 -0
- package/dist/permissions/index.d.mts.map +1 -0
- package/dist/permissions/index.mjs +579 -0
- package/dist/permissions/index.mjs.map +1 -0
- package/dist/plugins/index.d.mts +173 -0
- package/dist/plugins/index.d.mts.map +1 -0
- package/dist/plugins/index.mjs +523 -0
- package/dist/plugins/index.mjs.map +1 -0
- package/dist/plugins/response-cache.d.mts +88 -0
- package/dist/plugins/response-cache.d.mts.map +1 -0
- package/dist/plugins/response-cache.mjs +284 -0
- package/dist/plugins/response-cache.mjs.map +1 -0
- package/dist/plugins/tracing-entry.d.mts +2 -0
- package/dist/plugins/tracing-entry.mjs +186 -0
- package/dist/plugins/tracing-entry.mjs.map +1 -0
- package/dist/pluralize-CEweyOEm.mjs +87 -0
- package/dist/pluralize-CEweyOEm.mjs.map +1 -0
- package/dist/policies/{index.d.ts → index.d.mts} +204 -169
- package/dist/policies/index.d.mts.map +1 -0
- package/dist/policies/index.mjs +322 -0
- package/dist/policies/index.mjs.map +1 -0
- package/dist/presets/{index.d.ts → index.d.mts} +63 -131
- package/dist/presets/index.d.mts.map +1 -0
- package/dist/presets/index.mjs +144 -0
- package/dist/presets/index.mjs.map +1 -0
- package/dist/presets/multiTenant.d.mts +25 -0
- package/dist/presets/multiTenant.d.mts.map +1 -0
- package/dist/presets/multiTenant.mjs +114 -0
- package/dist/presets/multiTenant.mjs.map +1 -0
- package/dist/presets-BITljm96.mjs +120 -0
- package/dist/presets-BITljm96.mjs.map +1 -0
- package/dist/presets-DzSMwlKj.d.mts +58 -0
- package/dist/presets-DzSMwlKj.d.mts.map +1 -0
- package/dist/prisma-DJbMt3yf.mjs +628 -0
- package/dist/prisma-DJbMt3yf.mjs.map +1 -0
- package/dist/prisma-Dg9GoVdj.d.mts +275 -0
- package/dist/prisma-Dg9GoVdj.d.mts.map +1 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts +72 -0
- package/dist/queryCachePlugin-7THaI5mt.d.mts.map +1 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs +139 -0
- package/dist/queryCachePlugin-DMBnp2Q0.mjs.map +1 -0
- package/dist/redis-D-JAeLtm.d.mts +50 -0
- package/dist/redis-D-JAeLtm.d.mts.map +1 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts +104 -0
- package/dist/redis-stream-Bdh_vUU8.d.mts.map +1 -0
- package/dist/registry/index.d.mts +12 -0
- package/dist/registry/index.d.mts.map +1 -0
- package/dist/registry/index.mjs +4 -0
- package/dist/requestContext-QQD6ROJc.mjs +56 -0
- package/dist/requestContext-QQD6ROJc.mjs.map +1 -0
- package/dist/schemaConverter-BwrmWroW.mjs +99 -0
- package/dist/schemaConverter-BwrmWroW.mjs.map +1 -0
- package/dist/schemas/index.d.mts +64 -0
- package/dist/schemas/index.d.mts.map +1 -0
- package/dist/schemas/index.mjs +83 -0
- package/dist/schemas/index.mjs.map +1 -0
- package/dist/scope/index.d.mts +22 -0
- package/dist/scope/index.d.mts.map +1 -0
- package/dist/scope/index.mjs +66 -0
- package/dist/scope/index.mjs.map +1 -0
- package/dist/sessionManager-jPKLbHE0.d.mts +187 -0
- package/dist/sessionManager-jPKLbHE0.d.mts.map +1 -0
- package/dist/sse-B3c3_yZp.mjs +124 -0
- package/dist/sse-B3c3_yZp.mjs.map +1 -0
- package/dist/testing/index.d.mts +908 -0
- package/dist/testing/index.d.mts.map +1 -0
- package/dist/testing/index.mjs +1977 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/tracing-Cc7vVQPp.d.mts +71 -0
- package/dist/tracing-Cc7vVQPp.d.mts.map +1 -0
- package/dist/typeGuards-DhMNLuvU.mjs +10 -0
- package/dist/typeGuards-DhMNLuvU.mjs.map +1 -0
- package/dist/types/index.d.mts +947 -0
- package/dist/types/index.d.mts.map +1 -0
- package/dist/types/index.mjs +15 -0
- package/dist/types/index.mjs.map +1 -0
- package/dist/types-Beqn1Un7.mjs +39 -0
- package/dist/types-Beqn1Un7.mjs.map +1 -0
- package/dist/types-CIgB7UUl.d.mts +446 -0
- package/dist/types-CIgB7UUl.d.mts.map +1 -0
- package/dist/types-aYB4V7uN.d.mts +87 -0
- package/dist/types-aYB4V7uN.d.mts.map +1 -0
- package/dist/utils/index.d.mts +748 -0
- package/dist/utils/index.d.mts.map +1 -0
- package/dist/utils/index.mjs +6 -0
- package/package.json +194 -68
- package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
- package/dist/adapters/index.d.ts +0 -237
- package/dist/adapters/index.js +0 -668
- package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
- package/dist/audit/index.d.ts +0 -195
- package/dist/audit/index.js +0 -319
- package/dist/auth/index.d.ts +0 -47
- package/dist/auth/index.js +0 -174
- package/dist/cli/commands/docs.d.ts +0 -11
- package/dist/cli/commands/docs.js +0 -474
- package/dist/cli/commands/generate.js +0 -334
- package/dist/cli/commands/introspect.d.ts +0 -8
- package/dist/cli/commands/introspect.js +0 -338
- package/dist/cli/index.d.ts +0 -4
- package/dist/cli/index.js +0 -3269
- package/dist/core/index.d.ts +0 -220
- package/dist/core/index.js +0 -2786
- package/dist/createApp-Ce9wl8W9.d.ts +0 -77
- package/dist/docs/index.d.ts +0 -166
- package/dist/docs/index.js +0 -658
- package/dist/errors-8WIxGS_6.d.ts +0 -122
- package/dist/events/index.d.ts +0 -117
- package/dist/events/index.js +0 -89
- package/dist/factory/index.d.ts +0 -38
- package/dist/factory/index.js +0 -1652
- package/dist/hooks/index.d.ts +0 -4
- package/dist/hooks/index.js +0 -199
- package/dist/idempotency/index.d.ts +0 -323
- package/dist/idempotency/index.js +0 -500
- package/dist/index-B4t03KQ0.d.ts +0 -1366
- package/dist/index.d.ts +0 -135
- package/dist/index.js +0 -4756
- package/dist/migrations/index.d.ts +0 -185
- package/dist/migrations/index.js +0 -274
- package/dist/org/index.d.ts +0 -129
- package/dist/org/index.js +0 -220
- package/dist/permissions/index.d.ts +0 -144
- package/dist/permissions/index.js +0 -103
- package/dist/plugins/index.d.ts +0 -46
- package/dist/plugins/index.js +0 -1069
- package/dist/policies/index.js +0 -196
- package/dist/presets/index.js +0 -384
- package/dist/presets/multiTenant.d.ts +0 -39
- package/dist/presets/multiTenant.js +0 -112
- package/dist/registry/index.d.ts +0 -16
- package/dist/registry/index.js +0 -253
- package/dist/testing/index.d.ts +0 -618
- package/dist/testing/index.js +0 -48020
- package/dist/types/index.d.ts +0 -4
- package/dist/types/index.js +0 -8
- package/dist/types-B99TBmFV.d.ts +0 -76
- package/dist/types-BvckRbs2.d.ts +0 -143
- package/dist/utils/index.d.ts +0 -679
- package/dist/utils/index.js +0 -931
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 };
|