@appweaver/core 1.3.1 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/export/export-service.d.ts +4 -3
- package/export/export-service.js +39 -22
- package/factory/create-model.js +54 -22
- package/factory/create-service.js +5 -3
- package/package.json +2 -2
- package/prisma/client/commonInputTypes.d.ts +0 -50
- package/prisma/client/internal/class.js +3 -3
- package/prisma/client/models/File.d.ts +15 -28
- package/resource/resource-loader.js +6 -0
- package/resource/resource-routes.js +2 -2
- package/resource/resource-schema.d.ts +38 -12
- package/resource/resource-schema.js +63 -15
- package/resource/resource-service.d.ts +25 -16
- package/resource/resource-service.js +61 -26
- package/resource/schemas/resource-sort-schema.js +2 -2
- package/resource/utils/cursor-util.d.ts +60 -0
- package/resource/utils/cursor-util.js +117 -0
- package/resource/utils/index.d.ts +1 -0
- package/resource/utils/index.js +1 -0
- package/resource/utils/relation-util.d.ts +3 -3
- package/resource/utils/relation-util.js +52 -16
- package/resource/utils/sort-util.d.ts +11 -0
- package/resource/utils/sort-util.js +18 -0
- package/security/api-key/api-key-auth.js +4 -1
- package/security/auth-service.d.ts +7 -7
- package/security/auth-service.js +43 -5
- package/security/create-auth-resources.d.ts +2 -1
- package/security/oauth2/create-oauth2-plugin.js +3 -34
- package/security/oauth2/oauth2-microsoft.js +1 -1
- package/security/oauth2/oauth2-util.d.ts +9 -7
- package/security/oauth2/oauth2-util.js +17 -10
- package/security/resources/api-key/model.js +2 -0
- package/security/resources/connected-account/model.js +2 -0
- package/security/store/database-security-store.js +2 -1
- package/server/swagger.js +49 -1
- package/storage/file-service.d.ts +19 -4
- package/storage/file-service.js +199 -146
- package/storage/resources/file/model.js +4 -2
- package/types/auth.d.ts +8 -8
- package/types/generated.d.ts +8 -2
- package/types/index.d.ts +1 -0
- package/types/index.js +1 -0
- package/types/storage.d.ts +16 -0
- package/types/storage.js +2 -0
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
- package/utils/model-util.d.ts +12 -0
- package/utils/model-util.js +113 -0
package/storage/file-service.js
CHANGED
|
@@ -67,7 +67,9 @@ class FileService {
|
|
|
67
67
|
file.resourceName &&
|
|
68
68
|
file.resourceId) {
|
|
69
69
|
const resourceService = (0, context_1.injectService)(file.resourceName);
|
|
70
|
-
|
|
70
|
+
// The owning id is stored as text, so it is converted back to the
|
|
71
|
+
// primary key type of that model before the lookup
|
|
72
|
+
const resource = await resourceService.find((0, common_1.toResourceId)(file.resourceId, (0, context_1.injectModel)(file.resourceName, false)?.config?.id));
|
|
71
73
|
if (identity && policy.canAccess?.(identity, resource, file) === false) {
|
|
72
74
|
throw new errors_1.HttpError('File access is forbidden', 403);
|
|
73
75
|
}
|
|
@@ -101,145 +103,41 @@ class FileService {
|
|
|
101
103
|
* @throws {HttpError} Throws an error if file validation, storage, or resource association fails.
|
|
102
104
|
*/
|
|
103
105
|
async saveFile(data, resource, client) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
const fileConfig = this.getFileConfig(client.name, data.fieldname);
|
|
109
|
-
const policy = this.getFilePolicy(client.name, data.fieldname);
|
|
110
|
-
if (!(0, utils_1.isValidMimeType)(data.mimetype, fileConfig.mimeType)) {
|
|
111
|
-
throw new errors_1.HttpError(`Unsupported media file type: ${data.mimetype}`, 400);
|
|
112
|
-
}
|
|
113
|
-
if (fileConfig.array) {
|
|
114
|
-
const fileCount = await this.fileCount(data.fieldname, client.name, resource.id);
|
|
115
|
-
if (fileConfig.maxCount && fileConfig.maxCount < fileCount + 1) {
|
|
116
|
-
throw new errors_1.HttpError(`Maximum number of files allowed: ${fileConfig.maxCount}`, 400);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
let pattern;
|
|
120
|
-
if ((0, common_1.isFunction)(fileConfig.namePattern)) {
|
|
121
|
-
pattern = fileConfig.namePattern({
|
|
122
|
-
fieldName: data.fieldname,
|
|
123
|
-
fileName: data.filename,
|
|
124
|
-
encoding: data.encoding,
|
|
125
|
-
mimeType: data.mimetype,
|
|
126
|
-
bytesRead: data.file.bytesRead,
|
|
127
|
-
truncated: data.file.truncated
|
|
128
|
-
}, resource);
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
pattern = fileConfig.namePattern;
|
|
132
|
-
}
|
|
133
|
-
let generatedName = (0, utils_1.generateFileName)(data.filename, pattern, {
|
|
134
|
-
resourceField: data.fieldname,
|
|
135
|
-
resourceName: client.name,
|
|
136
|
-
resourceId: resource.id,
|
|
137
|
-
userId: identity?.id,
|
|
138
|
-
userEmail: identity?.email
|
|
139
|
-
});
|
|
140
|
-
// The generated name may come from a name pattern configured as a function,
|
|
141
|
-
// which cannot be validated on startup, so it is checked here as well.
|
|
142
|
-
this.assertPathNotReserved(generatedName);
|
|
143
|
-
let nameRegenCount = 0;
|
|
144
|
-
while (await this._storage.exists(generatedName)) {
|
|
145
|
-
nameRegenCount++;
|
|
146
|
-
if (nameRegenCount === 10) {
|
|
147
|
-
throw new errors_1.HttpError('Unable to generate unique file name', 500);
|
|
148
|
-
}
|
|
149
|
-
const hash = (0, common_1.generateToken)('bytes');
|
|
150
|
-
const nameParts = generatedName.split('.');
|
|
151
|
-
if (nameParts.length > 1) {
|
|
152
|
-
const ext = nameParts.pop();
|
|
153
|
-
const base = nameParts.join('.');
|
|
154
|
-
generatedName = `${base}-${hash}.${ext}`;
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
generatedName = `${nameParts[0]}-${hash}`;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
const createFile = {
|
|
161
|
-
name: generatedName,
|
|
162
|
-
originalName: data.filename,
|
|
106
|
+
return this.saveContent({
|
|
107
|
+
fieldName: data.fieldname,
|
|
108
|
+
fileName: data.filename,
|
|
163
109
|
mimeType: data.mimetype,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (!fileName) {
|
|
196
|
-
throw new errors_1.HttpError('Error saving file to storage', 500);
|
|
197
|
-
}
|
|
198
|
-
createFile.name = fileName;
|
|
199
|
-
createFile.checksum = checksum;
|
|
200
|
-
// File size checks must come after storing a file due to bytesRead and
|
|
201
|
-
// truncated fields being set only after reading the full file stream.
|
|
202
|
-
const maxSizeBytes = (0, utils_1.sizeInBytes)(fileConfig.maxSize);
|
|
203
|
-
if (data.file.truncated ||
|
|
204
|
-
(maxSizeBytes > 0 && data.file.bytesRead > maxSizeBytes)) {
|
|
205
|
-
await this._storage.delete(fileName);
|
|
206
|
-
throw new errors_1.HttpError(`File size exceeded limit of ${maxSizeBytes} bytes`, 400);
|
|
207
|
-
}
|
|
208
|
-
try {
|
|
209
|
-
// Check if a resource already has a file for a single file property and
|
|
210
|
-
// delete it after successfully creating the new one.
|
|
211
|
-
let existingFile = null;
|
|
212
|
-
if (!fileConfig.array) {
|
|
213
|
-
const resourceWithFile = await client.findFirst({
|
|
214
|
-
where: { id: resource.id },
|
|
215
|
-
include: { [data.fieldname]: true }
|
|
216
|
-
});
|
|
217
|
-
existingFile = resourceWithFile[data.fieldname];
|
|
218
|
-
}
|
|
219
|
-
const result = await client.update({
|
|
220
|
-
where: { id: resource.id },
|
|
221
|
-
data: {
|
|
222
|
-
[data.fieldname]: {
|
|
223
|
-
create: createFile
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
include: { [data.fieldname]: true }
|
|
227
|
-
});
|
|
228
|
-
if (existingFile !== null) {
|
|
229
|
-
await this.deleteSafe(existingFile.name);
|
|
230
|
-
}
|
|
231
|
-
let file = result[data.fieldname];
|
|
232
|
-
if ((0, common_1.isArray)(result[data.fieldname])) {
|
|
233
|
-
file = result[data.fieldname].find((f) => f.name === createFile.name);
|
|
234
|
-
}
|
|
235
|
-
file.url = (0, utils_1.buildFileUrl)(file);
|
|
236
|
-
common_1.logger.debug({ file }, 'File saved');
|
|
237
|
-
return file;
|
|
238
|
-
}
|
|
239
|
-
catch (e) {
|
|
240
|
-
await this.deleteSafe(fileName);
|
|
241
|
-
throw new errors_1.HttpError(`File create error`, 500, e);
|
|
242
|
-
}
|
|
110
|
+
encoding: data.encoding,
|
|
111
|
+
stream: data.file,
|
|
112
|
+
bytesRead: () => data.file.bytesRead,
|
|
113
|
+
truncated: () => data.file.truncated
|
|
114
|
+
}, resource, client);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Saves an in-memory file to storage and associates it with a file field of a specific resource. Behaves exactly like
|
|
118
|
+
* {@link saveFile}, except the content is taken from a buffer instead of a multipart upload, which makes it usable
|
|
119
|
+
* outside a file upload request, for an avatar downloaded from an OAuth2 provider or an image the application
|
|
120
|
+
* generates itself.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} fieldName The file field of the resource the file is saved to.
|
|
123
|
+
* @param {FileBuffer} file The file content, along with its original name and media type.
|
|
124
|
+
* @param {Resource} resource The resource object with which the file is being associated.
|
|
125
|
+
* @param {ResourceClient} client The database client responsible for handling the resource.
|
|
126
|
+
* @return {Promise<File>} A promise that resolves to the saved file object or rejects with an error if the operation
|
|
127
|
+
* fails.
|
|
128
|
+
* @throws {HttpError} Throws an error if file validation, storage, or resource association fails.
|
|
129
|
+
*/
|
|
130
|
+
async saveBuffer(fieldName, file, resource, client) {
|
|
131
|
+
const size = file.size ?? file.data.length;
|
|
132
|
+
return this.saveContent({
|
|
133
|
+
fieldName,
|
|
134
|
+
fileName: file.name,
|
|
135
|
+
mimeType: file.mimeType,
|
|
136
|
+
encoding: file.encoding ?? '7bit',
|
|
137
|
+
stream: node_stream_1.Readable.from(file.data),
|
|
138
|
+
bytesRead: () => size,
|
|
139
|
+
truncated: () => false
|
|
140
|
+
}, resource, client);
|
|
243
141
|
}
|
|
244
142
|
/**
|
|
245
143
|
* Saves files received as an asynchronous iterable of multipart data.
|
|
@@ -293,7 +191,7 @@ class FileService {
|
|
|
293
191
|
async deleteFile(fileName, fieldName, resource, client) {
|
|
294
192
|
const currentUser = (0, security_1.currentAuthUser)();
|
|
295
193
|
const file = await this.findByName(fileName);
|
|
296
|
-
if (file.resourceId !== resource.id ||
|
|
194
|
+
if (file.resourceId !== String(resource.id) ||
|
|
297
195
|
file.resourceName !== client.name ||
|
|
298
196
|
file.resourceField !== fieldName) {
|
|
299
197
|
throw new errors_1.HttpError(`File does not belong to a '${client.name}' resource`, 403);
|
|
@@ -360,10 +258,12 @@ class FileService {
|
|
|
360
258
|
* setting (or set to `'keep'`) are left untouched.
|
|
361
259
|
*
|
|
362
260
|
* @param {string} resourceName - The resource model name.
|
|
363
|
-
* @param {
|
|
261
|
+
* @param {ResourceId} id - The ID of the deleted resource.
|
|
364
262
|
* @return {Promise<File[]>} A promise that resolves to the list of successfully deleted files.
|
|
365
263
|
*/
|
|
366
|
-
async deleteResourceFiles(resourceName,
|
|
264
|
+
async deleteResourceFiles(resourceName, id) {
|
|
265
|
+
// Owning ids are stored as text, whatever the model primary key type is
|
|
266
|
+
const resourceId = String(id);
|
|
367
267
|
const resourceModel = (0, context_1.injectModel)(resourceName, false);
|
|
368
268
|
if (!resourceModel) {
|
|
369
269
|
return [];
|
|
@@ -405,6 +305,158 @@ class FileService {
|
|
|
405
305
|
return deletedFiles;
|
|
406
306
|
}
|
|
407
307
|
/** @internal */
|
|
308
|
+
async saveContent(data, resource, client) {
|
|
309
|
+
const identity = (0, security_1.currentAuthUser)();
|
|
310
|
+
// Checked against the model rather than the resource object, so a file field
|
|
311
|
+
// kept out of the resource output is still writable
|
|
312
|
+
const fileConfig = this.getFileConfig(client.name, data.fieldName);
|
|
313
|
+
if (!fileConfig) {
|
|
314
|
+
throw new errors_1.HttpError(`File field '${data.fieldName}' does not exist on resource '${client.name}'`, 400);
|
|
315
|
+
}
|
|
316
|
+
const policy = this.getFilePolicy(client.name, data.fieldName);
|
|
317
|
+
if (!(0, utils_1.isValidMimeType)(data.mimeType, fileConfig.mimeType)) {
|
|
318
|
+
throw new errors_1.HttpError(`Unsupported media file type: ${data.mimeType}`, 400);
|
|
319
|
+
}
|
|
320
|
+
if (fileConfig.array) {
|
|
321
|
+
const fileCount = await this.fileCount(data.fieldName, client.name, String(resource.id));
|
|
322
|
+
if (fileConfig.maxCount && fileConfig.maxCount < fileCount + 1) {
|
|
323
|
+
throw new errors_1.HttpError(`Maximum number of files allowed: ${fileConfig.maxCount}`, 400);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
let pattern;
|
|
327
|
+
if ((0, common_1.isFunction)(fileConfig.namePattern)) {
|
|
328
|
+
pattern = fileConfig.namePattern({
|
|
329
|
+
fieldName: data.fieldName,
|
|
330
|
+
fileName: data.fileName,
|
|
331
|
+
encoding: data.encoding,
|
|
332
|
+
mimeType: data.mimeType,
|
|
333
|
+
bytesRead: data.bytesRead(),
|
|
334
|
+
truncated: data.truncated()
|
|
335
|
+
}, resource);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
pattern = fileConfig.namePattern;
|
|
339
|
+
}
|
|
340
|
+
let generatedName = (0, utils_1.generateFileName)(data.fileName, pattern, {
|
|
341
|
+
resourceField: data.fieldName,
|
|
342
|
+
resourceName: client.name,
|
|
343
|
+
resourceId: resource.id,
|
|
344
|
+
userId: identity?.id,
|
|
345
|
+
userEmail: identity?.email
|
|
346
|
+
});
|
|
347
|
+
// The generated name may come from a name pattern configured as a function,
|
|
348
|
+
// which cannot be validated on startup, so it is checked here as well.
|
|
349
|
+
this.assertPathNotReserved(generatedName);
|
|
350
|
+
let nameRegenCount = 0;
|
|
351
|
+
while (await this._storage.exists(generatedName)) {
|
|
352
|
+
nameRegenCount++;
|
|
353
|
+
if (nameRegenCount === 10) {
|
|
354
|
+
throw new errors_1.HttpError('Unable to generate unique file name', 500);
|
|
355
|
+
}
|
|
356
|
+
const hash = (0, common_1.generateToken)('bytes');
|
|
357
|
+
const nameParts = generatedName.split('.');
|
|
358
|
+
if (nameParts.length > 1) {
|
|
359
|
+
const ext = nameParts.pop();
|
|
360
|
+
const base = nameParts.join('.');
|
|
361
|
+
generatedName = `${base}-${hash}.${ext}`;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
generatedName = `${nameParts[0]}-${hash}`;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const createFile = {
|
|
368
|
+
name: generatedName,
|
|
369
|
+
originalName: data.fileName,
|
|
370
|
+
mimeType: data.mimeType,
|
|
371
|
+
sizeBytes: data.bytesRead(),
|
|
372
|
+
resourceField: data.fieldName,
|
|
373
|
+
resourceName: client.name,
|
|
374
|
+
resourceId: String(resource.id),
|
|
375
|
+
createdById: identity?.id
|
|
376
|
+
};
|
|
377
|
+
if (identity &&
|
|
378
|
+
policy.canCreate?.(identity, resource, createFile) === false) {
|
|
379
|
+
throw new errors_1.HttpError('Creating file is forbidden', 403);
|
|
380
|
+
}
|
|
381
|
+
let fileStream = data.stream;
|
|
382
|
+
if ((0, utils_1.isProcessableImage)(data.mimeType)) {
|
|
383
|
+
fileStream = (0, utils_1.processImage)(data.stream, data.mimeType, fileConfig.image);
|
|
384
|
+
}
|
|
385
|
+
// Tee the stream so the checksum and the stored size are calculated over the
|
|
386
|
+
// exact bytes written to storage in a single pass, without buffering the file.
|
|
387
|
+
let storedBytes = 0;
|
|
388
|
+
const storageStream = new node_stream_1.Transform({
|
|
389
|
+
transform(chunk, _encoding, callback) {
|
|
390
|
+
storedBytes += chunk.length;
|
|
391
|
+
callback(null, chunk);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
const checksumStream = new node_stream_1.PassThrough();
|
|
395
|
+
fileStream.pipe(storageStream);
|
|
396
|
+
fileStream.pipe(checksumStream);
|
|
397
|
+
// pipe() does not forward source errors to destinations, so both branches
|
|
398
|
+
// must be destroyed manually to avoid hanging on a failed upload stream.
|
|
399
|
+
fileStream.on('error', (e) => {
|
|
400
|
+
common_1.logger.error(e, 'Error calculating file checksum');
|
|
401
|
+
storageStream.destroy(e);
|
|
402
|
+
checksumStream.destroy(e);
|
|
403
|
+
});
|
|
404
|
+
const [fileName, checksum] = await Promise.all([
|
|
405
|
+
this._storage.store(generatedName, storageStream),
|
|
406
|
+
(0, common_1.makeHash)(checksumStream)
|
|
407
|
+
]);
|
|
408
|
+
if (!fileName) {
|
|
409
|
+
throw new errors_1.HttpError('Error saving file to storage', 500);
|
|
410
|
+
}
|
|
411
|
+
createFile.name = fileName;
|
|
412
|
+
createFile.checksum = checksum;
|
|
413
|
+
// The size of the content in storage, which is the processed one for images
|
|
414
|
+
createFile.sizeBytes = storedBytes;
|
|
415
|
+
// The size limit applies to the received content, whose read size and
|
|
416
|
+
// truncation flag are only known once the stream has been consumed.
|
|
417
|
+
const maxSizeBytes = (0, utils_1.sizeInBytes)(fileConfig.maxSize);
|
|
418
|
+
if (data.truncated() ||
|
|
419
|
+
(maxSizeBytes > 0 && data.bytesRead() > maxSizeBytes)) {
|
|
420
|
+
await this._storage.delete(fileName);
|
|
421
|
+
throw new errors_1.HttpError(`File size exceeded limit of ${maxSizeBytes} bytes`, 400);
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
// Check if a resource already has a file for a single file property and
|
|
425
|
+
// delete it after successfully creating the new one.
|
|
426
|
+
let existingFile = null;
|
|
427
|
+
if (!fileConfig.array) {
|
|
428
|
+
const resourceWithFile = await client.findFirst({
|
|
429
|
+
where: { id: resource.id },
|
|
430
|
+
include: { [data.fieldName]: true }
|
|
431
|
+
});
|
|
432
|
+
existingFile = resourceWithFile[data.fieldName];
|
|
433
|
+
}
|
|
434
|
+
const result = await client.update({
|
|
435
|
+
where: { id: resource.id },
|
|
436
|
+
data: {
|
|
437
|
+
[data.fieldName]: {
|
|
438
|
+
create: createFile
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
include: { [data.fieldName]: true }
|
|
442
|
+
});
|
|
443
|
+
if (existingFile !== null) {
|
|
444
|
+
await this.deleteSafe(existingFile.name);
|
|
445
|
+
}
|
|
446
|
+
let file = result[data.fieldName];
|
|
447
|
+
if ((0, common_1.isArray)(result[data.fieldName])) {
|
|
448
|
+
file = result[data.fieldName].find((f) => f.name === createFile.name);
|
|
449
|
+
}
|
|
450
|
+
file.url = (0, utils_1.buildFileUrl)(file);
|
|
451
|
+
common_1.logger.debug({ file }, 'File saved');
|
|
452
|
+
return file;
|
|
453
|
+
}
|
|
454
|
+
catch (e) {
|
|
455
|
+
await this.deleteSafe(fileName);
|
|
456
|
+
throw new errors_1.HttpError(`File create error`, 500, e);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/** @internal */
|
|
408
460
|
assertPathNotReserved(fileName) {
|
|
409
461
|
const reservedPath = (0, common_1.findReservedStoragePath)(fileName, common_1.config.STORAGE_RESERVED_PATHS);
|
|
410
462
|
if (reservedPath !== null) {
|
|
@@ -444,13 +496,14 @@ class FileService {
|
|
|
444
496
|
throw new errors_1.HttpError(`File count read error`, 500, e);
|
|
445
497
|
}
|
|
446
498
|
}
|
|
447
|
-
/**
|
|
499
|
+
/** Returns undefined when the model has no such file field configured.
|
|
500
|
+
* @internal */
|
|
448
501
|
getFileConfig(resourceName, resourceField) {
|
|
449
502
|
if (!resourceName || !resourceField) {
|
|
450
|
-
return
|
|
503
|
+
return undefined;
|
|
451
504
|
}
|
|
452
|
-
const config = (0, context_1.injectModel)(resourceName, false)?.config.files?.[resourceField]
|
|
453
|
-
return { ...config };
|
|
505
|
+
const config = (0, context_1.injectModel)(resourceName, false)?.config.files?.[resourceField];
|
|
506
|
+
return config ? { ...config } : undefined;
|
|
454
507
|
}
|
|
455
508
|
/** @internal */
|
|
456
509
|
getFilePolicy(resourceName, resourceField) {
|
|
@@ -52,9 +52,11 @@ exports.default = (0, factory_1.createModel)({
|
|
|
52
52
|
required: false,
|
|
53
53
|
hidden: true
|
|
54
54
|
},
|
|
55
|
+
// Polymorphic reference without a foreign key, stored as text so a file can
|
|
56
|
+
// belong to a resource with either primary key type
|
|
55
57
|
resourceId: {
|
|
56
|
-
type: '
|
|
57
|
-
|
|
58
|
+
type: 'string',
|
|
59
|
+
maxLength: 36,
|
|
58
60
|
required: false,
|
|
59
61
|
hidden: true
|
|
60
62
|
}
|
package/types/auth.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { FastifyRequest } from 'fastify';
|
|
2
|
-
import { AuthScope, AuthSource, AuthUser } from '@appweaver/common';
|
|
2
|
+
import { AuthScope, AuthSource, AuthUser, ResourceId } from '@appweaver/common';
|
|
3
|
+
import { FileBuffer } from './storage';
|
|
3
4
|
export type JwtPayload = {
|
|
4
5
|
scope: AuthScope;
|
|
5
6
|
source: AuthSource;
|
|
6
7
|
username: string;
|
|
7
|
-
sub:
|
|
8
|
+
sub: ResourceId;
|
|
8
9
|
iat: number;
|
|
9
10
|
};
|
|
10
11
|
export type AuthTokens = {
|
|
@@ -14,25 +15,22 @@ export type AuthTokens = {
|
|
|
14
15
|
refreshExpiresIn: number;
|
|
15
16
|
};
|
|
16
17
|
export type AuthOTTData = {
|
|
17
|
-
authUserId:
|
|
18
|
+
authUserId: ResourceId;
|
|
18
19
|
authSource: AuthSource;
|
|
19
20
|
providerAccountId?: string;
|
|
20
21
|
scope?: string;
|
|
21
22
|
passwordRequired?: boolean;
|
|
22
23
|
};
|
|
23
24
|
export type TwoFactorAuthData = {
|
|
24
|
-
authUserId:
|
|
25
|
+
authUserId: ResourceId;
|
|
25
26
|
codeHash: string;
|
|
26
27
|
purpose: string;
|
|
27
28
|
};
|
|
28
29
|
export type OAuth2StateData = {
|
|
29
30
|
redirectToUrl: string;
|
|
30
31
|
};
|
|
31
|
-
export type AvatarFile = {
|
|
32
|
-
name: string;
|
|
33
|
-
mimeType: string;
|
|
32
|
+
export type AvatarFile = FileBuffer & {
|
|
34
33
|
size: number;
|
|
35
|
-
data: Buffer;
|
|
36
34
|
};
|
|
37
35
|
export type UserInfo = {
|
|
38
36
|
id: string;
|
|
@@ -62,4 +60,6 @@ export type UserAdditionalData = {
|
|
|
62
60
|
avatarFile?: AvatarFile;
|
|
63
61
|
};
|
|
64
62
|
export type RegistrationDataFn<T = any> = (source: AuthSource, email: string, password?: string, additionalData?: Partial<UserAdditionalData>) => T | Promise<T>;
|
|
63
|
+
export type RegistrationFilesFn = (source: AuthSource, additionalData?: Partial<UserAdditionalData>) => RegistrationFiles | Promise<RegistrationFiles>;
|
|
64
|
+
export type RegistrationFiles = Record<string, FileBuffer | null | undefined>;
|
|
65
65
|
export type CheckOAuth2UserFn = (source: AuthSource, userInfo: UserInfo, authUser: AuthUser | null) => void | string | Error | Promise<void | string | Error>;
|
package/types/generated.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AggregateSelect, QueryFilter, QuerySort } from '@appweaver/common';
|
|
1
|
+
import { AggregateSelect, IResourceService, QueryFilter, QuerySort } from '@appweaver/common';
|
|
2
2
|
export type ApiKey = {
|
|
3
3
|
id: number;
|
|
4
4
|
key: string;
|
|
@@ -66,6 +66,7 @@ export type ApiKeyRelationInput = {
|
|
|
66
66
|
export type ApiKeyQuery = QueryFilter<ApiKey>;
|
|
67
67
|
export type ApiKeySort = QuerySort<ApiKeyMultiple>;
|
|
68
68
|
export type ApiKeyAggregate = AggregateSelect<ApiKey>;
|
|
69
|
+
export type ApiKeyResourceService = IResourceService<ApiKey, ApiKeyMultiple, ApiKeyCreate, ApiKeyUpdate, ApiKeyQuery>;
|
|
69
70
|
export type ConnectedAccount = {
|
|
70
71
|
id: number;
|
|
71
72
|
provider: string;
|
|
@@ -128,6 +129,7 @@ export type ConnectedAccountRelationInput = {
|
|
|
128
129
|
export type ConnectedAccountQuery = QueryFilter<ConnectedAccount>;
|
|
129
130
|
export type ConnectedAccountSort = QuerySort<ConnectedAccountMultiple>;
|
|
130
131
|
export type ConnectedAccountAggregate = AggregateSelect<ConnectedAccount>;
|
|
132
|
+
export type ConnectedAccountResourceService = IResourceService<ConnectedAccount, ConnectedAccountMultiple, ConnectedAccountCreate, ConnectedAccountUpdate, ConnectedAccountQuery>;
|
|
131
133
|
export type OneTimeToken = {
|
|
132
134
|
id: number;
|
|
133
135
|
tokenHash: string;
|
|
@@ -190,6 +192,7 @@ export type OneTimeTokenRelationInput = {
|
|
|
190
192
|
export type OneTimeTokenQuery = QueryFilter<OneTimeToken>;
|
|
191
193
|
export type OneTimeTokenSort = QuerySort<OneTimeTokenMultiple>;
|
|
192
194
|
export type OneTimeTokenAggregate = AggregateSelect<OneTimeToken>;
|
|
195
|
+
export type OneTimeTokenResourceService = IResourceService<OneTimeToken, OneTimeTokenMultiple, OneTimeTokenCreate, OneTimeTokenUpdate, OneTimeTokenQuery>;
|
|
193
196
|
export type Permission = {
|
|
194
197
|
id: number;
|
|
195
198
|
name: string;
|
|
@@ -225,6 +228,7 @@ export type PermissionRelationInput = {
|
|
|
225
228
|
export type PermissionQuery = QueryFilter<Permission>;
|
|
226
229
|
export type PermissionSort = QuerySort<PermissionMultiple>;
|
|
227
230
|
export type PermissionAggregate = AggregateSelect<Permission>;
|
|
231
|
+
export type PermissionResourceService = IResourceService<Permission, PermissionMultiple, PermissionCreate, PermissionUpdate, PermissionQuery>;
|
|
228
232
|
export type Role = {
|
|
229
233
|
id: number;
|
|
230
234
|
name: string;
|
|
@@ -269,6 +273,7 @@ export type RoleRelationInput = {
|
|
|
269
273
|
export type RoleQuery = QueryFilter<Role>;
|
|
270
274
|
export type RoleSort = QuerySort<RoleMultiple>;
|
|
271
275
|
export type RoleAggregate = AggregateSelect<Role>;
|
|
276
|
+
export type RoleResourceService = IResourceService<Role, RoleMultiple, RoleCreate, RoleUpdate, RoleQuery>;
|
|
272
277
|
export type File = {
|
|
273
278
|
id: number;
|
|
274
279
|
name: string;
|
|
@@ -280,7 +285,7 @@ export type File = {
|
|
|
280
285
|
description?: string | null;
|
|
281
286
|
resourceField?: string | null;
|
|
282
287
|
resourceName?: string | null;
|
|
283
|
-
resourceId?:
|
|
288
|
+
resourceId?: string | null;
|
|
284
289
|
url: string;
|
|
285
290
|
updatedAt: Date;
|
|
286
291
|
createdAt: Date;
|
|
@@ -364,3 +369,4 @@ export type FileRelationInput = {
|
|
|
364
369
|
export type FileQuery = QueryFilter<File>;
|
|
365
370
|
export type FileSort = QuerySort<FileMultiple>;
|
|
366
371
|
export type FileAggregate = AggregateSelect<File>;
|
|
372
|
+
export type FileResourceService = IResourceService<File, FileMultiple, FileCreate, FileUpdate, FileQuery>;
|
package/types/index.d.ts
CHANGED
package/types/index.js
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An in-memory file, used to store a file that was not received as a multipart upload, such as an avatar downloaded
|
|
3
|
+
* from an OAuth2 provider or an image generated by the application itself.
|
|
4
|
+
*/
|
|
5
|
+
export type FileBuffer = {
|
|
6
|
+
/** The original file name, used to derive the stored name and its extension. */
|
|
7
|
+
name: string;
|
|
8
|
+
/** The media type of the content, checked against the `mimeType` of the file field. */
|
|
9
|
+
mimeType: string;
|
|
10
|
+
/** The file content. */
|
|
11
|
+
data: Buffer;
|
|
12
|
+
/** The content size in bytes checked against the size limit of the file field, defaults to the data buffer length. */
|
|
13
|
+
size?: number;
|
|
14
|
+
/** The content encoding, defaults to `7bit`. */
|
|
15
|
+
encoding?: string;
|
|
16
|
+
};
|
package/types/storage.js
ADDED
package/utils/index.d.ts
CHANGED
package/utils/index.js
CHANGED
|
@@ -17,5 +17,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
__exportStar(require("./export-util"), exports);
|
|
18
18
|
__exportStar(require("./file-util"), exports);
|
|
19
19
|
__exportStar(require("./image-util"), exports);
|
|
20
|
+
__exportStar(require("./model-util"), exports);
|
|
20
21
|
__exportStar(require("./schema-util"), exports);
|
|
21
22
|
__exportStar(require("./virtual-util"), exports);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ResourceModel } from '@appweaver/common';
|
|
2
|
+
/**
|
|
3
|
+
* Validates that the default value of every scalar and virtual field satisfies
|
|
4
|
+
* the constraints declared on that same field. A default outside its own
|
|
5
|
+
* constraints produces records that do not match the output schema of their
|
|
6
|
+
* model, which only surfaces once the model is nested in a nullable relation,
|
|
7
|
+
* where the response serializer validates it before picking a schema branch.
|
|
8
|
+
*
|
|
9
|
+
* @param {Record<string, ResourceModel>} models - All loaded models keyed by name.
|
|
10
|
+
* @throws {Error} When any default value violates the constraints of its field.
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateScalarDefaults(models: Record<string, ResourceModel>): void;
|