@appweaver/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +7 -0
  3. package/app/application.d.ts +43 -0
  4. package/app/application.js +112 -0
  5. package/app/create-app.d.ts +32 -0
  6. package/app/create-app.js +38 -0
  7. package/app/index.d.ts +2 -0
  8. package/app/index.js +18 -0
  9. package/app/lifecycle-manager.d.ts +20 -0
  10. package/app/lifecycle-manager.js +42 -0
  11. package/app/load-modules.d.ts +10 -0
  12. package/app/load-modules.js +50 -0
  13. package/app/load-providers.d.ts +8 -0
  14. package/app/load-providers.js +33 -0
  15. package/cache/cache-service.d.ts +72 -0
  16. package/cache/cache-service.js +202 -0
  17. package/cache/cache.d.ts +12 -0
  18. package/cache/cache.js +184 -0
  19. package/cache/caching.d.ts +8 -0
  20. package/cache/caching.js +72 -0
  21. package/cache/eviction/eviction-index.d.ts +35 -0
  22. package/cache/eviction/eviction-index.js +2 -0
  23. package/cache/eviction/fifo-eviction-index.d.ts +7 -0
  24. package/cache/eviction/fifo-eviction-index.js +28 -0
  25. package/cache/eviction/index.d.ts +4 -0
  26. package/cache/eviction/index.js +20 -0
  27. package/cache/eviction/lfu-eviction-index.d.ts +8 -0
  28. package/cache/eviction/lfu-eviction-index.js +82 -0
  29. package/cache/eviction/lru-eviction-index.d.ts +8 -0
  30. package/cache/eviction/lru-eviction-index.js +81 -0
  31. package/cache/index.d.ts +2 -0
  32. package/cache/index.js +18 -0
  33. package/cache/memory-cache.d.ts +4 -0
  34. package/cache/memory-cache.js +12 -0
  35. package/cache/redis-cache.d.ts +4 -0
  36. package/cache/redis-cache.js +12 -0
  37. package/context/context.d.ts +2 -0
  38. package/context/context.js +13 -0
  39. package/context/dependency-injection.d.ts +98 -0
  40. package/context/dependency-injection.js +276 -0
  41. package/context/index.d.ts +2 -0
  42. package/context/index.js +18 -0
  43. package/database/create-client.d.ts +11 -0
  44. package/database/create-client.js +142 -0
  45. package/database/index.d.ts +1 -0
  46. package/database/index.js +17 -0
  47. package/database/prisma-database.d.ts +15 -0
  48. package/database/prisma-database.js +53 -0
  49. package/errors/error-handler.d.ts +3 -0
  50. package/errors/error-handler.js +24 -0
  51. package/errors/error-schema.d.ts +32 -0
  52. package/errors/error-schema.js +31 -0
  53. package/errors/http-error.d.ts +21 -0
  54. package/errors/http-error.js +33 -0
  55. package/errors/index.d.ts +3 -0
  56. package/errors/index.js +19 -0
  57. package/events/node-events.d.ts +7 -0
  58. package/events/node-events.js +39 -0
  59. package/export/export-service.d.ts +19 -0
  60. package/export/export-service.js +154 -0
  61. package/export/index.d.ts +1 -0
  62. package/export/index.js +17 -0
  63. package/factory/create-model.d.ts +2 -0
  64. package/factory/create-model.js +366 -0
  65. package/factory/create-policy.d.ts +2 -0
  66. package/factory/create-policy.js +16 -0
  67. package/factory/create-routes.d.ts +2 -0
  68. package/factory/create-routes.js +21 -0
  69. package/factory/create-service.d.ts +3 -0
  70. package/factory/create-service.js +97 -0
  71. package/factory/index.d.ts +4 -0
  72. package/factory/index.js +20 -0
  73. package/health/health-routes.d.ts +2 -0
  74. package/health/health-routes.js +36 -0
  75. package/health/health-schema.d.ts +45 -0
  76. package/health/health-schema.js +52 -0
  77. package/health/health-service.d.ts +38 -0
  78. package/health/health-service.js +82 -0
  79. package/health/index.d.ts +2 -0
  80. package/health/index.js +18 -0
  81. package/index.d.ts +15 -0
  82. package/index.js +31 -0
  83. package/mailer/email-service.d.ts +36 -0
  84. package/mailer/email-service.js +89 -0
  85. package/mailer/index.d.ts +1 -0
  86. package/mailer/index.js +17 -0
  87. package/mailer/json-mailer.d.ts +7 -0
  88. package/mailer/json-mailer.js +32 -0
  89. package/mailer/smtp-mailer.d.ts +7 -0
  90. package/mailer/smtp-mailer.js +43 -0
  91. package/memory/in-memory.d.ts +24 -0
  92. package/memory/in-memory.js +164 -0
  93. package/memory/redis.d.ts +26 -0
  94. package/memory/redis.js +158 -0
  95. package/package.json +68 -0
  96. package/prisma/client/browser.d.ts +34 -0
  97. package/prisma/client/browser.js +56 -0
  98. package/prisma/client/client.d.ts +53 -0
  99. package/prisma/client/client.js +70 -0
  100. package/prisma/client/commonInputTypes.d.ts +395 -0
  101. package/prisma/client/commonInputTypes.js +11 -0
  102. package/prisma/client/enums.d.ts +1 -0
  103. package/prisma/client/enums.js +11 -0
  104. package/prisma/client/internal/class.d.ts +187 -0
  105. package/prisma/client/internal/class.js +85 -0
  106. package/prisma/client/internal/prismaNamespace.d.ts +987 -0
  107. package/prisma/client/internal/prismaNamespace.js +193 -0
  108. package/prisma/client/internal/prismaNamespaceBrowser.d.ts +125 -0
  109. package/prisma/client/internal/prismaNamespaceBrowser.js +164 -0
  110. package/prisma/client/models/ApiKey.d.ts +1167 -0
  111. package/prisma/client/models/ApiKey.js +2 -0
  112. package/prisma/client/models/File.d.ts +1248 -0
  113. package/prisma/client/models/File.js +2 -0
  114. package/prisma/client/models/OneTimeToken.d.ts +1083 -0
  115. package/prisma/client/models/OneTimeToken.js +2 -0
  116. package/prisma/client/models/Permission.d.ts +1209 -0
  117. package/prisma/client/models/Permission.js +2 -0
  118. package/prisma/client/models/Role.d.ts +1209 -0
  119. package/prisma/client/models/Role.js +2 -0
  120. package/prisma/client/models/Seeder.d.ts +1023 -0
  121. package/prisma/client/models/Seeder.js +2 -0
  122. package/prisma/client/models.d.ts +7 -0
  123. package/prisma/client/models.js +2 -0
  124. package/queue/bull-queue.d.ts +37 -0
  125. package/queue/bull-queue.js +207 -0
  126. package/queue/memory-queue.d.ts +54 -0
  127. package/queue/memory-queue.js +283 -0
  128. package/resource/index.d.ts +4 -0
  129. package/resource/index.js +20 -0
  130. package/resource/resource-loader.d.ts +26 -0
  131. package/resource/resource-loader.js +161 -0
  132. package/resource/resource-routes.d.ts +3 -0
  133. package/resource/resource-routes.js +174 -0
  134. package/resource/resource-schema.d.ts +33 -0
  135. package/resource/resource-schema.js +186 -0
  136. package/resource/resource-service.d.ts +89 -0
  137. package/resource/resource-service.js +772 -0
  138. package/resources.d.ts +9 -0
  139. package/resources.js +24 -0
  140. package/scheduler/cron-scheduler.d.ts +12 -0
  141. package/scheduler/cron-scheduler.js +73 -0
  142. package/security/account/account-routes.d.ts +2 -0
  143. package/security/account/account-routes.js +104 -0
  144. package/security/account/account-schema.d.ts +290 -0
  145. package/security/account/account-schema.js +166 -0
  146. package/security/account/account-service.d.ts +88 -0
  147. package/security/account/account-service.js +218 -0
  148. package/security/account/index.d.ts +3 -0
  149. package/security/account/index.js +19 -0
  150. package/security/api-key/api-key-auth.d.ts +4 -0
  151. package/security/api-key/api-key-auth.js +66 -0
  152. package/security/api-key/index.d.ts +1 -0
  153. package/security/api-key/index.js +17 -0
  154. package/security/auth-routes.d.ts +2 -0
  155. package/security/auth-routes.js +80 -0
  156. package/security/auth-schema.d.ts +215 -0
  157. package/security/auth-schema.js +129 -0
  158. package/security/auth-service.d.ts +117 -0
  159. package/security/auth-service.js +272 -0
  160. package/security/auth.d.ts +3 -0
  161. package/security/auth.js +127 -0
  162. package/security/basic/basic-auth.d.ts +4 -0
  163. package/security/basic/basic-auth.js +32 -0
  164. package/security/basic/index.d.ts +1 -0
  165. package/security/basic/index.js +17 -0
  166. package/security/create-auth-resources.d.ts +6 -0
  167. package/security/create-auth-resources.js +129 -0
  168. package/security/helper.d.ts +127 -0
  169. package/security/helper.js +329 -0
  170. package/security/index.d.ts +4 -0
  171. package/security/index.js +20 -0
  172. package/security/jwt/index.d.ts +2 -0
  173. package/security/jwt/index.js +18 -0
  174. package/security/jwt/jwt-auth.d.ts +4 -0
  175. package/security/jwt/jwt-auth.js +57 -0
  176. package/security/jwt/jwt-keys.d.ts +7 -0
  177. package/security/jwt/jwt-keys.js +43 -0
  178. package/security/oauth2/create-oauth2-plugin.d.ts +17 -0
  179. package/security/oauth2/create-oauth2-plugin.js +85 -0
  180. package/security/oauth2/index.d.ts +3 -0
  181. package/security/oauth2/index.js +19 -0
  182. package/security/oauth2/oauth2-custom.d.ts +7 -0
  183. package/security/oauth2/oauth2-custom.js +40 -0
  184. package/security/oauth2/oauth2-facebook.d.ts +1 -0
  185. package/security/oauth2/oauth2-facebook.js +30 -0
  186. package/security/oauth2/oauth2-google.d.ts +1 -0
  187. package/security/oauth2/oauth2-google.js +28 -0
  188. package/security/oauth2/oauth2-schema.d.ts +10 -0
  189. package/security/oauth2/oauth2-schema.js +53 -0
  190. package/security/recaptcha/index.d.ts +2 -0
  191. package/security/recaptcha/index.js +18 -0
  192. package/security/recaptcha/recaptcha-verify.d.ts +1 -0
  193. package/security/recaptcha/recaptcha-verify.js +33 -0
  194. package/security/recaptcha/recaptcha.d.ts +2 -0
  195. package/security/recaptcha/recaptcha.js +22 -0
  196. package/security/resources/api-key/model.d.ts +2 -0
  197. package/security/resources/api-key/model.js +63 -0
  198. package/security/resources/api-key/policy.d.ts +2 -0
  199. package/security/resources/api-key/policy.js +26 -0
  200. package/security/resources/api-key/routes.d.ts +2 -0
  201. package/security/resources/api-key/routes.js +15 -0
  202. package/security/resources/api-key/service.d.ts +3 -0
  203. package/security/resources/api-key/service.js +33 -0
  204. package/security/resources/one-time-token/model.d.ts +2 -0
  205. package/security/resources/one-time-token/model.js +32 -0
  206. package/security/resources/permission/model.d.ts +2 -0
  207. package/security/resources/permission/model.js +16 -0
  208. package/security/resources/role/model.d.ts +2 -0
  209. package/security/resources/role/model.js +22 -0
  210. package/security/store/database-security-store.d.ts +5 -0
  211. package/security/store/database-security-store.js +51 -0
  212. package/security/store/redis-security-store.d.ts +5 -0
  213. package/security/store/redis-security-store.js +32 -0
  214. package/seeder/create-seeder.d.ts +21 -0
  215. package/seeder/create-seeder.js +17 -0
  216. package/seeder/index.d.ts +2 -0
  217. package/seeder/index.js +18 -0
  218. package/seeder/resources/seeder/model.d.ts +2 -0
  219. package/seeder/resources/seeder/model.js +38 -0
  220. package/seeder/seeder.d.ts +20 -0
  221. package/seeder/seeder.js +175 -0
  222. package/server/create-server.d.ts +20 -0
  223. package/server/create-server.js +160 -0
  224. package/server/index.d.ts +4 -0
  225. package/server/index.js +20 -0
  226. package/server/info-route.d.ts +2 -0
  227. package/server/info-route.js +18 -0
  228. package/server/info-schema.d.ts +37 -0
  229. package/server/info-schema.js +19 -0
  230. package/server/register-model.d.ts +2 -0
  231. package/server/register-model.js +12 -0
  232. package/server/register-plugin.d.ts +2 -0
  233. package/server/register-plugin.js +13 -0
  234. package/server/register-route.d.ts +3 -0
  235. package/server/register-route.js +74 -0
  236. package/server/schemas.d.ts +3 -0
  237. package/server/schemas.js +30 -0
  238. package/server/swagger.d.ts +3 -0
  239. package/server/swagger.js +122 -0
  240. package/storage/file-route.d.ts +2 -0
  241. package/storage/file-route.js +39 -0
  242. package/storage/file-schema.d.ts +9 -0
  243. package/storage/file-schema.js +42 -0
  244. package/storage/file-service.d.ts +81 -0
  245. package/storage/file-service.js +384 -0
  246. package/storage/filesystem-storage.d.ts +10 -0
  247. package/storage/filesystem-storage.js +128 -0
  248. package/storage/index.d.ts +2 -0
  249. package/storage/index.js +18 -0
  250. package/storage/resources/file/model.d.ts +2 -0
  251. package/storage/resources/file/model.js +75 -0
  252. package/types/application-context.d.ts +20 -0
  253. package/types/application-context.js +2 -0
  254. package/types/auth.d.ts +31 -0
  255. package/types/auth.js +2 -0
  256. package/types/fastify-extensions.d.ts +24 -0
  257. package/types/fastify-extensions.js +4 -0
  258. package/types/generated.d.ts +193 -0
  259. package/types/generated.js +3 -0
  260. package/types/index.d.ts +4 -0
  261. package/types/index.js +20 -0
  262. package/types/server.d.ts +4 -0
  263. package/types/server.js +2 -0
  264. package/utils/export-util.d.ts +8 -0
  265. package/utils/export-util.js +25 -0
  266. package/utils/file-util.d.ts +64 -0
  267. package/utils/file-util.js +141 -0
  268. package/utils/index.d.ts +3 -0
  269. package/utils/index.js +19 -0
  270. package/utils/schema-util.d.ts +20 -0
  271. package/utils/schema-util.js +37 -0
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CacheService = void 0;
4
+ const common_1 = require("@appweaver/common");
5
+ const flatted_1 = require("flatted");
6
+ const context_1 = require("../context");
7
+ class CacheService {
8
+ /** @internal */
9
+ _cache = (0, context_1.inject)(common_1.Cache);
10
+ /**
11
+ * Retrieves the cache instance associated with this service.
12
+ *
13
+ * @return {Cache} The current cache instance.
14
+ */
15
+ get cache() {
16
+ return this._cache;
17
+ }
18
+ /**
19
+ * Retrieves a cached value associated with the specified key.
20
+ *
21
+ * @param {string} key - The key used to look up the cached value.
22
+ * @return A promise that resolves to the cached value if the key exists, or null if the key is not found.
23
+ */
24
+ async getCachedValue(key) {
25
+ const exists = await this._cache.has(key);
26
+ if (exists) {
27
+ common_1.logger.debug({ key }, 'Retrieved value from cache');
28
+ return this._cache.get(key);
29
+ }
30
+ return null;
31
+ }
32
+ /**
33
+ * Adds a key-value pair to the cache if the key does not already exist.
34
+ * Optionally allows specifying a time-to-live (TTL) for the cached item.
35
+ *
36
+ * @param {string} key - The unique key to identify the value in the cache.
37
+ * @param {Object} value - The value to store in the cache.
38
+ * @param {number} [ttl] - Optional time-to-live for the cache entry in seconds.
39
+ * @param {boolean} [replace=false] - Optional flag that indicates if the existing cache entry should be replaced
40
+ * with the new value and ttl.
41
+ * @return {Promise<boolean>} A promise that resolves to true if the key-value pair was added, or false if the key
42
+ * already exists.
43
+ */
44
+ async addToCache(key, value, ttl, replace = false) {
45
+ const exists = await this._cache.has(key);
46
+ if (exists && !replace) {
47
+ return false;
48
+ }
49
+ common_1.logger.debug({ key, ttl, replace }, 'Added value to cache');
50
+ return this._cache.set(key, value, ttl);
51
+ }
52
+ /**
53
+ * Removes a cached value associated with the specified key from the cache store.
54
+ *
55
+ * @param {string} key - The key associated with the cached value to be removed.
56
+ * @return {Promise<boolean>} A promise that resolves to `true` if the value was successfully removed, or
57
+ * `false` otherwise.
58
+ */
59
+ async removeCachedValue(key) {
60
+ const removed = await this._cache.evict(key);
61
+ if (removed) {
62
+ common_1.logger.debug({ key }, 'Removed value from cache');
63
+ }
64
+ return removed;
65
+ }
66
+ /**
67
+ * Invalidates the cache for the specified model and action type based on the configured cache invalidation strategy.
68
+ *
69
+ * @param {string} modelName - The name of the model whose cache needs to be invalidated.
70
+ * @param {Extract<ActionType, 'create' | 'update' | 'delete' | 'uploadFiles' | 'deleteFiles'>} action - The action type
71
+ * that triggered the cache invalidation (e.g., 'create', 'update', 'delete', 'uploadFiles', or 'fileDelete').
72
+ * @return {Promise<void>} Resolves when the cache invalidation process is completed.
73
+ */
74
+ async invalidateCache(modelName, action) {
75
+ if (!common_1.config.CACHE_ENABLED) {
76
+ return;
77
+ }
78
+ const strategy = common_1.config.CACHE_INVALIDATION_STRATEGY;
79
+ common_1.logger.debug({ modelName, action, strategy }, `Invalidating resource cache`);
80
+ let invalidation;
81
+ switch (strategy) {
82
+ case common_1.CacheInvalidationStrategy.ExpireRelated:
83
+ invalidation = this._cache.expire(`*!${modelName}!*:inv`);
84
+ break;
85
+ case common_1.CacheInvalidationStrategy.ExpireAll:
86
+ invalidation = this._cache.expire(`*:inv`);
87
+ break;
88
+ }
89
+ if (invalidation) {
90
+ if (common_1.config.CACHE_INVALIDATION_DEFERRED) {
91
+ void invalidation.catch((error) => {
92
+ common_1.logger.error(error, 'Cache invalidation error');
93
+ });
94
+ }
95
+ else {
96
+ try {
97
+ await invalidation;
98
+ }
99
+ catch (error) {
100
+ common_1.logger.error(error, 'Cache invalidation error');
101
+ }
102
+ }
103
+ }
104
+ }
105
+ /**
106
+ * Builds a unique cache key based on the provided input parameters. The key is constructed
107
+ * using properties such as the base key, HTTP method, URL, request body, model name, and any
108
+ * associated relations. It also includes optional user-specific data and cache invalidation settings.
109
+ *
110
+ * @param {Object} data Input data for constructing the cache key.
111
+ * @param {string} data.baseKey The base key that serves as the starting point of the cache key.
112
+ * @param {string} [data.method] The HTTP method (e.g., GET, POST) associated with the key.
113
+ * @param {string} [data.url] The URL associated with the request.
114
+ * @param {string} [data.body] The stringified request body, which will be hashed if present.
115
+ * @param {string} [data.modelName] The name of the model to associate with the cache key.
116
+ * @param {string[]} [data.relations] A list of related model names to associate with the cache key.
117
+ * @param {boolean} [data.skipInvalidation] Whether to skip invalidation logic for this cache key.
118
+ * @param {AuthUser} [data.authUser] The authenticated user information, if applicable, to create user-specific keys.
119
+ * @return {string} The generated cache key string.
120
+ */
121
+ buildCacheKey(data) {
122
+ const userPrefix = data.authUser ? data.authUser.id : '';
123
+ const bodyHash = data.body ? (0, common_1.makeHash)((0, flatted_1.stringify)(data.body)) : '';
124
+ // Extract model relations based on route cache config so they can be used to
125
+ // evict cache entries when related data changes
126
+ const allRelations = [];
127
+ if (data.relations?.[0] === '*' || data.modelName) {
128
+ if (data.modelName) {
129
+ // Add all relations from a specified model
130
+ const model = (0, context_1.injectModel)(data.modelName, false);
131
+ if (model) {
132
+ allRelations.push(...this.collectAllRelations(model));
133
+ }
134
+ }
135
+ else {
136
+ // Add all defined models as relations
137
+ for (const model of context_1.context.resource.models.values()) {
138
+ for (const relation of Object.values(model.config.relations ?? {})) {
139
+ allRelations.push(relation.model);
140
+ }
141
+ }
142
+ }
143
+ }
144
+ else if (data.relations && data.relations.length > 1) {
145
+ allRelations.push(...data.relations);
146
+ }
147
+ // Create a list of unique relation names
148
+ const relations = [...new Set(allRelations)];
149
+ const relationList = relations.length > 0 ? `!${relations.join('!')}!` : '';
150
+ const invalidateSuffix = data.skipInvalidation ? '' : 'inv';
151
+ return [
152
+ data.baseKey,
153
+ userPrefix,
154
+ data.method,
155
+ data.url,
156
+ bodyHash,
157
+ relationList,
158
+ invalidateSuffix
159
+ ]
160
+ .filter((v) => !!v)
161
+ .join(':');
162
+ }
163
+ /** @internal */
164
+ collectAllRelations(model) {
165
+ const relations = [];
166
+ // Add the model itself as a relation
167
+ relations.push(model.name);
168
+ // Traverse top-level relations (using their own output config)
169
+ for (const relation of Object.values(model.config.relations ?? {})) {
170
+ const outputType = relation.output?.type;
171
+ if (outputType === 'none') {
172
+ continue;
173
+ }
174
+ relations.push(relation.model);
175
+ // Traverse nested includes (if any)
176
+ if (relation.output?.include) {
177
+ this.traverseInclude(relation.output.include, model, relations);
178
+ }
179
+ }
180
+ return relations;
181
+ }
182
+ /** @internal */
183
+ traverseInclude(include, model, acc) {
184
+ const relations = model.config.relations ?? {};
185
+ for (const [relKey, includeConfig] of Object.entries(include)) {
186
+ const type = includeConfig.type;
187
+ if (type === 'none') {
188
+ continue;
189
+ }
190
+ const relationDef = relations[relKey];
191
+ if (!relationDef) {
192
+ continue;
193
+ }
194
+ acc.push(relationDef.model);
195
+ // If this include has nested includes, recurse
196
+ if (includeConfig.include) {
197
+ this.traverseInclude(includeConfig.include, model, acc);
198
+ }
199
+ }
200
+ }
201
+ }
202
+ exports.CacheService = CacheService;
@@ -0,0 +1,12 @@
1
+ import { Cache as CommonCache, Memory } from '@appweaver/common';
2
+ export declare abstract class Cache extends CommonCache {
3
+ private readonly _memory;
4
+ protected constructor(_memory: Memory);
5
+ onInit(): Promise<void>;
6
+ get<T>(key: string): Promise<T | null>;
7
+ set(key: string, value: any, ttl?: number): Promise<boolean>;
8
+ has(key: string): Promise<boolean>;
9
+ evict(key: string): Promise<boolean>;
10
+ expire(pattern?: string): Promise<number>;
11
+ keys(pattern?: string): Promise<string[]>;
12
+ }
package/cache/cache.js ADDED
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Cache = void 0;
4
+ const common_1 = require("@appweaver/common");
5
+ const eviction_1 = require("./eviction");
6
+ class Cache extends common_1.Cache {
7
+ _memory;
8
+ /** @internal */
9
+ _entryMeta = new Map();
10
+ /** @internal */
11
+ _evictionIndex = this.createEvictionIndex();
12
+ /** @internal */
13
+ _maxSizeBytes = (0, common_1.textToBytes)(common_1.config.CACHE_MAX_SIZE);
14
+ constructor(_memory) {
15
+ super();
16
+ this._memory = _memory;
17
+ }
18
+ async onInit() {
19
+ if (common_1.config.CACHE_ENABLED && common_1.config.CACHE_CLEAN_START) {
20
+ await this.expire();
21
+ }
22
+ }
23
+ async get(key) {
24
+ const prefixedKey = this.addPrefix(key);
25
+ const data = await this._memory.getValue(prefixedKey);
26
+ if (data) {
27
+ const keyMeta = this._entryMeta.get(prefixedKey);
28
+ const now = Date.now();
29
+ const count = keyMeta?.usedCount ?? 0;
30
+ const updatedMeta = {
31
+ ...(keyMeta || {
32
+ key,
33
+ createdAt: now,
34
+ sizeBytes: await this._memory.valueSizeBytes(prefixedKey)
35
+ }),
36
+ usedCount: count + 1,
37
+ lastUsedAt: now
38
+ };
39
+ this._entryMeta.set(prefixedKey, updatedMeta);
40
+ this._evictionIndex.touch(prefixedKey, updatedMeta);
41
+ }
42
+ else if (prefixedKey in this._entryMeta) {
43
+ // Remove key from meta and index storage if it does not exist memory
44
+ this._entryMeta.delete(prefixedKey);
45
+ this._evictionIndex.remove(prefixedKey);
46
+ }
47
+ return data;
48
+ }
49
+ async set(key, value, ttl) {
50
+ const prefixedKey = this.addPrefix(key);
51
+ const expireMs = ttl === 0 ? undefined : (ttl ?? common_1.config.CACHE_DEFAULT_TTL);
52
+ await this.evictExcessEntries();
53
+ const result = await this._memory.putValue(prefixedKey, value, expireMs);
54
+ const sizeBytes = await this._memory.valueSizeBytes(prefixedKey);
55
+ const now = Date.now();
56
+ const meta = {
57
+ key,
58
+ sizeBytes,
59
+ usedCount: 0,
60
+ createdAt: now,
61
+ lastUsedAt: now,
62
+ expiresAt: expireMs ? now + expireMs : undefined
63
+ };
64
+ this._entryMeta.set(prefixedKey, meta);
65
+ this._evictionIndex.add(prefixedKey, meta);
66
+ await this.ensureMemoryLimit();
67
+ return result;
68
+ }
69
+ async has(key) {
70
+ return this._memory.hasKey(this.addPrefix(key));
71
+ }
72
+ async evict(key) {
73
+ const prefixedKey = this.addPrefix(key);
74
+ const result = await this._memory.removeValue(prefixedKey);
75
+ this._entryMeta.delete(prefixedKey);
76
+ this._evictionIndex.remove(prefixedKey);
77
+ return result;
78
+ }
79
+ async expire(pattern = '*') {
80
+ const prefixedPattern = this.addPrefix(pattern);
81
+ const prefixedKeys = await this._memory.findKeys(prefixedPattern);
82
+ await this._memory.removeEntries(prefixedPattern);
83
+ for (const prefixedKey of prefixedKeys) {
84
+ this._entryMeta.delete(prefixedKey);
85
+ this._evictionIndex.remove(prefixedKey);
86
+ }
87
+ return prefixedKeys.size;
88
+ }
89
+ async keys(pattern = '*') {
90
+ const prefixedPattern = this.addPrefix(pattern);
91
+ const prefixedKeys = await this._memory.findKeys(prefixedPattern);
92
+ return Array.from(prefixedKeys);
93
+ }
94
+ /** @internal */
95
+ async evictExcessEntries() {
96
+ const now = Date.now();
97
+ // Delete expired entries metadata first, underlying memory implementation
98
+ // should have already removed cached values
99
+ for (const [prefixedKey, meta] of this._entryMeta.entries()) {
100
+ if (meta.expiresAt && meta.expiresAt < now) {
101
+ this._entryMeta.delete(prefixedKey);
102
+ this._evictionIndex.remove(prefixedKey);
103
+ }
104
+ }
105
+ // Calculate the number of entries that should be evicted, plus one is added
106
+ // because this method is called before adding new cache entry
107
+ const excessItemsCount = this._entryMeta.size - common_1.config.CACHE_MAX_ITEMS + 1;
108
+ if (excessItemsCount <= 0) {
109
+ return;
110
+ }
111
+ const candidateKeys = this._evictionIndex.evictionCandidates(excessItemsCount, now, common_1.config.CACHE_EVICTION_GRACE_PERIOD);
112
+ const evictActions = candidateKeys.map((key) => this.evict(key));
113
+ if (common_1.config.CACHE_EVICTION_DEFERRED) {
114
+ Promise.all(evictActions).catch((error) => {
115
+ common_1.logger.error(error, 'Cache eviction error');
116
+ });
117
+ }
118
+ else {
119
+ await Promise.all(evictActions);
120
+ }
121
+ }
122
+ /** @internal */
123
+ async ensureMemoryLimit() {
124
+ if (!this._maxSizeBytes) {
125
+ return;
126
+ }
127
+ if (common_1.config.CACHE_EVICTION_DEFERRED) {
128
+ this.runEnsureMemoryLimit().catch((error) => {
129
+ common_1.logger.error(error, 'Cache memory limit eviction error');
130
+ });
131
+ return;
132
+ }
133
+ await this.runEnsureMemoryLimit();
134
+ }
135
+ /** @internal */
136
+ async runEnsureMemoryLimit() {
137
+ if (!this._maxSizeBytes) {
138
+ return;
139
+ }
140
+ const now = Date.now();
141
+ let currentSizeBytes = [...this._entryMeta.values()]
142
+ .map((e) => e.sizeBytes ?? 0)
143
+ .reduce((a, b) => a + b, 0);
144
+ // Evict candidates incrementally until memory usage falls within the limit
145
+ while (currentSizeBytes > this._maxSizeBytes) {
146
+ const candidateKeys = this._evictionIndex.evictionCandidates(10, now, common_1.config.CACHE_EVICTION_GRACE_PERIOD);
147
+ if (candidateKeys.length === 0) {
148
+ break;
149
+ }
150
+ const keysToEvict = [];
151
+ for (const candidateKey of candidateKeys) {
152
+ keysToEvict.push(candidateKey);
153
+ currentSizeBytes -= this._entryMeta.get(candidateKey)?.sizeBytes ?? 0;
154
+ // Skip eviction of the rest of candidates when memory usage falls
155
+ // within the configured limit
156
+ if (currentSizeBytes <= this._maxSizeBytes) {
157
+ break;
158
+ }
159
+ }
160
+ const evictActions = keysToEvict.map((key) => this.evict(key));
161
+ await Promise.all(evictActions);
162
+ }
163
+ }
164
+ /** @internal */
165
+ addPrefix(key) {
166
+ const prefix = common_1.config.CACHE_KEY_PREFIX;
167
+ if (key.startsWith(prefix)) {
168
+ return key;
169
+ }
170
+ return `${prefix}${key}`;
171
+ }
172
+ /** @internal */
173
+ createEvictionIndex() {
174
+ switch (common_1.config.CACHE_EVICTION_STRATEGY) {
175
+ case common_1.CacheEvictionStrategy.LRU:
176
+ return new eviction_1.LruEvictionIndex();
177
+ case common_1.CacheEvictionStrategy.LFU:
178
+ return new eviction_1.LfuEvictionIndex();
179
+ case common_1.CacheEvictionStrategy.FIFO:
180
+ return new eviction_1.FifoEvictionIndex();
181
+ }
182
+ }
183
+ }
184
+ exports.Cache = Cache;
@@ -0,0 +1,8 @@
1
+ import { Server } from '../types';
2
+ export type CacheEntry = {
3
+ statusCode: number;
4
+ headers: Record<string, string | string[]>;
5
+ payload: string;
6
+ };
7
+ declare const _default: (server: Server) => void;
8
+ export default _default;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
7
+ const request_context_1 = require("@fastify/request-context");
8
+ const common_1 = require("@appweaver/common");
9
+ const cache_service_1 = require("./cache-service");
10
+ const context_1 = require("../context");
11
+ exports.default = (0, fastify_plugin_1.default)((server) => {
12
+ const { currentUser } = server;
13
+ const cacheService = (0, context_1.inject)(cache_service_1.CacheService);
14
+ server.addHook('preHandler', async (request, reply) => {
15
+ const config = getRouteConfig(request);
16
+ if (!shouldUseCache(request, config)) {
17
+ return;
18
+ }
19
+ const user = config.public ? undefined : currentUser();
20
+ const key = buildCacheKey(request, config, cacheService, user);
21
+ const entry = await cacheService.getCachedValue(key);
22
+ if (entry) {
23
+ request_context_1.requestContext.set('cached', true);
24
+ return reply
25
+ .code(entry.statusCode)
26
+ .headers(entry.headers)
27
+ .send(entry.payload);
28
+ }
29
+ });
30
+ server.addHook('onSend', async (request, reply, payload) => {
31
+ const config = getRouteConfig(request);
32
+ if (!shouldUseCache(request, config)) {
33
+ return payload;
34
+ }
35
+ if (reply.statusCode >= 200 &&
36
+ reply.statusCode < 300 &&
37
+ !request_context_1.requestContext.get('cached')) {
38
+ const user = config.public ? undefined : currentUser();
39
+ const key = buildCacheKey(request, config, cacheService, user);
40
+ await cacheService.addToCache(key, {
41
+ statusCode: reply.statusCode,
42
+ headers: reply.getHeaders(),
43
+ payload
44
+ }, config.cacheTTL);
45
+ }
46
+ return payload;
47
+ });
48
+ });
49
+ function getRouteConfig(req) {
50
+ return req.routeOptions.config;
51
+ }
52
+ function shouldUseCache(req, config) {
53
+ return !!((req.method === 'GET' || req.method === 'POST') &&
54
+ (config.cacheTTL || config.cacheKey || config.cache) &&
55
+ (config.cacheTTL === undefined || config.cacheTTL > 0) &&
56
+ config.cache !== false);
57
+ }
58
+ function buildCacheKey(req, config, cacheService, user) {
59
+ return (0, common_1.isFunction)(config.cacheKey)
60
+ ? config.cacheKey(req, user)
61
+ : config.cacheKey ||
62
+ cacheService.buildCacheKey({
63
+ baseKey: 'req',
64
+ method: req.method,
65
+ url: req.raw.url,
66
+ body: req.body,
67
+ modelName: config.cacheModelName,
68
+ relations: config.cacheRelations,
69
+ skipInvalidation: config.cacheSkipInvalidation,
70
+ authUser: user
71
+ });
72
+ }
@@ -0,0 +1,35 @@
1
+ import { CacheEntryMeta } from '@appweaver/common';
2
+ /**
3
+ * Interface representing operations for managing eviction policies in a cache system.
4
+ */
5
+ export interface EvictionIndex {
6
+ /**
7
+ * Called when a key-value pair is added to the cache with associated metadata.
8
+ *
9
+ * @param {string} key - The unique identifier for the cache entry.
10
+ * @param {CacheEntryMeta} meta - The metadata associated with the cache entry.
11
+ */
12
+ add(key: string, meta: CacheEntryMeta): void;
13
+ /**
14
+ * Called when a cache entry is removed from the cache by the specified key.
15
+ *
16
+ * @param {string} key - The unique identifier for the removed entry.
17
+ */
18
+ remove(key: string): void;
19
+ /**
20
+ * Updates the metadata of a cached entry identified by the provided key.
21
+ *
22
+ * @param {string} key - The unique identifier for the accessed cache entry.
23
+ * @param {CacheEntryMeta} meta - The metadata object containing updated information for the cache entry.
24
+ */
25
+ touch(key: string, meta: CacheEntryMeta): void;
26
+ /**
27
+ * Returns a list of cache keys that meet the eviction criteria.
28
+ *
29
+ * @param {number} count - The maximum number of candidates to evict.
30
+ * @param {number} now - The current timestamp used to determine eligibility for eviction.
31
+ * @param {number} gracePeriod - The allowed time period after which candidates are eligible for eviction.
32
+ * @return {string[]} A list of keys for the cache entries to evict.
33
+ */
34
+ evictionCandidates(count: number, now: number, gracePeriod: number): string[];
35
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,7 @@
1
+ import { CacheEntryMeta } from '@appweaver/common';
2
+ import { EvictionIndex } from './eviction-index';
3
+ export declare class FifoEvictionIndex implements EvictionIndex {
4
+ add(key: string, meta: CacheEntryMeta): void;
5
+ remove(key: string): void;
6
+ touch(_key: string, _meta: CacheEntryMeta): void;
7
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FifoEvictionIndex = void 0;
4
+ class FifoEvictionIndex {
5
+ /** @internal */
6
+ _keys = new Map();
7
+ add(key, meta) {
8
+ this._keys.set(key, meta);
9
+ }
10
+ remove(key) {
11
+ this._keys.delete(key);
12
+ }
13
+ touch(_key, _meta) {
14
+ // FIFO strategy ignores access recency or frequency
15
+ }
16
+ /** @internal */
17
+ evictionCandidates(count) {
18
+ const candidates = [];
19
+ for (const [key] of this._keys) {
20
+ if (candidates.length >= count) {
21
+ break;
22
+ }
23
+ candidates.push(key);
24
+ }
25
+ return candidates;
26
+ }
27
+ }
28
+ exports.FifoEvictionIndex = FifoEvictionIndex;
@@ -0,0 +1,4 @@
1
+ export * from './eviction-index';
2
+ export * from './fifo-eviction-index';
3
+ export * from './lfu-eviction-index';
4
+ export * from './lru-eviction-index';
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./eviction-index"), exports);
18
+ __exportStar(require("./fifo-eviction-index"), exports);
19
+ __exportStar(require("./lfu-eviction-index"), exports);
20
+ __exportStar(require("./lru-eviction-index"), exports);
@@ -0,0 +1,8 @@
1
+ import { CacheEntryMeta } from '@appweaver/common';
2
+ import { EvictionIndex } from './eviction-index';
3
+ export declare class LfuEvictionIndex implements EvictionIndex {
4
+ add(key: string, meta: CacheEntryMeta): void;
5
+ remove(key: string): void;
6
+ touch(key: string, meta: CacheEntryMeta): void;
7
+ evictionCandidates(count: number, now: number, gracePeriod: number): string[];
8
+ }
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LfuEvictionIndex = void 0;
4
+ class LfuEvictionIndex {
5
+ /** @internal */
6
+ _entries = new Map();
7
+ add(key, meta) {
8
+ this._entries.set(key, meta);
9
+ }
10
+ remove(key) {
11
+ this._entries.delete(key);
12
+ }
13
+ touch(key, meta) {
14
+ this._entries.set(key, meta);
15
+ }
16
+ evictionCandidates(count, now, gracePeriod) {
17
+ // Build a min-heap of size `count` to find the k entries with the lowest
18
+ // frequency score (usedCount / age), avoiding a full sort.
19
+ // Time complexity: O(n log k) where k = count (typically 1).
20
+ const heap = [];
21
+ for (const [key, meta] of this._entries) {
22
+ if (meta.createdAt + gracePeriod >= now) {
23
+ continue;
24
+ }
25
+ const entry = { key, meta };
26
+ if (heap.length < count) {
27
+ heap.push(entry);
28
+ this.siftUp(heap, heap.length - 1, now);
29
+ }
30
+ else if (this.score(meta, now) < this.score(heap[0].meta, now)) {
31
+ heap[0] = entry;
32
+ this.siftDown(heap, 0, now);
33
+ }
34
+ }
35
+ // Extract candidates — heap contains the `count` lowest-scored entries
36
+ // We use a max-heap so heap[0] is the largest among selected candidates.
37
+ // All entries in the heap are valid eviction candidates.
38
+ return heap.map((e) => e.key);
39
+ }
40
+ /** @internal */
41
+ score(meta, now) {
42
+ const age = now - meta.createdAt;
43
+ return age > 0 ? (meta.usedCount + 1) / age : Infinity;
44
+ }
45
+ // Max-heap by score: the root has the highest score among selected candidates.
46
+ // When a new entry has a lower score than the root, it replaces the root,
47
+ // ensuring the heap always contains the k lowest-scored entries.
48
+ /** @internal */
49
+ siftUp(heap, index, now) {
50
+ while (index > 0) {
51
+ const parent = (index - 1) >> 1;
52
+ if (this.score(heap[index].meta, now) <= this.score(heap[parent].meta, now)) {
53
+ break;
54
+ }
55
+ [heap[index], heap[parent]] = [heap[parent], heap[index]];
56
+ index = parent;
57
+ }
58
+ }
59
+ /** @internal */
60
+ siftDown(heap, index, now) {
61
+ const length = heap.length;
62
+ while (true) {
63
+ let largest = index;
64
+ const left = 2 * index + 1;
65
+ const right = 2 * index + 2;
66
+ if (left < length &&
67
+ this.score(heap[left].meta, now) > this.score(heap[largest].meta, now)) {
68
+ largest = left;
69
+ }
70
+ if (right < length &&
71
+ this.score(heap[right].meta, now) > this.score(heap[largest].meta, now)) {
72
+ largest = right;
73
+ }
74
+ if (largest === index) {
75
+ break;
76
+ }
77
+ [heap[index], heap[largest]] = [heap[largest], heap[index]];
78
+ index = largest;
79
+ }
80
+ }
81
+ }
82
+ exports.LfuEvictionIndex = LfuEvictionIndex;