@appweaver/core 1.3.0 → 1.4.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 (47) hide show
  1. package/export/export-service.d.ts +4 -3
  2. package/export/export-service.js +33 -22
  3. package/factory/create-model.js +36 -21
  4. package/factory/create-service.js +5 -3
  5. package/package.json +2 -2
  6. package/prisma/client/commonInputTypes.d.ts +0 -50
  7. package/prisma/client/internal/class.js +3 -3
  8. package/prisma/client/models/File.d.ts +15 -28
  9. package/resource/resource-loader.js +6 -0
  10. package/resource/resource-routes.js +2 -2
  11. package/resource/resource-schema.d.ts +38 -12
  12. package/resource/resource-schema.js +63 -15
  13. package/resource/resource-service.d.ts +25 -16
  14. package/resource/resource-service.js +61 -26
  15. package/resource/schemas/resource-sort-schema.js +2 -2
  16. package/resource/utils/cursor-util.d.ts +60 -0
  17. package/resource/utils/cursor-util.js +117 -0
  18. package/resource/utils/index.d.ts +1 -0
  19. package/resource/utils/index.js +1 -0
  20. package/resource/utils/relation-util.d.ts +3 -3
  21. package/resource/utils/relation-util.js +1 -1
  22. package/resource/utils/sort-util.d.ts +11 -0
  23. package/resource/utils/sort-util.js +18 -0
  24. package/security/api-key/api-key-auth.js +4 -1
  25. package/security/auth-service.d.ts +7 -7
  26. package/security/auth-service.js +43 -5
  27. package/security/create-auth-resources.d.ts +2 -1
  28. package/security/oauth2/create-oauth2-plugin.js +3 -34
  29. package/security/oauth2/oauth2-microsoft.js +1 -1
  30. package/security/oauth2/oauth2-util.d.ts +9 -7
  31. package/security/oauth2/oauth2-util.js +17 -10
  32. package/security/resources/api-key/model.js +2 -0
  33. package/security/resources/connected-account/model.js +2 -0
  34. package/security/store/database-security-store.js +2 -1
  35. package/storage/file-service.d.ts +19 -4
  36. package/storage/file-service.js +199 -146
  37. package/storage/resources/file/model.js +4 -2
  38. package/types/auth.d.ts +8 -8
  39. package/types/generated.d.ts +8 -2
  40. package/types/index.d.ts +1 -0
  41. package/types/index.js +1 -0
  42. package/types/storage.d.ts +16 -0
  43. package/types/storage.js +2 -0
  44. package/utils/index.d.ts +1 -0
  45. package/utils/index.js +1 -0
  46. package/utils/model-util.d.ts +12 -0
  47. package/utils/model-util.js +113 -0
@@ -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
- const resource = await resourceService.find(file.resourceId);
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
- const identity = (0, security_1.currentAuthUser)();
105
- if (!(data.fieldname in resource)) {
106
- throw new errors_1.HttpError(`File field '${data.fieldname}' does not exist on resource '${client.name}'`, 400);
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
- sizeBytes: data.file.bytesRead,
165
- resourceField: data.fieldname,
166
- resourceName: client.name,
167
- resourceId: resource.id,
168
- createdById: identity?.id
169
- };
170
- if (identity &&
171
- policy.canCreate?.(identity, resource, createFile) === false) {
172
- throw new errors_1.HttpError('Creating file is forbidden', 403);
173
- }
174
- let fileStream = data.file;
175
- if ((0, utils_1.isProcessableImage)(data.mimetype)) {
176
- fileStream = (0, utils_1.processImage)(data.file, data.mimetype, fileConfig.image);
177
- }
178
- // Tee the stream so the checksum is calculated over the exact bytes
179
- // written to storage in a single pass, without buffering the file.
180
- const storageStream = new node_stream_1.PassThrough();
181
- const checksumStream = new node_stream_1.PassThrough();
182
- fileStream.pipe(storageStream);
183
- fileStream.pipe(checksumStream);
184
- // pipe() does not forward source errors to destinations, so both branches
185
- // must be destroyed manually to avoid hanging on a failed upload stream.
186
- fileStream.on('error', (e) => {
187
- common_1.logger.error(e, 'Error calculating file checksum');
188
- storageStream.destroy(e);
189
- checksumStream.destroy(e);
190
- });
191
- const [fileName, checksum] = await Promise.all([
192
- this._storage.store(generatedName, storageStream),
193
- (0, common_1.makeHash)(checksumStream)
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 {number} resourceId - The ID of the deleted resource.
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, resourceId) {
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
- /** @internal */
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: 'int',
57
- minimum: 1,
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: number;
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: number;
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: number;
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>;
@@ -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?: number | null;
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
@@ -2,3 +2,4 @@ export * from './application-context';
2
2
  export * from './auth';
3
3
  export * from './generated';
4
4
  export * from './server';
5
+ export * from './storage';
package/types/index.js CHANGED
@@ -18,3 +18,4 @@ __exportStar(require("./application-context"), exports);
18
18
  __exportStar(require("./auth"), exports);
19
19
  __exportStar(require("./generated"), exports);
20
20
  __exportStar(require("./server"), exports);
21
+ __exportStar(require("./storage"), exports);
@@ -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
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/utils/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './export-util';
2
2
  export * from './file-util';
3
3
  export * from './image-util';
4
+ export * from './model-util';
4
5
  export * from './schema-util';
5
6
  export * from './virtual-util';
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;