@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,384 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileService = void 0;
4
+ const common_1 = require("@appweaver/common");
5
+ const context_1 = require("../context");
6
+ const errors_1 = require("../errors");
7
+ const security_1 = require("../security");
8
+ const cache_1 = require("../cache");
9
+ const utils_1 = require("../utils");
10
+ class FileService {
11
+ /** @internal */
12
+ _db = (0, context_1.inject)(common_1.Database);
13
+ /** @internal */
14
+ _storage = (0, context_1.inject)(common_1.Storage);
15
+ /** @internal */
16
+ _cacheService = (0, context_1.inject)(cache_1.CacheService);
17
+ /**
18
+ * Searches for a file by its name from a database.
19
+ *
20
+ * @param {string} fileName - The name of the file to search for.
21
+ * @return {Promise<File>} A promise that resolves to the file object if found.
22
+ * @throws {HttpError} Throws an error if a database error occurs or if the file is not found.
23
+ */
24
+ async findByName(fileName) {
25
+ let file;
26
+ try {
27
+ file = (await this._db.client().file.findFirst({
28
+ where: { name: fileName }
29
+ }));
30
+ }
31
+ catch (e) {
32
+ throw new errors_1.HttpError(`File read error`, 500, e);
33
+ }
34
+ if (!file) {
35
+ throw new errors_1.HttpError('File does not exist', 404);
36
+ }
37
+ return file;
38
+ }
39
+ /**
40
+ * Streams a file from storage, handling authorization and range-based requests.
41
+ *
42
+ * @param {string} fileName - The name of the file to stream.
43
+ * @param {string} [range] - An optional range header specifying the byte range to stream.
44
+ * @return {Promise<FileStream>} A promise that resolves to a file stream object containing the content, metadata, and
45
+ * range details.
46
+ * @throws {HttpError} Throws a 404 error if the file does not exist.
47
+ * @throws {HttpError} Throws a 403 error if user access to the file is forbidden.
48
+ * @throws {HttpError} Throws a 500 error if an error occurs while reading the file from storage.
49
+ */
50
+ async stream(fileName, range) {
51
+ const identity = (0, security_1.currentAuthUser)();
52
+ const file = await this.findByName(fileName);
53
+ if (!file) {
54
+ throw new errors_1.HttpError('File does not exist', 404);
55
+ }
56
+ const policy = this.getFilePolicy(file.resourceName, file.resourceField);
57
+ if (!identity && policy.accessType !== 'public') {
58
+ throw new errors_1.HttpError('Public file access is forbidden', 403);
59
+ }
60
+ if (policy.accessType === 'private' && identity?.id !== file.createdById) {
61
+ throw new errors_1.HttpError('Private file access is forbidden', 403);
62
+ }
63
+ if (policy.accessType == 'protected' &&
64
+ file.resourceName &&
65
+ file.resourceId) {
66
+ const resourceService = (0, context_1.injectService)(file.resourceName);
67
+ const resource = await resourceService.find(file.resourceId);
68
+ if (identity && policy.canAccess?.(identity, resource, file) === false) {
69
+ throw new errors_1.HttpError('File access is forbidden', 403);
70
+ }
71
+ }
72
+ const parsedRange = (0, utils_1.parseRange)(range);
73
+ const start = parsedRange.start;
74
+ let end = parsedRange.end;
75
+ const fileStream = await this._storage.stream(fileName, start, end);
76
+ if (!fileStream) {
77
+ throw new errors_1.HttpError('Error reading file from storage', 500);
78
+ }
79
+ if (end === undefined || end >= fileStream.size || end >= file.sizeBytes) {
80
+ end = (fileStream.size ?? file.sizeBytes) - 1;
81
+ }
82
+ return {
83
+ content: fileStream,
84
+ fileName: file.name.split('/')[0],
85
+ mimeType: file.mimeType,
86
+ start,
87
+ end
88
+ };
89
+ }
90
+ /**
91
+ * Saves a file to storage, validates its properties, and associates it with a specific resource and client.
92
+ *
93
+ * @param {MultipartFile} data The multipart file object containing the file data, field name, filename, and metadata.
94
+ * @param {Resource} resource The resource object with which the file is being associated.
95
+ * @param {ResourceClient} client The database client responsible for handling the resource.
96
+ * @return {Promise<File>} A promise that resolves to the saved file object or rejects with an error if the operation
97
+ * fails.
98
+ * @throws {HttpError} Throws an error if file validation, storage, or resource association fails.
99
+ */
100
+ async saveFile(data, resource, client) {
101
+ const identity = (0, security_1.currentAuthUser)();
102
+ if (!(data.fieldname in resource)) {
103
+ throw new errors_1.HttpError(`File field '${data.fieldname}' does not exist on resource '${client.name}'`, 400);
104
+ }
105
+ const fileConfig = this.getFileConfig(client.name, data.fieldname);
106
+ const policy = this.getFilePolicy(client.name, data.fieldname);
107
+ if (!(0, utils_1.isValidMimeType)(data.mimetype, fileConfig.mimeType)) {
108
+ throw new errors_1.HttpError(`Unsupported media file type: ${data.mimetype}`, 400);
109
+ }
110
+ if (fileConfig.array) {
111
+ const fileCount = await this.fileCount(data.fieldname, client.name, resource.id);
112
+ if (fileConfig.maxCount && fileConfig.maxCount < fileCount + 1) {
113
+ throw new errors_1.HttpError(`Maximum number of files allowed: ${fileConfig.maxCount}`, 400);
114
+ }
115
+ }
116
+ let pattern;
117
+ if ((0, common_1.isFunction)(fileConfig.namePattern)) {
118
+ pattern = fileConfig.namePattern({
119
+ fieldName: data.fieldname,
120
+ fileName: data.filename,
121
+ encoding: data.encoding,
122
+ mimeType: data.mimetype,
123
+ bytesRead: data.file.bytesRead,
124
+ truncated: data.file.truncated
125
+ }, resource);
126
+ }
127
+ else {
128
+ pattern = fileConfig.namePattern;
129
+ }
130
+ let generatedName = (0, utils_1.generateFileName)(data.filename, pattern, {
131
+ resourceField: data.fieldname,
132
+ resourceName: client.name,
133
+ resourceId: resource.id,
134
+ userId: identity?.id,
135
+ userEmail: identity?.email
136
+ });
137
+ let nameRegenCount = 0;
138
+ while (await this._storage.exists(generatedName)) {
139
+ nameRegenCount++;
140
+ if (nameRegenCount === 10) {
141
+ throw new errors_1.HttpError('Unable to generate unique file name', 500);
142
+ }
143
+ const hash = (0, common_1.generateToken)('bytes');
144
+ const nameParts = generatedName.split('.');
145
+ if (nameParts.length > 1) {
146
+ const ext = nameParts.pop();
147
+ const base = nameParts.join('.');
148
+ generatedName = `${base}-${hash}.${ext}`;
149
+ }
150
+ else {
151
+ generatedName = `${nameParts[0]}-${hash}`;
152
+ }
153
+ }
154
+ const createFile = {
155
+ name: generatedName,
156
+ originalName: data.filename,
157
+ mimeType: data.mimetype,
158
+ sizeBytes: data.file.bytesRead,
159
+ resourceField: data.fieldname,
160
+ resourceName: client.name,
161
+ resourceId: resource.id,
162
+ createdById: identity?.id
163
+ };
164
+ if (identity &&
165
+ policy.canCreate?.(identity, resource, createFile) === false) {
166
+ throw new errors_1.HttpError('Creating file is forbidden', 403);
167
+ }
168
+ const fileName = await this._storage.store(generatedName, data.file);
169
+ if (!fileName) {
170
+ throw new errors_1.HttpError('Error saving file to storage', 500);
171
+ }
172
+ createFile.name = fileName;
173
+ // File size checks must come after storing a file due to bytesRead and
174
+ // truncated fields being set only after reading the full file stream.
175
+ const maxSizeBytes = (0, utils_1.sizeInBytes)(fileConfig.maxSize);
176
+ if (data.file.truncated ||
177
+ (maxSizeBytes > 0 && data.file.bytesRead > maxSizeBytes)) {
178
+ await this._storage.delete(fileName);
179
+ throw new errors_1.HttpError(`File size exceeded limit of ${maxSizeBytes} bytes`, 400);
180
+ }
181
+ try {
182
+ // Check if a resource already has a file for a single file property and
183
+ // delete it after successfully creating the new one.
184
+ let existingFile = null;
185
+ if (!fileConfig.array) {
186
+ const resourceWithFile = await client.findFirst({
187
+ where: { id: resource.id },
188
+ include: { [data.fieldname]: true }
189
+ });
190
+ existingFile = resourceWithFile[data.fieldname];
191
+ }
192
+ const result = await client.update({
193
+ where: { id: resource.id },
194
+ data: {
195
+ [data.fieldname]: {
196
+ create: createFile
197
+ }
198
+ },
199
+ include: { [data.fieldname]: true }
200
+ });
201
+ if (existingFile !== null) {
202
+ await this.deleteSafe(existingFile.name);
203
+ }
204
+ let file = result[data.fieldname];
205
+ if ((0, common_1.isArray)(result[data.fieldname])) {
206
+ file = result[data.fieldname].find((f) => f.name === createFile.name);
207
+ }
208
+ file.url = this.buildFileUrl(file, policy);
209
+ common_1.logger.debug({ file }, 'File saved');
210
+ return file;
211
+ }
212
+ catch (e) {
213
+ await this.deleteSafe(fileName);
214
+ throw new errors_1.HttpError(`File create error`, 500, e);
215
+ }
216
+ }
217
+ /**
218
+ * Saves files received as an asynchronous iterable of multipart data.
219
+ *
220
+ * @param {AsyncIterableIterator<Multipart>} parts - The asynchronous iterable providing multipart data where each
221
+ * part is expected to represent a file or other data.
222
+ * @param {Resource} resource - The resource associated with the files being saved, providing additional contextual
223
+ * information.
224
+ * @param {ResourceClient} client - The database client responsible for handling resource-related operations, such as
225
+ * database interaction.
226
+ * @return {Promise<File[]>} A promise that resolves to an array of successfully saved files. Rejects with an error
227
+ * if one or more files fail to save.
228
+ */
229
+ async saveFiles(parts, resource, client) {
230
+ const saveActions = [];
231
+ for await (const part of parts) {
232
+ if (part.type === 'file') {
233
+ saveActions.push(this.saveFile(part, resource, client));
234
+ }
235
+ }
236
+ const saveResults = await Promise.allSettled(saveActions);
237
+ const savedFiles = saveResults
238
+ .filter((r) => r.status === 'fulfilled')
239
+ .map((r) => r.value);
240
+ const errors = saveResults
241
+ .filter((r) => r.status === 'rejected')
242
+ .map((r) => r.reason);
243
+ if (errors.length > 0) {
244
+ // Delete all successfully saved files in storage and rows in a database.
245
+ const deleteActions = savedFiles.map((f) => this.deleteSafe(f.name));
246
+ await Promise.all(deleteActions);
247
+ const errorMessage = errors.join('\n');
248
+ throw new errors_1.HttpError(`Error while saving files: ${errorMessage}`, 400);
249
+ }
250
+ await this._cacheService.invalidateCache(client.name, 'uploadFiles');
251
+ return savedFiles;
252
+ }
253
+ /**
254
+ * Deletes a file from the storage and database if it meets the specified conditions.
255
+ *
256
+ * @param {string} fileName - The name of the file to be deleted.
257
+ * @param {string} fieldName - The field associated with the resource to which the file belongs.
258
+ * @param {Resource} resource - The resource that the file is associated with.
259
+ * @param {ResourceClient} client - The client instance representing the resource's type.
260
+ * @return {Promise<File>} A promise that resolves with the deleted file information upon successful deletion.
261
+ * @throws {HttpError} If the file is not associated with the provided resource or client,
262
+ * if the current user does not have permission to delete the file,
263
+ * if there is an error removing the file from storage,
264
+ * or if there is an error deleting the file from the database.
265
+ */
266
+ async deleteFile(fileName, fieldName, resource, client) {
267
+ const currentUser = (0, security_1.currentAuthUser)();
268
+ const file = await this.findByName(fileName);
269
+ if (file.resourceId !== resource.id ||
270
+ file.resourceName !== client.name ||
271
+ file.resourceField !== fieldName) {
272
+ throw new errors_1.HttpError(`File does not belong to a '${client.name}' resource`, 403);
273
+ }
274
+ const policy = this.getFilePolicy(file.resourceName, file.resourceField);
275
+ if (currentUser &&
276
+ policy.canDelete?.(currentUser, resource, file) === false) {
277
+ throw new errors_1.HttpError('Deleting file is forbidden', 403);
278
+ }
279
+ const result = await this._storage.delete(fileName);
280
+ if (!result) {
281
+ throw new errors_1.HttpError('Error deleting file from storage', 500);
282
+ }
283
+ try {
284
+ const deletedFile = (await this._db.client().file.delete({
285
+ where: { name: fileName }
286
+ }));
287
+ deletedFile.url = this.buildFileUrl(deletedFile, policy);
288
+ common_1.logger.debug({ deletedFile }, 'File deleted');
289
+ return deletedFile;
290
+ }
291
+ catch (e) {
292
+ throw new errors_1.HttpError(`File delete error`, 500, e);
293
+ }
294
+ }
295
+ /**
296
+ * Deletes a list of files associated with a given resource using the provided resource client.
297
+ *
298
+ * @param {Record<string, string | string[]>} fileNames - An object where the keys correspond to file fields and the
299
+ * values are either a string (single file name) or an array of file names to be deleted.
300
+ * @param {Resource} resource - The resource instance associated with the files to be deleted.
301
+ * @param {ResourceClient} client - The resource a client used to handle deletion operations.
302
+ * @return {Promise<File[]>} A promise that resolves to an array of files that were successfully deleted. Rejects with
303
+ * an error if no files could be deleted.
304
+ */
305
+ async deleteFiles(fileNames, resource, client) {
306
+ if (Object.keys(fileNames ?? {}).length === 0) {
307
+ throw new errors_1.HttpError('No files for deletion are provided', 400);
308
+ }
309
+ const deleteActions = [];
310
+ for (const [field, value] of Object.entries(fileNames)) {
311
+ const names = typeof value === 'string' ? [value] : value;
312
+ for (const name of names) {
313
+ deleteActions.push(this.deleteFile(name, field, resource, client));
314
+ }
315
+ }
316
+ const deleteResults = await Promise.allSettled(deleteActions);
317
+ const deletedFiles = deleteResults
318
+ .filter((r) => r.status === 'fulfilled')
319
+ .map((r) => r.value);
320
+ const errors = deleteResults
321
+ .filter((r) => r.status === 'rejected')
322
+ .map((r) => r.reason);
323
+ if (errors.length > 0 && deletedFiles.length === 0) {
324
+ const errorMessage = errors.join('\n');
325
+ throw new errors_1.HttpError(`Error while deleting files: ${errorMessage}`, 400);
326
+ }
327
+ await this._cacheService.invalidateCache(client.name, 'deleteFiles');
328
+ return deletedFiles;
329
+ }
330
+ /** @internal */
331
+ buildFileUrl(file, policy) {
332
+ return `${common_1.config.APP_HOSTNAME}/files/${policy.accessType === 'public' ? 'public' : 'protected'}/${file.name}`;
333
+ }
334
+ /** @internal */
335
+ async deleteSafe(fileName) {
336
+ let success = true;
337
+ try {
338
+ const result = await this._storage.delete(fileName);
339
+ if (!result) {
340
+ success = false;
341
+ }
342
+ }
343
+ catch {
344
+ success = false;
345
+ }
346
+ try {
347
+ await this._db.client().file.delete({
348
+ where: { name: fileName }
349
+ });
350
+ }
351
+ catch {
352
+ success = false;
353
+ }
354
+ return success;
355
+ }
356
+ /** @internal */
357
+ async fileCount(resourceField, resourceName, resourceId) {
358
+ try {
359
+ return await this._db.client().file.count({
360
+ where: { resourceField, resourceName, resourceId }
361
+ });
362
+ }
363
+ catch (e) {
364
+ throw new errors_1.HttpError(`File count read error`, 500, e);
365
+ }
366
+ }
367
+ /** @internal */
368
+ getFileConfig(resourceName, resourceField) {
369
+ if (!resourceName || !resourceField) {
370
+ return {};
371
+ }
372
+ const config = (0, context_1.injectModel)(resourceName, false)?.config.files?.[resourceField] ?? {};
373
+ return { ...config };
374
+ }
375
+ /** @internal */
376
+ getFilePolicy(resourceName, resourceField) {
377
+ if (!resourceName || !resourceField) {
378
+ return {};
379
+ }
380
+ const policy = (0, context_1.injectPolicy)(resourceName, false)?.files?.[resourceField] ?? {};
381
+ return { ...policy };
382
+ }
383
+ }
384
+ exports.FileService = FileService;
@@ -0,0 +1,10 @@
1
+ import { Readable } from 'node:stream';
2
+ import { ContentStream, HealthCheckResult, Storage } from '@appweaver/common';
3
+ export declare class FilesystemStorage extends Storage {
4
+ onInit(): Promise<void>;
5
+ stream(fileName: string, start?: number, end?: number): Promise<ContentStream | null>;
6
+ store(fileName: string, data: Readable): Promise<string | null>;
7
+ delete(fileName: string): Promise<boolean>;
8
+ exists(fileName: string): Promise<boolean>;
9
+ checkHealth(): Promise<HealthCheckResult>;
10
+ }
@@ -0,0 +1,128 @@
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
+ exports.FilesystemStorage = void 0;
7
+ const promises_1 = __importDefault(require("node:fs/promises"));
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const promises_2 = require("node:stream/promises");
11
+ const common_1 = require("@appweaver/common");
12
+ class FilesystemStorage extends common_1.Storage {
13
+ /** @internal */
14
+ _dirPath = common_1.config.STORAGE_PATH;
15
+ async onInit() {
16
+ const directoryExists = await this.exists('');
17
+ if (!directoryExists) {
18
+ await promises_1.default.mkdir(this._dirPath, { recursive: true });
19
+ common_1.logger.info(`Storage directory initialized: ${this._dirPath}`);
20
+ }
21
+ }
22
+ async stream(fileName, start = 0, end) {
23
+ const filePath = `${this._dirPath}/${fileName}`;
24
+ try {
25
+ const { size } = await promises_1.default.stat(filePath);
26
+ const endIndex = start >= 0 && !end && end !== 0
27
+ ? size - 1
28
+ : Math.min(end ?? Number.MAX_SAFE_INTEGER, size - 1);
29
+ const fileReadStream = node_fs_1.default.createReadStream(filePath, {
30
+ start,
31
+ end: !Number.isNaN(endIndex) ? endIndex : undefined
32
+ });
33
+ return { stream: fileReadStream, size };
34
+ }
35
+ catch (e) {
36
+ common_1.logger.error(e, `Error streaming file: ${filePath}`);
37
+ return null;
38
+ }
39
+ }
40
+ async store(fileName, data) {
41
+ const filePath = await this.getFilePath(fileName, true);
42
+ try {
43
+ await (0, promises_2.pipeline)(data, node_fs_1.default.createWriteStream(filePath));
44
+ return fileName;
45
+ }
46
+ catch (e) {
47
+ common_1.logger.error(e, `Error storing file: ${filePath}`);
48
+ return null;
49
+ }
50
+ }
51
+ async delete(fileName) {
52
+ const filePath = await this.getFilePath(fileName);
53
+ try {
54
+ await promises_1.default.unlink(filePath);
55
+ await this.removeEmptyDirectories(node_path_1.default.dirname(filePath));
56
+ return true;
57
+ }
58
+ catch (e) {
59
+ common_1.logger.error(e, `Error deleting file: ${filePath}`);
60
+ return false;
61
+ }
62
+ }
63
+ async exists(fileName) {
64
+ const filePath = await this.getFilePath(fileName);
65
+ try {
66
+ await promises_1.default.access(filePath, node_fs_1.default.constants.F_OK);
67
+ return true;
68
+ }
69
+ catch (e) {
70
+ return false;
71
+ }
72
+ }
73
+ async checkHealth() {
74
+ try {
75
+ await promises_1.default.access(this._dirPath, node_fs_1.default.constants.R_OK | node_fs_1.default.constants.W_OK);
76
+ return { success: true };
77
+ }
78
+ catch (e) {
79
+ return { success: false, message: e.message };
80
+ }
81
+ }
82
+ /** @internal */
83
+ async getFilePath(fileName, mkdir = false) {
84
+ const filePath = `${this._dirPath}/${fileName}`;
85
+ if (mkdir) {
86
+ await this.ensureDirectoryExists(filePath);
87
+ }
88
+ return filePath;
89
+ }
90
+ /** @internal */
91
+ async removeEmptyDirectories(filePath) {
92
+ const normalizedDirPath = node_path_1.default.normalize(filePath);
93
+ if (!normalizedDirPath ||
94
+ normalizedDirPath === node_path_1.default.normalize(this._dirPath)) {
95
+ return;
96
+ }
97
+ try {
98
+ const fileStats = await promises_1.default.lstat(filePath);
99
+ if (!fileStats.isDirectory()) {
100
+ return;
101
+ }
102
+ }
103
+ catch {
104
+ return;
105
+ }
106
+ let files = await promises_1.default.readdir(filePath);
107
+ if (files.length > 0) {
108
+ const recursiveRemovalActions = files.map((file) => this.removeEmptyDirectories(node_path_1.default.join(filePath, file)));
109
+ await Promise.all(recursiveRemovalActions);
110
+ files = await promises_1.default.readdir(filePath);
111
+ }
112
+ if (files.length === 0) {
113
+ await promises_1.default.rmdir(filePath);
114
+ await this.removeEmptyDirectories(node_path_1.default.dirname(filePath));
115
+ }
116
+ }
117
+ /** @internal */
118
+ async ensureDirectoryExists(filePath) {
119
+ const dirname = node_path_1.default.dirname(filePath);
120
+ try {
121
+ await promises_1.default.access(dirname, node_fs_1.default.constants.F_OK);
122
+ }
123
+ catch (e) {
124
+ await promises_1.default.mkdir(dirname, { recursive: true });
125
+ }
126
+ }
127
+ }
128
+ exports.FilesystemStorage = FilesystemStorage;
@@ -0,0 +1,2 @@
1
+ export * from './file-route';
2
+ export * from './file-service';
@@ -0,0 +1,18 @@
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("./file-route"), exports);
18
+ __exportStar(require("./file-service"), exports);
@@ -0,0 +1,2 @@
1
+ declare const _default: import("@appweaver/common").ResourceModel;
2
+ export default _default;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const common_1 = require("@appweaver/common");
4
+ const factory_1 = require("../../../factory");
5
+ const context_1 = require("../../../context");
6
+ exports.default = (0, factory_1.createModel)({
7
+ name: 'File',
8
+ scalars: {
9
+ name: {
10
+ type: 'string',
11
+ maxLength: 1023,
12
+ unique: true,
13
+ example: 'image_123.png'
14
+ },
15
+ originalName: {
16
+ type: 'string',
17
+ maxLength: 255,
18
+ example: 'image.png'
19
+ },
20
+ mimeType: {
21
+ type: 'string',
22
+ maxLength: 127
23
+ },
24
+ sizeBytes: {
25
+ type: 'int',
26
+ minimum: 0,
27
+ example: 1024
28
+ },
29
+ title: {
30
+ type: 'string',
31
+ maxLength: 511,
32
+ required: false
33
+ },
34
+ description: {
35
+ type: 'string',
36
+ maxLength: 4095,
37
+ required: false
38
+ },
39
+ resourceField: {
40
+ type: 'string',
41
+ maxLength: 255,
42
+ required: false,
43
+ hidden: true
44
+ },
45
+ resourceName: {
46
+ type: 'string',
47
+ maxLength: 255,
48
+ required: false,
49
+ hidden: true
50
+ },
51
+ resourceId: {
52
+ type: 'int',
53
+ minimum: 1,
54
+ required: false,
55
+ hidden: true
56
+ }
57
+ },
58
+ virtual: {
59
+ url: {
60
+ type: 'string',
61
+ input: {
62
+ type: 'none'
63
+ },
64
+ output: {
65
+ value: (file) => {
66
+ const accessType = (0, context_1.injectPolicy)(file.resourceName ?? '', false)
67
+ ?.files?.[file.resourceField ?? '']?.accessType;
68
+ const pathPrefix = accessType === 'public' ? 'public' : 'protected';
69
+ return `${common_1.config.APP_HOSTNAME}/files/${pathPrefix}/${file.name}`;
70
+ }
71
+ }
72
+ }
73
+ },
74
+ index: [['resourceField', 'resourceName', 'resourceId']]
75
+ });
@@ -0,0 +1,20 @@
1
+ import { AbstractCtor, Ctor, IResourceService, ResourceModel, ResourcePolicyConfig, ResourceRoutes } from '@appweaver/common';
2
+ import { Server } from './server';
3
+ export type DefinitionMode = 'ignore' | 'append' | 'override' | 'fail';
4
+ export type DefinitionClass<T = any> = AbstractCtor<T> | Ctor<T>;
5
+ export type DefinitionValue = object | ((...args: any[]) => any) | Record<string, any>;
6
+ export type DefinitionEntry = {
7
+ name: string | symbol;
8
+ value: DefinitionValue;
9
+ };
10
+ export type ResourceContext = {
11
+ models: Map<string | symbol, ResourceModel>;
12
+ services: Map<string | symbol, IResourceService | Ctor<IResourceService>>;
13
+ policies: Map<string | symbol, ResourcePolicyConfig>;
14
+ routes: Map<string | symbol, ResourceRoutes>;
15
+ };
16
+ export type ApplicationContext = {
17
+ server: Server | null;
18
+ resource: ResourceContext;
19
+ definitions: DefinitionEntry[];
20
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,31 @@
1
+ import { AuthScope, AuthSource } from '@appweaver/common';
2
+ export type JwtPayload = {
3
+ scope: AuthScope;
4
+ source: AuthSource;
5
+ username: string;
6
+ sub: number;
7
+ iat: number;
8
+ };
9
+ export type AuthTokens = {
10
+ accessToken: string;
11
+ refreshToken: string;
12
+ expiresIn: number;
13
+ refreshExpiresIn: number;
14
+ };
15
+ export type AuthOTTData = {
16
+ authUserId: number;
17
+ authSource: AuthSource;
18
+ };
19
+ export type TwoFactorAuthData = {
20
+ authUserId: number;
21
+ codeHash: string;
22
+ purpose: string;
23
+ };
24
+ export type OAuth2StateData = {
25
+ redirectToUrl: string;
26
+ };
27
+ export type UserAdditionalData = {
28
+ firstName: string;
29
+ lastName: string;
30
+ };
31
+ export type RegistrationDataFn<T = any> = (source: AuthSource, email: string, password?: string, additionalData?: Partial<UserAdditionalData>) => T | Promise<T>;
package/types/auth.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });