create-forgeon 0.3.6 → 0.3.8
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/package.json +1 -1
- package/src/modules/dependencies.mjs +1 -1
- package/src/modules/executor.test.mjs +33 -0
- package/src/modules/files-access.mjs +12 -3
- package/src/modules/files-image.mjs +12 -3
- package/src/modules/files-quotas.mjs +24 -3
- package/src/modules/shared/patch-utils.mjs +25 -0
- package/src/run-add-module.mjs +1 -1
- package/templates/module-presets/files/packages/files/src/files-env.schema.ts +1 -0
- package/templates/module-presets/files/packages/files/src/files.controller.ts +2 -1
- package/templates/module-presets/files/packages/files/src/files.service.ts +26 -8
package/package.json
CHANGED
|
@@ -102,7 +102,7 @@ async function selectProviderForCapability({
|
|
|
102
102
|
|
|
103
103
|
const picked = await promptSelectImpl({
|
|
104
104
|
message: `Module "${moduleId}" requires capability: ${capabilityId}`,
|
|
105
|
-
defaultValue:
|
|
105
|
+
defaultValue: providers[0].id,
|
|
106
106
|
choices,
|
|
107
107
|
});
|
|
108
108
|
|
|
@@ -324,6 +324,11 @@ function assertFilesAccessWiring(projectRoot) {
|
|
|
324
324
|
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
325
325
|
assert.match(apiPackage, /@forgeon\/files-access/);
|
|
326
326
|
assert.match(apiPackage, /pnpm --filter @forgeon\/files-access build/);
|
|
327
|
+
assert.equal(
|
|
328
|
+
apiPackage.indexOf('pnpm --filter @forgeon/files-access build') <
|
|
329
|
+
apiPackage.indexOf('pnpm --filter @forgeon/files build'),
|
|
330
|
+
true,
|
|
331
|
+
);
|
|
327
332
|
|
|
328
333
|
const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
|
|
329
334
|
assert.match(filesPackage, /@forgeon\/files-access/);
|
|
@@ -335,6 +340,11 @@ function assertFilesAccessWiring(projectRoot) {
|
|
|
335
340
|
);
|
|
336
341
|
assert.match(apiDockerfile, /COPY packages\/files-access packages\/files-access/);
|
|
337
342
|
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-access build/);
|
|
343
|
+
assert.equal(
|
|
344
|
+
apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files-access build') <
|
|
345
|
+
apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files build'),
|
|
346
|
+
true,
|
|
347
|
+
);
|
|
338
348
|
|
|
339
349
|
const filesController = fs.readFileSync(
|
|
340
350
|
path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
|
|
@@ -373,6 +383,14 @@ function assertFilesQuotasWiring(projectRoot) {
|
|
|
373
383
|
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
374
384
|
assert.match(apiPackage, /@forgeon\/files-quotas/);
|
|
375
385
|
assert.match(apiPackage, /pnpm --filter @forgeon\/files-quotas build/);
|
|
386
|
+
assert.equal(
|
|
387
|
+
apiPackage.indexOf('pnpm --filter @forgeon/files-quotas build') <
|
|
388
|
+
apiPackage.indexOf('pnpm --filter @forgeon/files build'),
|
|
389
|
+
true,
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
|
|
393
|
+
assert.match(filesPackage, /@forgeon\/files-quotas/);
|
|
376
394
|
|
|
377
395
|
const apiDockerfile = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'Dockerfile'), 'utf8');
|
|
378
396
|
assert.match(
|
|
@@ -381,6 +399,11 @@ function assertFilesQuotasWiring(projectRoot) {
|
|
|
381
399
|
);
|
|
382
400
|
assert.match(apiDockerfile, /COPY packages\/files-quotas packages\/files-quotas/);
|
|
383
401
|
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-quotas build/);
|
|
402
|
+
assert.equal(
|
|
403
|
+
apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files-quotas build') <
|
|
404
|
+
apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files build'),
|
|
405
|
+
true,
|
|
406
|
+
);
|
|
384
407
|
|
|
385
408
|
const filesController = fs.readFileSync(
|
|
386
409
|
path.join(projectRoot, 'packages', 'files', 'src', 'files.controller.ts'),
|
|
@@ -421,6 +444,11 @@ function assertFilesImageWiring(projectRoot) {
|
|
|
421
444
|
const apiPackage = fs.readFileSync(path.join(projectRoot, 'apps', 'api', 'package.json'), 'utf8');
|
|
422
445
|
assert.match(apiPackage, /@forgeon\/files-image/);
|
|
423
446
|
assert.match(apiPackage, /pnpm --filter @forgeon\/files-image build/);
|
|
447
|
+
assert.equal(
|
|
448
|
+
apiPackage.indexOf('pnpm --filter @forgeon/files-image build') <
|
|
449
|
+
apiPackage.indexOf('pnpm --filter @forgeon/files build'),
|
|
450
|
+
true,
|
|
451
|
+
);
|
|
424
452
|
|
|
425
453
|
const filesPackage = fs.readFileSync(path.join(projectRoot, 'packages', 'files', 'package.json'), 'utf8');
|
|
426
454
|
assert.match(filesPackage, /@forgeon\/files-image/);
|
|
@@ -457,6 +485,11 @@ function assertFilesImageWiring(projectRoot) {
|
|
|
457
485
|
);
|
|
458
486
|
assert.match(apiDockerfile, /COPY packages\/files-image packages\/files-image/);
|
|
459
487
|
assert.match(apiDockerfile, /RUN pnpm --filter @forgeon\/files-image build/);
|
|
488
|
+
assert.equal(
|
|
489
|
+
apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files-image build') <
|
|
490
|
+
apiDockerfile.indexOf('RUN pnpm --filter @forgeon/files build'),
|
|
491
|
+
true,
|
|
492
|
+
);
|
|
460
493
|
|
|
461
494
|
const healthController = fs.readFileSync(
|
|
462
495
|
path.join(projectRoot, 'apps', 'api', 'src', 'health', 'health.controller.ts'),
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
4
|
import {
|
|
5
|
+
ensureBuildStepBefore,
|
|
5
6
|
ensureBuildSteps,
|
|
6
7
|
ensureClassMember,
|
|
7
8
|
ensureDependency,
|
|
@@ -29,6 +30,12 @@ function patchApiPackage(targetRoot) {
|
|
|
29
30
|
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
30
31
|
ensureDependency(packageJson, '@forgeon/files-access', 'workspace:*');
|
|
31
32
|
ensureBuildSteps(packageJson, 'predev', ['pnpm --filter @forgeon/files-access build']);
|
|
33
|
+
ensureBuildStepBefore(
|
|
34
|
+
packageJson,
|
|
35
|
+
'predev',
|
|
36
|
+
'pnpm --filter @forgeon/files-access build',
|
|
37
|
+
'pnpm --filter @forgeon/files build',
|
|
38
|
+
);
|
|
32
39
|
writeJson(packagePath, packageJson);
|
|
33
40
|
}
|
|
34
41
|
|
|
@@ -373,9 +380,11 @@ function patchApiDockerfile(targetRoot) {
|
|
|
373
380
|
content = ensureLineAfter(content, sourceAnchor, 'COPY packages/files-access packages/files-access');
|
|
374
381
|
|
|
375
382
|
content = content.replace(/^RUN pnpm --filter @forgeon\/files-access build\r?\n?/gm, '');
|
|
376
|
-
const buildAnchor = content.includes('RUN pnpm --filter @forgeon/
|
|
377
|
-
? 'RUN pnpm --filter @forgeon/
|
|
378
|
-
: 'RUN pnpm --filter @forgeon/api
|
|
383
|
+
const buildAnchor = content.includes('RUN pnpm --filter @forgeon/files build')
|
|
384
|
+
? 'RUN pnpm --filter @forgeon/files build'
|
|
385
|
+
: content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
|
|
386
|
+
? 'RUN pnpm --filter @forgeon/api prisma:generate'
|
|
387
|
+
: 'RUN pnpm --filter @forgeon/api build';
|
|
379
388
|
content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/files-access build');
|
|
380
389
|
|
|
381
390
|
fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
4
|
import {
|
|
5
|
+
ensureBuildStepBefore,
|
|
5
6
|
ensureBuildSteps,
|
|
6
7
|
ensureClassMember,
|
|
7
8
|
ensureDependency,
|
|
@@ -32,6 +33,12 @@ function patchApiPackage(targetRoot) {
|
|
|
32
33
|
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
33
34
|
ensureDependency(packageJson, '@forgeon/files-image', 'workspace:*');
|
|
34
35
|
ensureBuildSteps(packageJson, 'predev', ['pnpm --filter @forgeon/files-image build']);
|
|
36
|
+
ensureBuildStepBefore(
|
|
37
|
+
packageJson,
|
|
38
|
+
'predev',
|
|
39
|
+
'pnpm --filter @forgeon/files-image build',
|
|
40
|
+
'pnpm --filter @forgeon/files build',
|
|
41
|
+
);
|
|
35
42
|
writeJson(packagePath, packageJson);
|
|
36
43
|
}
|
|
37
44
|
|
|
@@ -402,9 +409,11 @@ function patchApiDockerfile(targetRoot) {
|
|
|
402
409
|
content = ensureLineAfter(content, sourceAnchor, 'COPY packages/files-image packages/files-image');
|
|
403
410
|
|
|
404
411
|
content = content.replace(/^RUN pnpm --filter @forgeon\/files-image build\r?\n?/gm, '');
|
|
405
|
-
const buildAnchor = content.includes('RUN pnpm --filter @forgeon/
|
|
406
|
-
? 'RUN pnpm --filter @forgeon/
|
|
407
|
-
: 'RUN pnpm --filter @forgeon/api
|
|
412
|
+
const buildAnchor = content.includes('RUN pnpm --filter @forgeon/files build')
|
|
413
|
+
? 'RUN pnpm --filter @forgeon/files build'
|
|
414
|
+
: content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
|
|
415
|
+
? 'RUN pnpm --filter @forgeon/api prisma:generate'
|
|
416
|
+
: 'RUN pnpm --filter @forgeon/api build';
|
|
408
417
|
content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/files-image build');
|
|
409
418
|
|
|
410
419
|
fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { copyRecursive, writeJson } from '../utils/fs.mjs';
|
|
4
4
|
import {
|
|
5
|
+
ensureBuildStepBefore,
|
|
5
6
|
ensureBuildSteps,
|
|
6
7
|
ensureClassMember,
|
|
7
8
|
ensureDependency,
|
|
@@ -32,6 +33,23 @@ function patchApiPackage(targetRoot) {
|
|
|
32
33
|
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
33
34
|
ensureDependency(packageJson, '@forgeon/files-quotas', 'workspace:*');
|
|
34
35
|
ensureBuildSteps(packageJson, 'predev', ['pnpm --filter @forgeon/files-quotas build']);
|
|
36
|
+
ensureBuildStepBefore(
|
|
37
|
+
packageJson,
|
|
38
|
+
'predev',
|
|
39
|
+
'pnpm --filter @forgeon/files-quotas build',
|
|
40
|
+
'pnpm --filter @forgeon/files build',
|
|
41
|
+
);
|
|
42
|
+
writeJson(packagePath, packageJson);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function patchFilesPackage(targetRoot) {
|
|
46
|
+
const packagePath = path.join(targetRoot, 'packages', 'files', 'package.json');
|
|
47
|
+
if (!fs.existsSync(packagePath)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
52
|
+
ensureDependency(packageJson, '@forgeon/files-quotas', 'workspace:*');
|
|
35
53
|
writeJson(packagePath, packageJson);
|
|
36
54
|
}
|
|
37
55
|
|
|
@@ -280,9 +298,11 @@ function patchApiDockerfile(targetRoot) {
|
|
|
280
298
|
content = ensureLineAfter(content, sourceAnchor, 'COPY packages/files-quotas packages/files-quotas');
|
|
281
299
|
|
|
282
300
|
content = content.replace(/^RUN pnpm --filter @forgeon\/files-quotas build\r?\n?/gm, '');
|
|
283
|
-
const buildAnchor = content.includes('RUN pnpm --filter @forgeon/
|
|
284
|
-
? 'RUN pnpm --filter @forgeon/
|
|
285
|
-
: 'RUN pnpm --filter @forgeon/api
|
|
301
|
+
const buildAnchor = content.includes('RUN pnpm --filter @forgeon/files build')
|
|
302
|
+
? 'RUN pnpm --filter @forgeon/files build'
|
|
303
|
+
: content.includes('RUN pnpm --filter @forgeon/api prisma:generate')
|
|
304
|
+
? 'RUN pnpm --filter @forgeon/api prisma:generate'
|
|
305
|
+
: 'RUN pnpm --filter @forgeon/api build';
|
|
286
306
|
content = ensureLineBefore(content, buildAnchor, 'RUN pnpm --filter @forgeon/files-quotas build');
|
|
287
307
|
|
|
288
308
|
fs.writeFileSync(dockerfilePath, `${content.trimEnd()}\n`, 'utf8');
|
|
@@ -360,6 +380,7 @@ Key env:
|
|
|
360
380
|
export function applyFilesQuotasModule({ packageRoot, targetRoot }) {
|
|
361
381
|
copyFromPreset(packageRoot, targetRoot, path.join('packages', 'files-quotas'));
|
|
362
382
|
patchApiPackage(targetRoot);
|
|
383
|
+
patchFilesPackage(targetRoot);
|
|
363
384
|
patchAppModule(targetRoot);
|
|
364
385
|
patchFilesController(targetRoot);
|
|
365
386
|
patchHealthController(targetRoot);
|
|
@@ -46,6 +46,31 @@ export function ensureBuildSteps(packageJson, scriptName, requiredCommands) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export function ensureBuildStepBefore(packageJson, scriptName, command, beforeCommand) {
|
|
50
|
+
if (!packageJson.scripts) {
|
|
51
|
+
packageJson.scripts = {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const current = packageJson.scripts[scriptName];
|
|
55
|
+
const steps =
|
|
56
|
+
typeof current === 'string' && current.trim().length > 0
|
|
57
|
+
? current
|
|
58
|
+
.split('&&')
|
|
59
|
+
.map((item) => item.trim())
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
: [];
|
|
62
|
+
|
|
63
|
+
const withoutCommand = steps.filter((item) => item !== command);
|
|
64
|
+
const beforeIndex = withoutCommand.indexOf(beforeCommand);
|
|
65
|
+
if (beforeIndex >= 0) {
|
|
66
|
+
withoutCommand.splice(beforeIndex, 0, command);
|
|
67
|
+
} else {
|
|
68
|
+
withoutCommand.push(command);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
packageJson.scripts[scriptName] = withoutCommand.join(' && ');
|
|
72
|
+
}
|
|
73
|
+
|
|
49
74
|
export function ensureLineAfter(content, anchorLine, lineToInsert) {
|
|
50
75
|
if (content.includes(lineToInsert)) {
|
|
51
76
|
return content;
|
package/src/run-add-module.mjs
CHANGED
|
@@ -147,7 +147,7 @@ async function confirmInstallPlan(moduleSequence, requestedModuleId) {
|
|
|
147
147
|
printInstallPlan(moduleSequence);
|
|
148
148
|
const picked = await promptSelect({
|
|
149
149
|
message: `Apply now for "${requestedModuleId}"?`,
|
|
150
|
-
defaultValue: '
|
|
150
|
+
defaultValue: 'apply',
|
|
151
151
|
choices: [
|
|
152
152
|
{ label: 'Yes, apply install plan', value: 'apply' },
|
|
153
153
|
{ label: 'Cancel', value: 'cancel' },
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
UseInterceptors,
|
|
13
13
|
} from '@nestjs/common';
|
|
14
14
|
import { FileInterceptor } from '@nestjs/platform-express';
|
|
15
|
+
import { Readable } from 'node:stream';
|
|
15
16
|
import { CreateFileDto } from './dto/create-file.dto';
|
|
16
17
|
import { FilesService } from './files.service';
|
|
17
18
|
import type { FileVariantKey } from './files.types';
|
|
@@ -58,7 +59,7 @@ export class FilesController {
|
|
|
58
59
|
async download(@Param('publicId') publicId: string, @Query('variant') variantQuery?: string) {
|
|
59
60
|
const variant = this.parseVariant(variantQuery);
|
|
60
61
|
const payload = await this.filesService.openDownload(publicId, variant);
|
|
61
|
-
return new StreamableFile(payload.stream, {
|
|
62
|
+
return new StreamableFile(payload.stream as Readable, {
|
|
62
63
|
disposition: `inline; filename="${payload.fileName}"`,
|
|
63
64
|
type: payload.mimeType,
|
|
64
65
|
});
|
|
@@ -15,6 +15,17 @@ import { PrismaService } from '@forgeon/db-prisma';
|
|
|
15
15
|
import { FilesConfigService } from './files-config.service';
|
|
16
16
|
import type { FileRecordDto, FileVariantKey, StoredFileInput } from './files.types';
|
|
17
17
|
|
|
18
|
+
type S3ModuleLike = {
|
|
19
|
+
S3Client: new (config: Record<string, unknown>) => {
|
|
20
|
+
send: (command: unknown) => Promise<{
|
|
21
|
+
Body?: unknown;
|
|
22
|
+
}>;
|
|
23
|
+
};
|
|
24
|
+
PutObjectCommand: new (input: Record<string, unknown>) => unknown;
|
|
25
|
+
GetObjectCommand: new (input: Record<string, unknown>) => unknown;
|
|
26
|
+
DeleteObjectCommand: new (input: Record<string, unknown>) => unknown;
|
|
27
|
+
};
|
|
28
|
+
|
|
18
29
|
type BlobRef = {
|
|
19
30
|
id: string;
|
|
20
31
|
hash: string;
|
|
@@ -225,7 +236,7 @@ export class FilesService {
|
|
|
225
236
|
}
|
|
226
237
|
|
|
227
238
|
async openDownload(publicId: string, variant: FileVariantKey = 'original'): Promise<{
|
|
228
|
-
stream:
|
|
239
|
+
stream: Readable;
|
|
229
240
|
mimeType: string;
|
|
230
241
|
fileName: string;
|
|
231
242
|
}> {
|
|
@@ -439,7 +450,7 @@ export class FilesService {
|
|
|
439
450
|
}
|
|
440
451
|
|
|
441
452
|
private async storeS3(buffer: Buffer, storageKey: string): Promise<{ storageKey: string }> {
|
|
442
|
-
const { PutObjectCommand } = await
|
|
453
|
+
const { PutObjectCommand } = await this.loadS3Module();
|
|
443
454
|
const client = await this.getS3Client();
|
|
444
455
|
const config = this.resolveS3Config();
|
|
445
456
|
|
|
@@ -454,8 +465,8 @@ export class FilesService {
|
|
|
454
465
|
return { storageKey };
|
|
455
466
|
}
|
|
456
467
|
|
|
457
|
-
private async openS3(storageKey: string): Promise<
|
|
458
|
-
const { GetObjectCommand } = await
|
|
468
|
+
private async openS3(storageKey: string): Promise<Readable> {
|
|
469
|
+
const { GetObjectCommand } = await this.loadS3Module();
|
|
459
470
|
const client = await this.getS3Client();
|
|
460
471
|
const config = this.resolveS3Config();
|
|
461
472
|
|
|
@@ -473,7 +484,7 @@ export class FilesService {
|
|
|
473
484
|
}
|
|
474
485
|
|
|
475
486
|
private async deleteS3(storageKey: string): Promise<void> {
|
|
476
|
-
const { DeleteObjectCommand } = await
|
|
487
|
+
const { DeleteObjectCommand } = await this.loadS3Module();
|
|
477
488
|
const client = await this.getS3Client();
|
|
478
489
|
const config = this.resolveS3Config();
|
|
479
490
|
await client.send(
|
|
@@ -566,7 +577,7 @@ export class FilesService {
|
|
|
566
577
|
if (this.s3Client) {
|
|
567
578
|
return this.s3Client;
|
|
568
579
|
}
|
|
569
|
-
const { S3Client } = await
|
|
580
|
+
const { S3Client } = await this.loadS3Module();
|
|
570
581
|
const config = this.resolveS3Config();
|
|
571
582
|
this.s3Client = new S3Client({
|
|
572
583
|
region: config.region,
|
|
@@ -581,7 +592,7 @@ export class FilesService {
|
|
|
581
592
|
return this.s3Client;
|
|
582
593
|
}
|
|
583
594
|
|
|
584
|
-
private toNodeReadable(body: unknown):
|
|
595
|
+
private toNodeReadable(body: unknown): Readable {
|
|
585
596
|
if (body instanceof Readable) {
|
|
586
597
|
return body;
|
|
587
598
|
}
|
|
@@ -594,12 +605,19 @@ export class FilesService {
|
|
|
594
605
|
typeof (body as { transformToWebStream?: unknown }).transformToWebStream === 'function'
|
|
595
606
|
) {
|
|
596
607
|
return Readable.fromWeb(
|
|
597
|
-
(body as { transformToWebStream: () =>
|
|
608
|
+
(body as { transformToWebStream: () => unknown }).transformToWebStream() as never,
|
|
598
609
|
);
|
|
599
610
|
}
|
|
600
611
|
throw new InternalServerErrorException('Unsupported S3 response body type');
|
|
601
612
|
}
|
|
602
613
|
|
|
614
|
+
private async loadS3Module(): Promise<S3ModuleLike> {
|
|
615
|
+
const importModule = new Function('specifier', 'return import(specifier)') as (
|
|
616
|
+
specifier: string,
|
|
617
|
+
) => Promise<S3ModuleLike>;
|
|
618
|
+
return importModule('@aws-sdk/client-s3');
|
|
619
|
+
}
|
|
620
|
+
|
|
603
621
|
private generatePublicId(): string {
|
|
604
622
|
return crypto.randomUUID().replace(/-/g, '');
|
|
605
623
|
}
|