@classytic/arc 1.1.0 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/README.md +247 -794
  2. package/bin/arc.js +91 -52
  3. package/dist/EventTransport-BkUDYZEb.d.mts +99 -0
  4. package/dist/HookSystem-BsGV-j2l.mjs +404 -0
  5. package/dist/ResourceRegistry-7Ic20ZMw.mjs +249 -0
  6. package/dist/adapters/index.d.mts +5 -0
  7. package/dist/adapters/index.mjs +3 -0
  8. package/dist/audit/index.d.mts +81 -0
  9. package/dist/audit/index.mjs +275 -0
  10. package/dist/audit/mongodb.d.mts +5 -0
  11. package/dist/audit/mongodb.mjs +3 -0
  12. package/dist/audited-CGdLiSlE.mjs +140 -0
  13. package/dist/auth/index.d.mts +188 -0
  14. package/dist/auth/index.mjs +1096 -0
  15. package/dist/auth/redis-session.d.mts +43 -0
  16. package/dist/auth/redis-session.mjs +75 -0
  17. package/dist/betterAuthOpenApi-DjWDddNc.mjs +249 -0
  18. package/dist/cache/index.d.mts +145 -0
  19. package/dist/cache/index.mjs +91 -0
  20. package/dist/caching-GSDJcA6-.mjs +93 -0
  21. package/dist/chunk-C7Uep-_p.mjs +20 -0
  22. package/dist/circuitBreaker-DYhWBW_D.mjs +1096 -0
  23. package/dist/cli/commands/describe.d.mts +18 -0
  24. package/dist/cli/commands/describe.mjs +238 -0
  25. package/dist/cli/commands/docs.d.mts +13 -0
  26. package/dist/cli/commands/docs.mjs +52 -0
  27. package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -2
  28. package/dist/cli/commands/generate.mjs +357 -0
  29. package/dist/cli/commands/{init.d.ts → init.d.mts} +11 -8
  30. package/dist/cli/commands/{init.js → init.mjs} +807 -617
  31. package/dist/cli/commands/introspect.d.mts +10 -0
  32. package/dist/cli/commands/introspect.mjs +75 -0
  33. package/dist/cli/index.d.mts +16 -0
  34. package/dist/cli/index.mjs +156 -0
  35. package/dist/constants-DdXFXQtN.mjs +84 -0
  36. package/dist/core/index.d.mts +5 -0
  37. package/dist/core/index.mjs +4 -0
  38. package/dist/createApp-D2D5XXaV.mjs +559 -0
  39. package/dist/defineResource-PXzSJ15_.mjs +2197 -0
  40. package/dist/discovery/index.d.mts +46 -0
  41. package/dist/discovery/index.mjs +109 -0
  42. package/dist/docs/index.d.mts +162 -0
  43. package/dist/docs/index.mjs +74 -0
  44. package/dist/elevation-DGo5shaX.d.mts +87 -0
  45. package/dist/elevation-DSTbVvYj.mjs +113 -0
  46. package/dist/errorHandler-C3GY3_ow.mjs +108 -0
  47. package/dist/errorHandler-CW3OOeYq.d.mts +72 -0
  48. package/dist/errors-DAWRdiYP.d.mts +124 -0
  49. package/dist/errors-DBANPbGr.mjs +211 -0
  50. package/dist/eventPlugin-BEOvaDqo.mjs +229 -0
  51. package/dist/eventPlugin-H6wDDjGO.d.mts +124 -0
  52. package/dist/events/index.d.mts +53 -0
  53. package/dist/events/index.mjs +51 -0
  54. package/dist/events/transports/redis-stream-entry.d.mts +2 -0
  55. package/dist/events/transports/redis-stream-entry.mjs +177 -0
  56. package/dist/events/transports/redis.d.mts +76 -0
  57. package/dist/events/transports/redis.mjs +124 -0
  58. package/dist/externalPaths-SyPF2tgK.d.mts +50 -0
  59. package/dist/factory/index.d.mts +63 -0
  60. package/dist/factory/index.mjs +3 -0
  61. package/dist/fastifyAdapter-C8DlE0YH.d.mts +216 -0
  62. package/dist/fields-Bi_AVKSo.d.mts +109 -0
  63. package/dist/fields-CTd_CrKr.mjs +114 -0
  64. package/dist/hooks/index.d.mts +4 -0
  65. package/dist/hooks/index.mjs +3 -0
  66. package/dist/idempotency/index.d.mts +96 -0
  67. package/dist/idempotency/index.mjs +319 -0
  68. package/dist/idempotency/mongodb.d.mts +2 -0
  69. package/dist/idempotency/mongodb.mjs +114 -0
  70. package/dist/idempotency/redis.d.mts +2 -0
  71. package/dist/idempotency/redis.mjs +103 -0
  72. package/dist/index.d.mts +260 -0
  73. package/dist/index.mjs +104 -0
  74. package/dist/integrations/event-gateway.d.mts +46 -0
  75. package/dist/integrations/event-gateway.mjs +43 -0
  76. package/dist/integrations/index.d.mts +5 -0
  77. package/dist/integrations/index.mjs +1 -0
  78. package/dist/integrations/jobs.d.mts +103 -0
  79. package/dist/integrations/jobs.mjs +123 -0
  80. package/dist/integrations/streamline.d.mts +60 -0
  81. package/dist/integrations/streamline.mjs +125 -0
  82. package/dist/integrations/websocket.d.mts +82 -0
  83. package/dist/integrations/websocket.mjs +288 -0
  84. package/dist/interface-CSNjltAc.d.mts +77 -0
  85. package/dist/interface-DTbsvIWe.d.mts +54 -0
  86. package/dist/interface-e9XfSsUV.d.mts +1097 -0
  87. package/dist/introspectionPlugin-B3JkrjwU.mjs +53 -0
  88. package/dist/keys-DhqDRxv3.mjs +42 -0
  89. package/dist/logger-ByrvQWZO.mjs +78 -0
  90. package/dist/memory-B2v7KrCB.mjs +143 -0
  91. package/dist/migrations/index.d.mts +156 -0
  92. package/dist/migrations/index.mjs +260 -0
  93. package/dist/mongodb-ClykrfGo.d.mts +118 -0
  94. package/dist/mongodb-DNKEExbf.mjs +93 -0
  95. package/dist/mongodb-Dg8O_gvd.d.mts +71 -0
  96. package/dist/openapi-9nB_kiuR.mjs +525 -0
  97. package/dist/org/index.d.mts +68 -0
  98. package/dist/org/index.mjs +513 -0
  99. package/dist/org/types.d.mts +82 -0
  100. package/dist/org/types.mjs +1 -0
  101. package/dist/permissions/index.d.mts +278 -0
  102. package/dist/permissions/index.mjs +579 -0
  103. package/dist/plugins/index.d.mts +172 -0
  104. package/dist/plugins/index.mjs +522 -0
  105. package/dist/plugins/response-cache.d.mts +87 -0
  106. package/dist/plugins/response-cache.mjs +283 -0
  107. package/dist/plugins/tracing-entry.d.mts +2 -0
  108. package/dist/plugins/tracing-entry.mjs +185 -0
  109. package/dist/pluralize-CM-jZg7p.mjs +86 -0
  110. package/dist/policies/{index.d.ts → index.d.mts} +204 -170
  111. package/dist/policies/index.mjs +321 -0
  112. package/dist/presets/{index.d.ts → index.d.mts} +62 -131
  113. package/dist/presets/index.mjs +143 -0
  114. package/dist/presets/multiTenant.d.mts +24 -0
  115. package/dist/presets/multiTenant.mjs +113 -0
  116. package/dist/presets-BTeYbw7h.d.mts +57 -0
  117. package/dist/presets-CeFtfDR8.mjs +119 -0
  118. package/dist/prisma-C3iornoK.d.mts +274 -0
  119. package/dist/prisma-DJbMt3yf.mjs +627 -0
  120. package/dist/queryCachePlugin-B6R0d4av.mjs +138 -0
  121. package/dist/queryCachePlugin-Q6SYuHZ6.d.mts +71 -0
  122. package/dist/redis-UwjEp8Ea.d.mts +49 -0
  123. package/dist/redis-stream-CBg0upHI.d.mts +103 -0
  124. package/dist/registry/index.d.mts +11 -0
  125. package/dist/registry/index.mjs +4 -0
  126. package/dist/requestContext-xi6OKBL-.mjs +55 -0
  127. package/dist/schemaConverter-Dtg0Kt9T.mjs +98 -0
  128. package/dist/schemas/index.d.mts +63 -0
  129. package/dist/schemas/index.mjs +82 -0
  130. package/dist/scope/index.d.mts +21 -0
  131. package/dist/scope/index.mjs +65 -0
  132. package/dist/sessionManager-D_iEHjQl.d.mts +186 -0
  133. package/dist/sse-DkqQ1uxb.mjs +123 -0
  134. package/dist/testing/index.d.mts +907 -0
  135. package/dist/testing/index.mjs +1976 -0
  136. package/dist/tracing-8CEbhF0w.d.mts +70 -0
  137. package/dist/typeGuards-DwxA1t_L.mjs +9 -0
  138. package/dist/types/index.d.mts +946 -0
  139. package/dist/types/index.mjs +14 -0
  140. package/dist/types-B0dhNrnd.d.mts +445 -0
  141. package/dist/types-Beqn1Un7.mjs +38 -0
  142. package/dist/types-DelU6kln.mjs +25 -0
  143. package/dist/types-RLkFVgaw.d.mts +101 -0
  144. package/dist/utils/index.d.mts +747 -0
  145. package/dist/utils/index.mjs +6 -0
  146. package/package.json +194 -68
  147. package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
  148. package/dist/adapters/index.d.ts +0 -237
  149. package/dist/adapters/index.js +0 -668
  150. package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
  151. package/dist/audit/index.d.ts +0 -195
  152. package/dist/audit/index.js +0 -319
  153. package/dist/auth/index.d.ts +0 -47
  154. package/dist/auth/index.js +0 -174
  155. package/dist/cli/commands/docs.d.ts +0 -11
  156. package/dist/cli/commands/docs.js +0 -474
  157. package/dist/cli/commands/generate.js +0 -334
  158. package/dist/cli/commands/introspect.d.ts +0 -8
  159. package/dist/cli/commands/introspect.js +0 -338
  160. package/dist/cli/index.d.ts +0 -4
  161. package/dist/cli/index.js +0 -3269
  162. package/dist/core/index.d.ts +0 -220
  163. package/dist/core/index.js +0 -2786
  164. package/dist/createApp-Ce9wl8W9.d.ts +0 -77
  165. package/dist/docs/index.d.ts +0 -166
  166. package/dist/docs/index.js +0 -658
  167. package/dist/errors-8WIxGS_6.d.ts +0 -122
  168. package/dist/events/index.d.ts +0 -117
  169. package/dist/events/index.js +0 -89
  170. package/dist/factory/index.d.ts +0 -38
  171. package/dist/factory/index.js +0 -1652
  172. package/dist/hooks/index.d.ts +0 -4
  173. package/dist/hooks/index.js +0 -199
  174. package/dist/idempotency/index.d.ts +0 -323
  175. package/dist/idempotency/index.js +0 -500
  176. package/dist/index-B4t03KQ0.d.ts +0 -1366
  177. package/dist/index.d.ts +0 -135
  178. package/dist/index.js +0 -4756
  179. package/dist/migrations/index.d.ts +0 -185
  180. package/dist/migrations/index.js +0 -274
  181. package/dist/org/index.d.ts +0 -129
  182. package/dist/org/index.js +0 -220
  183. package/dist/permissions/index.d.ts +0 -144
  184. package/dist/permissions/index.js +0 -103
  185. package/dist/plugins/index.d.ts +0 -46
  186. package/dist/plugins/index.js +0 -1069
  187. package/dist/policies/index.js +0 -196
  188. package/dist/presets/index.js +0 -384
  189. package/dist/presets/multiTenant.d.ts +0 -39
  190. package/dist/presets/multiTenant.js +0 -112
  191. package/dist/registry/index.d.ts +0 -16
  192. package/dist/registry/index.js +0 -253
  193. package/dist/testing/index.d.ts +0 -618
  194. package/dist/testing/index.js +0 -48020
  195. package/dist/types/index.d.ts +0 -4
  196. package/dist/types/index.js +0 -8
  197. package/dist/types-B99TBmFV.d.ts +0 -76
  198. package/dist/types-BvckRbs2.d.ts +0 -143
  199. package/dist/utils/index.d.ts +0 -679
  200. package/dist/utils/index.js +0 -931
@@ -0,0 +1,87 @@
1
+ import { FastifyPluginAsync, FastifyRequest } from "fastify";
2
+
3
+ //#region src/plugins/response-cache.d.ts
4
+ interface ResponseCacheRule {
5
+ /** Path prefix to match (e.g., '/api/products') */
6
+ match: string;
7
+ /** TTL in seconds for this path (0 = don't cache) */
8
+ ttl: number;
9
+ }
10
+ interface ResponseCacheOptions {
11
+ /** Maximum number of cached entries (default: 500). LRU eviction when exceeded. */
12
+ maxEntries?: number;
13
+ /** Default TTL in seconds (default: 30). Set to 0 to require explicit rules. */
14
+ defaultTTL?: number;
15
+ /** Per-path cache rules */
16
+ rules?: ResponseCacheRule[];
17
+ /** Paths to exclude from caching (prefix match) */
18
+ exclude?: string[];
19
+ /** HTTP methods that trigger cache invalidation (default: POST, PUT, PATCH, DELETE) */
20
+ invalidateOn?: string[];
21
+ /** Whether to add X-Cache header (HIT/MISS) to responses (default: true) */
22
+ xCacheHeader?: boolean;
23
+ /** Enable stats endpoint at this path (default: null = disabled) */
24
+ statsPath?: string | null;
25
+ /** Custom cache key function (default: method + url + userId + orgId) */
26
+ keyFn?: (request: FastifyRequest) => string | null;
27
+ /**
28
+ * Auto-invalidate cache entries when CRUD domain events fire (requires eventPlugin).
29
+ *
30
+ * - `true`: Invalidate resource prefix on its own CRUD events
31
+ * - `{ patterns: { 'order.*': ['/api/products'] } }`: Cross-resource invalidation rules
32
+ * - `false` / omitted: Disabled (default)
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * await fastify.register(responseCachePlugin, {
37
+ * eventInvalidation: {
38
+ * patterns: {
39
+ * 'order.*': ['/api/products', '/api/inventory'],
40
+ * },
41
+ * },
42
+ * });
43
+ * ```
44
+ */
45
+ eventInvalidation?: boolean | {
46
+ patterns?: Record<string, string[]>;
47
+ };
48
+ }
49
+ interface ResponseCacheStats {
50
+ entries: number;
51
+ maxEntries: number;
52
+ hits: number;
53
+ misses: number;
54
+ hitRate: number;
55
+ evictions: number;
56
+ }
57
+ declare module "fastify" {
58
+ interface FastifyInstance {
59
+ responseCache: {
60
+ /** Invalidate all cached responses matching a path prefix */invalidate: (pathPrefix: string) => number; /** Clear the entire cache */
61
+ invalidateAll: () => void; /** Get cache statistics */
62
+ stats: () => ResponseCacheStats;
63
+ /**
64
+ * Route-level preHandler for cache lookup.
65
+ * Wire AFTER authenticate in the preHandler chain so that
66
+ * `request.user` / `request.scope` are populated before the
67
+ * cache key is computed.
68
+ *
69
+ * `createCrudRouter` injects this automatically for GET routes.
70
+ * For custom routes, add it manually:
71
+ * ```typescript
72
+ * fastify.get('/data', {
73
+ * preHandler: [fastify.authenticate, fastify.responseCache.middleware],
74
+ * }, handler);
75
+ * ```
76
+ */
77
+ middleware: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
78
+ };
79
+ }
80
+ interface FastifyRequest {
81
+ /** @internal Cache TTL in seconds — set by onRequest, consumed by middleware + onSend */
82
+ __arcCacheTTL?: number;
83
+ }
84
+ }
85
+ declare const responseCachePlugin: FastifyPluginAsync<ResponseCacheOptions>;
86
+ //#endregion
87
+ export { ResponseCacheOptions, ResponseCacheRule, ResponseCacheStats, responseCachePlugin as default, responseCachePlugin };
@@ -0,0 +1,283 @@
1
+ import { t as hasEvents } from "../typeGuards-DwxA1t_L.mjs";
2
+ import fp from "fastify-plugin";
3
+
4
+ //#region src/plugins/response-cache.ts
5
+ /**
6
+ * Response Cache Plugin for Arc
7
+ *
8
+ * In-memory LRU/TTL response cache that sits in front of your database.
9
+ * Caches serialized responses for GET requests, dramatically reducing DB load
10
+ * for frequently accessed resources.
11
+ *
12
+ * Features:
13
+ * - LRU eviction with configurable max entries
14
+ * - Per-route TTL configuration
15
+ * - Automatic invalidation on mutations (POST/PUT/PATCH/DELETE)
16
+ * - Manual invalidation via `fastify.responseCache.invalidate()`
17
+ * - Cache stats endpoint for monitoring
18
+ * - Zero external deps — pure in-memory, serverless-safe
19
+ *
20
+ * NOTE: This cache is per-instance (in-memory). In multi-instance deployments,
21
+ * each instance maintains its own cache. For cross-instance invalidation,
22
+ * wire `fastify.responseCache.invalidate()` to your event bus manually.
23
+ *
24
+ * ## Auth Safety
25
+ *
26
+ * The cache check runs as a **route-level middleware** (`responseCache.middleware`)
27
+ * that must be wired AFTER authentication in the preHandler chain. Arc's
28
+ * `createCrudRouter` does this automatically. For custom routes, wire it
29
+ * manually:
30
+ *
31
+ * ```typescript
32
+ * fastify.get('/data', {
33
+ * preHandler: [fastify.authenticate, fastify.responseCache.middleware],
34
+ * }, handler);
35
+ * ```
36
+ *
37
+ * This ensures cached responses are never served before auth validates the
38
+ * caller's identity. The default cache key includes `userId` and `orgId`
39
+ * to prevent cross-caller data leaks.
40
+ *
41
+ * This is a SEPARATE subpath import — only loaded when explicitly used:
42
+ * import { responseCachePlugin } from '@classytic/arc/plugins/response-cache';
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * import { responseCachePlugin } from '@classytic/arc/plugins/response-cache';
47
+ *
48
+ * await fastify.register(responseCachePlugin, {
49
+ * maxEntries: 1000,
50
+ * defaultTTL: 30, // 30 seconds
51
+ * rules: [
52
+ * { match: '/api/products', ttl: 120 }, // 2 min for products
53
+ * { match: '/api/categories', ttl: 300 }, // 5 min for categories
54
+ * { match: '/api/users', ttl: 0 }, // never cache users
55
+ * ],
56
+ * invalidateOn: ['POST', 'PUT', 'PATCH', 'DELETE'],
57
+ * });
58
+ *
59
+ * // Manual invalidation
60
+ * fastify.responseCache.invalidate('/api/products');
61
+ * fastify.responseCache.invalidateAll();
62
+ *
63
+ * // Stats
64
+ * const stats = fastify.responseCache.stats();
65
+ * // { entries: 42, hits: 1250, misses: 180, hitRate: 0.87, evictions: 5 }
66
+ * ```
67
+ */
68
+ /**
69
+ * Simple LRU cache using Map iteration order.
70
+ * Map in JS preserves insertion order — we re-insert on access to make it LRU.
71
+ */
72
+ var LRUCache = class {
73
+ cache = /* @__PURE__ */ new Map();
74
+ maxEntries;
75
+ invalidatedPrefixes = /* @__PURE__ */ new Map();
76
+ hits = 0;
77
+ misses = 0;
78
+ evictions = 0;
79
+ constructor(maxEntries) {
80
+ this.maxEntries = maxEntries;
81
+ }
82
+ get(key) {
83
+ const entry = this.cache.get(key);
84
+ if (!entry) {
85
+ this.misses++;
86
+ return null;
87
+ }
88
+ if (Date.now() - entry.createdAt > entry.ttl) {
89
+ this.cache.delete(key);
90
+ this.misses++;
91
+ return null;
92
+ }
93
+ this.cache.delete(key);
94
+ this.cache.set(key, entry);
95
+ this.hits++;
96
+ return entry;
97
+ }
98
+ set(key, entry) {
99
+ if (this.isPrefixLocked(key)) return;
100
+ if (this.cache.has(key)) this.cache.delete(key);
101
+ while (this.cache.size >= this.maxEntries) {
102
+ const firstKey = this.cache.keys().next().value;
103
+ if (firstKey !== void 0) {
104
+ this.cache.delete(firstKey);
105
+ this.evictions++;
106
+ }
107
+ }
108
+ this.cache.set(key, entry);
109
+ }
110
+ /** Invalidate entries matching a path prefix and lock it from caching to allow DB replicas to catch up */
111
+ invalidatePrefix(prefix, jitterMs = 1500) {
112
+ let count = 0;
113
+ for (const key of this.cache.keys()) {
114
+ const colonIdx = key.indexOf(":");
115
+ if ((colonIdx >= 0 ? key.slice(colonIdx + 1) : key).split("?")[0].startsWith(prefix)) {
116
+ this.cache.delete(key);
117
+ count++;
118
+ }
119
+ }
120
+ if (jitterMs > 0) this.invalidatedPrefixes.set(prefix, Date.now() + jitterMs);
121
+ return count;
122
+ }
123
+ /** Check if a key falls under a recently invalidated prefix */
124
+ isPrefixLocked(key) {
125
+ if (this.invalidatedPrefixes.size === 0) return false;
126
+ const colonIdx = key.indexOf(":");
127
+ const pathOnly = (colonIdx >= 0 ? key.slice(colonIdx + 1) : key).split("?")[0];
128
+ const now = Date.now();
129
+ for (const [prefix, expiresAt] of this.invalidatedPrefixes.entries()) if (now > expiresAt) this.invalidatedPrefixes.delete(prefix);
130
+ else if (pathOnly.startsWith(prefix)) return true;
131
+ return false;
132
+ }
133
+ /** Clear all entries */
134
+ clear() {
135
+ this.cache.clear();
136
+ }
137
+ get size() {
138
+ return this.cache.size;
139
+ }
140
+ getStats(maxEntries) {
141
+ const total = this.hits + this.misses;
142
+ return {
143
+ entries: this.cache.size,
144
+ maxEntries,
145
+ hits: this.hits,
146
+ misses: this.misses,
147
+ hitRate: total > 0 ? Math.round(this.hits / total * 100) / 100 : 0,
148
+ evictions: this.evictions
149
+ };
150
+ }
151
+ };
152
+ const responseCachePluginImpl = async (fastify, opts = {}) => {
153
+ const { maxEntries = 500, defaultTTL = 30, rules = [], exclude = [], invalidateOn = [
154
+ "POST",
155
+ "PUT",
156
+ "PATCH",
157
+ "DELETE"
158
+ ], xCacheHeader = true, statsPath = null, keyFn } = opts;
159
+ const cache = new LRUCache(maxEntries);
160
+ const invalidateMethods = new Set(invalidateOn.map((m) => m.toUpperCase()));
161
+ /** Find TTL for a given URL path (seconds) */
162
+ function getTTL(url) {
163
+ const path = url.split("?")[0];
164
+ for (const rule of rules) if (path.startsWith(rule.match)) return rule.ttl;
165
+ return defaultTTL;
166
+ }
167
+ /** Check if a URL should be excluded */
168
+ function isExcluded(url) {
169
+ return exclude.some((p) => url.startsWith(p));
170
+ }
171
+ /** Build cache key — includes user/org scope by default to prevent cross-caller leaks */
172
+ function buildKey(request) {
173
+ if (keyFn) return keyFn(request);
174
+ const user = request.user;
175
+ const userId = user?.id ?? user?._id ?? "anon";
176
+ const orgId = request.scope?.organizationId ?? "no-org";
177
+ return `${request.method}:${request.url}:u=${userId}:o=${orgId}`;
178
+ }
179
+ fastify.addHook("onRequest", async (request) => {
180
+ if (request.method !== "GET" && request.method !== "HEAD") return;
181
+ if (isExcluded(request.url)) return;
182
+ const ttl = getTTL(request.url);
183
+ if (ttl <= 0) return;
184
+ request.__arcCacheTTL = ttl;
185
+ });
186
+ fastify.addHook("onResponse", async (request, reply) => {
187
+ if (!invalidateMethods.has(request.method.toUpperCase())) return;
188
+ const statusCode = reply.statusCode;
189
+ if (statusCode < 200 || statusCode >= 300) return;
190
+ const path = request.url.split("?")[0];
191
+ const segments = path.split("/").filter(Boolean);
192
+ const lastSegment = segments[segments.length - 1];
193
+ if (segments.length >= 2 && lastSegment != null && /^[0-9a-f]{8,}$|^\d+$/.test(lastSegment)) {
194
+ const resourceRoot = "/" + segments.slice(0, -1).join("/");
195
+ cache.invalidatePrefix(resourceRoot);
196
+ cache.invalidatePrefix(path);
197
+ } else cache.invalidatePrefix(path);
198
+ });
199
+ const cacheMiddleware = async (request, reply) => {
200
+ const ttl = request.__arcCacheTTL;
201
+ if (!ttl || ttl <= 0) return;
202
+ if (request.method !== "GET" && request.method !== "HEAD") return;
203
+ const key = buildKey(request);
204
+ if (!key) return;
205
+ const entry = cache.get(key);
206
+ if (!entry) return;
207
+ if (xCacheHeader) reply.header("x-cache", "HIT");
208
+ for (const [name, value] of Object.entries(entry.headers)) reply.header(name, value);
209
+ request.__arcCacheTTL = 0;
210
+ reply.code(entry.statusCode).send(entry.body);
211
+ };
212
+ fastify.addHook("onSend", async (request, reply, payload) => {
213
+ const ttl = request.__arcCacheTTL;
214
+ if (!ttl || ttl <= 0) return payload;
215
+ if (request.method !== "GET" && request.method !== "HEAD") return payload;
216
+ const statusCode = reply.statusCode;
217
+ if (statusCode < 200 || statusCode >= 300) return payload;
218
+ const key = buildKey(request);
219
+ if (!key) return payload;
220
+ if (xCacheHeader) reply.header("x-cache", "MISS");
221
+ let body;
222
+ if (typeof payload === "string") body = payload;
223
+ else if (Buffer.isBuffer(payload)) body = payload.toString("utf-8");
224
+ else if (payload != null) body = JSON.stringify(payload);
225
+ else body = "";
226
+ const headers = {};
227
+ const contentType = reply.getHeader("content-type");
228
+ if (contentType) headers["content-type"] = String(contentType);
229
+ const etag = reply.getHeader("etag");
230
+ if (etag) headers["etag"] = String(etag);
231
+ cache.set(key, {
232
+ body,
233
+ statusCode,
234
+ headers,
235
+ createdAt: Date.now(),
236
+ ttl: ttl * 1e3
237
+ });
238
+ return payload;
239
+ });
240
+ fastify.decorate("responseCache", {
241
+ invalidate: (pathPrefix) => cache.invalidatePrefix(pathPrefix),
242
+ invalidateAll: () => cache.clear(),
243
+ stats: () => cache.getStats(maxEntries),
244
+ middleware: cacheMiddleware
245
+ });
246
+ if (statsPath) fastify.get(statsPath, async () => {
247
+ return cache.getStats(maxEntries);
248
+ });
249
+ const evtInv = opts.eventInvalidation;
250
+ if (evtInv && hasEvents(fastify)) {
251
+ const crossResourcePatterns = typeof evtInv === "object" ? evtInv.patterns ?? {} : {};
252
+ fastify.events.subscribe("*", async (event) => {
253
+ const parts = event.type.split(".");
254
+ if (parts.length !== 2) return;
255
+ const [resource, action] = parts;
256
+ if (!resource || ![
257
+ "created",
258
+ "updated",
259
+ "deleted"
260
+ ].includes(action)) return;
261
+ cache.invalidatePrefix(`/${resource}s`);
262
+ cache.invalidatePrefix(`/${resource}`);
263
+ for (const [pattern, prefixes] of Object.entries(crossResourcePatterns)) if (eventMatchesPattern(event.type, pattern)) for (const prefix of prefixes) cache.invalidatePrefix(prefix);
264
+ }).catch((err) => {
265
+ fastify.log?.warn?.({ err }, "Response cache: failed to subscribe to events for invalidation");
266
+ });
267
+ fastify.log?.debug?.("Response cache: event-driven invalidation enabled");
268
+ } else if (evtInv) fastify.log?.warn?.("Response cache: eventInvalidation enabled but eventPlugin not registered.");
269
+ fastify.log?.debug?.(`Response cache: registered (max=${maxEntries}, defaultTTL=${defaultTTL}s, rules=${rules.length})`);
270
+ };
271
+ /** Check if an event type matches a pattern (supports wildcards) */
272
+ function eventMatchesPattern(type, pattern) {
273
+ if (pattern === "*") return true;
274
+ if (pattern.endsWith(".*")) return type.startsWith(pattern.slice(0, -1));
275
+ return type === pattern;
276
+ }
277
+ const responseCachePlugin = fp(responseCachePluginImpl, {
278
+ name: "arc-response-cache",
279
+ fastify: "5.x"
280
+ });
281
+
282
+ //#endregion
283
+ export { responseCachePlugin as default, responseCachePlugin };
@@ -0,0 +1,2 @@
1
+ import { a as traced, i as isTracingAvailable, n as _default, r as createSpan, t as TracingOptions } from "../tracing-8CEbhF0w.mjs";
2
+ export { type TracingOptions, createSpan, isTracingAvailable, traced, _default as tracingPlugin };
@@ -0,0 +1,185 @@
1
+ import { createRequire } from "node:module";
2
+ import fp from "fastify-plugin";
3
+
4
+ //#region src/plugins/tracing.ts
5
+ const require = createRequire(import.meta.url);
6
+ let trace;
7
+ let context;
8
+ let SpanStatusCode;
9
+ let NodeTracerProvider;
10
+ let BatchSpanProcessor;
11
+ let OTLPTraceExporter;
12
+ let getNodeAutoInstrumentations;
13
+ let isAvailable = false;
14
+ try {
15
+ const api = require("@opentelemetry/api");
16
+ trace = api.trace;
17
+ context = api.context;
18
+ SpanStatusCode = api.SpanStatusCode;
19
+ const sdkNode = require("@opentelemetry/sdk-node");
20
+ NodeTracerProvider = sdkNode.NodeTracerProvider;
21
+ BatchSpanProcessor = sdkNode.BatchSpanProcessor;
22
+ OTLPTraceExporter = require("@opentelemetry/exporter-trace-otlp-http").OTLPTraceExporter;
23
+ require("@opentelemetry/instrumentation-http").HttpInstrumentation;
24
+ require("@opentelemetry/instrumentation-mongodb").MongoDBInstrumentation;
25
+ getNodeAutoInstrumentations = require("@opentelemetry/auto-instrumentations-node").getNodeAutoInstrumentations;
26
+ isAvailable = true;
27
+ } catch (e) {}
28
+ /**
29
+ * Create a tracer provider
30
+ */
31
+ function createTracerProvider(options) {
32
+ if (!isAvailable) return null;
33
+ const { serviceName = "@classytic/arc", exporterUrl = "http://localhost:4318/v1/traces" } = options;
34
+ const exporter = new OTLPTraceExporter({ url: exporterUrl });
35
+ const provider = new NodeTracerProvider({ resource: { attributes: {
36
+ "service.name": serviceName,
37
+ "service.version": "1.0.0"
38
+ } } });
39
+ provider.addSpanProcessor(new BatchSpanProcessor(exporter));
40
+ provider.register();
41
+ return provider;
42
+ }
43
+ /**
44
+ * OpenTelemetry Distributed Tracing Plugin
45
+ */
46
+ async function tracingPlugin(fastify, options = {}) {
47
+ const { serviceName = "@classytic/arc", autoInstrumentation = true, sampleRate = 1 } = options;
48
+ if (!isAvailable) {
49
+ fastify.log.warn("OpenTelemetry not installed. Tracing disabled.");
50
+ fastify.log.warn("Install: npm install @opentelemetry/api @opentelemetry/sdk-node");
51
+ return;
52
+ }
53
+ if (!createTracerProvider(options)) return;
54
+ if (autoInstrumentation && getNodeAutoInstrumentations) {
55
+ const instrumentations = getNodeAutoInstrumentations({
56
+ "@opentelemetry/instrumentation-http": { enabled: true },
57
+ "@opentelemetry/instrumentation-mongodb": { enabled: true }
58
+ });
59
+ for (const instrumentation of instrumentations) instrumentation.enable();
60
+ fastify.log.debug("OpenTelemetry auto-instrumentation enabled");
61
+ }
62
+ const tracer = trace.getTracer(serviceName);
63
+ fastify.decorateRequest("tracer", void 0);
64
+ fastify.addHook("onRequest", async (request, reply) => {
65
+ if (Math.random() > sampleRate) return;
66
+ const span = tracer.startSpan(`HTTP ${request.method} ${request.url}`, {
67
+ kind: 1,
68
+ attributes: {
69
+ "http.method": request.method,
70
+ "http.url": request.url,
71
+ "http.target": request.routeOptions?.url ?? request.url,
72
+ "http.host": request.hostname,
73
+ "http.scheme": request.protocol,
74
+ "http.user_agent": request.headers["user-agent"]
75
+ }
76
+ });
77
+ request.tracer = {
78
+ tracer,
79
+ currentSpan: span
80
+ };
81
+ context.with(trace.setSpan(context.active(), span), () => {});
82
+ });
83
+ fastify.addHook("onResponse", async (request, reply) => {
84
+ if (!request.tracer?.currentSpan) return;
85
+ const span = request.tracer.currentSpan;
86
+ span.setAttributes({
87
+ "http.status_code": reply.statusCode,
88
+ "http.response_content_length": reply.getHeader("content-length")
89
+ });
90
+ if (reply.statusCode >= 500) span.setStatus({
91
+ code: SpanStatusCode.ERROR,
92
+ message: `HTTP ${reply.statusCode}`
93
+ });
94
+ else span.setStatus({ code: SpanStatusCode.OK });
95
+ span.end();
96
+ });
97
+ fastify.addHook("onError", async (request, reply, error) => {
98
+ if (!request.tracer?.currentSpan) return;
99
+ const span = request.tracer.currentSpan;
100
+ span.recordException(error);
101
+ span.setStatus({
102
+ code: SpanStatusCode.ERROR,
103
+ message: error.message
104
+ });
105
+ });
106
+ fastify.log.debug({ serviceName }, "OpenTelemetry tracing enabled");
107
+ }
108
+ /**
109
+ * Utility to create custom spans in your code
110
+ *
111
+ * @example
112
+ * import { createSpan } from '@classytic/arc/plugins';
113
+ *
114
+ * async function expensiveOperation(req) {
115
+ * return createSpan(req, 'expensiveOperation', async (span) => {
116
+ * span.setAttribute('custom.attribute', 'value');
117
+ * return await doWork();
118
+ * });
119
+ * }
120
+ */
121
+ function createSpan(request, name, fn, attributes) {
122
+ if (!isAvailable || !request.tracer) return fn(null);
123
+ const { tracer, currentSpan } = request.tracer;
124
+ const span = tracer.startSpan(name, {
125
+ parent: currentSpan,
126
+ attributes: attributes || {}
127
+ }, trace.setSpan(context.active(), currentSpan));
128
+ return context.with(trace.setSpan(context.active(), span), async () => {
129
+ try {
130
+ const result = await fn(span);
131
+ span.setStatus({ code: SpanStatusCode.OK });
132
+ return result;
133
+ } catch (error) {
134
+ span.recordException(error);
135
+ span.setStatus({
136
+ code: SpanStatusCode.ERROR,
137
+ message: error.message
138
+ });
139
+ throw error;
140
+ } finally {
141
+ span.end();
142
+ }
143
+ });
144
+ }
145
+ /**
146
+ * Decorator to automatically trace repository methods
147
+ *
148
+ * @example
149
+ * class ProductRepository extends Repository {
150
+ * @traced()
151
+ * async findActive() {
152
+ * return this.findAll({ filter: { isActive: true } });
153
+ * }
154
+ * }
155
+ */
156
+ function traced(spanName) {
157
+ return function(target, propertyKey, descriptor) {
158
+ const originalMethod = descriptor.value;
159
+ descriptor.value = async function(...args) {
160
+ const request = args.find((arg) => arg && arg.tracer);
161
+ if (!request?.tracer) return originalMethod.apply(this, args);
162
+ return createSpan(request, spanName || `${target.constructor.name}.${propertyKey}`, async (span) => {
163
+ if (span) {
164
+ span.setAttribute("db.operation", propertyKey);
165
+ span.setAttribute("db.system", "mongodb");
166
+ }
167
+ return originalMethod.apply(this, args);
168
+ });
169
+ };
170
+ return descriptor;
171
+ };
172
+ }
173
+ /**
174
+ * Check if OpenTelemetry is available
175
+ */
176
+ function isTracingAvailable() {
177
+ return isAvailable;
178
+ }
179
+ var tracing_default = fp(tracingPlugin, {
180
+ name: "arc-tracing",
181
+ fastify: "5.x"
182
+ });
183
+
184
+ //#endregion
185
+ export { createSpan, isTracingAvailable, traced, tracing_default as tracingPlugin };
@@ -0,0 +1,86 @@
1
+ //#region src/cli/utils/pluralize.ts
2
+ /**
3
+ * Lightweight English pluralization for the Arc CLI.
4
+ *
5
+ * Covers the common cases developers hit when naming resources:
6
+ * company → companies
7
+ * category → categories
8
+ * status → statuses
9
+ * address → addresses
10
+ * person → people
11
+ * child → children
12
+ * bus → buses
13
+ * box → boxes
14
+ * quiz → quizzes
15
+ * leaf → leaves
16
+ * wolf → wolves
17
+ *
18
+ * No external dependencies — designed to keep the CLI install-free.
19
+ */
20
+ const IRREGULARS = {
21
+ person: "people",
22
+ child: "children",
23
+ man: "men",
24
+ woman: "women",
25
+ mouse: "mice",
26
+ goose: "geese",
27
+ tooth: "teeth",
28
+ foot: "feet",
29
+ ox: "oxen",
30
+ datum: "data",
31
+ medium: "media",
32
+ index: "indices",
33
+ matrix: "matrices",
34
+ vertex: "vertices",
35
+ criterion: "criteria"
36
+ };
37
+ const UNCOUNTABLES = new Set([
38
+ "sheep",
39
+ "fish",
40
+ "deer",
41
+ "series",
42
+ "species",
43
+ "money",
44
+ "rice",
45
+ "information",
46
+ "equipment",
47
+ "media",
48
+ "data"
49
+ ]);
50
+ /**
51
+ * Pluralize an English word.
52
+ *
53
+ * @param word - Singular noun (e.g. "company", "product", "person")
54
+ * @returns Plural form (e.g. "companies", "products", "people")
55
+ */
56
+ function pluralize(word) {
57
+ const lower = word.toLowerCase();
58
+ if (UNCOUNTABLES.has(lower)) return word;
59
+ if (IRREGULARS[lower]) {
60
+ const plural = IRREGULARS[lower];
61
+ return word[0] === word[0].toUpperCase() ? plural.charAt(0).toUpperCase() + plural.slice(1) : plural;
62
+ }
63
+ if (lower.endsWith("fe")) return word.slice(0, -2) + "ves";
64
+ if (lower.endsWith("f") && !lower.endsWith("ff") && !lower.endsWith("roof") && !lower.endsWith("chief") && !lower.endsWith("belief")) return word.slice(0, -1) + "ves";
65
+ if (lower.endsWith("y") && !/[aeiou]y$/i.test(lower)) return word.slice(0, -1) + "ies";
66
+ if (lower.endsWith("is")) return word.slice(0, -2) + "es";
67
+ if (new Set([
68
+ "cactus",
69
+ "stimulus",
70
+ "focus",
71
+ "fungus",
72
+ "nucleus",
73
+ "syllabus",
74
+ "radius",
75
+ "alumnus",
76
+ "terminus",
77
+ "bacillus"
78
+ ]).has(lower)) return word.slice(0, -2) + "i";
79
+ if (lower.endsWith("z") && !lower.endsWith("zz")) return word + "zes";
80
+ if (/(?:s|sh|ch|x|zz)$/i.test(lower)) return word + "es";
81
+ if (lower.endsWith("o") && !/[aeiou]o$/i.test(lower)) return word + "es";
82
+ return word + "s";
83
+ }
84
+
85
+ //#endregion
86
+ export { pluralize as t };