create-forgeon 0.3.19 → 0.3.21
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/cli/add-help.mjs +3 -2
- package/src/core/docs.test.mjs +1 -0
- package/src/core/scaffold.test.mjs +1 -0
- package/src/modules/accounts.mjs +9 -18
- package/src/modules/dependencies.mjs +153 -4
- package/src/modules/dependencies.test.mjs +58 -0
- package/src/modules/executor.test.mjs +544 -515
- package/src/modules/files-access.mjs +375 -375
- package/src/modules/files-image.mjs +512 -510
- package/src/modules/files-quotas.mjs +365 -365
- package/src/modules/files.mjs +5 -6
- package/src/modules/idempotency.test.mjs +3 -2
- package/src/modules/registry.mjs +20 -0
- package/src/modules/shared/files-runtime-wiring.mjs +13 -10
- package/src/run-add-module.mjs +39 -26
- package/src/run-add-module.test.mjs +228 -152
- package/src/run-scan-integrations.mjs +1 -0
- package/templates/base/package.json +1 -0
- package/templates/module-presets/accounts/packages/accounts-api/package.json +1 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/auth-core.service.ts +15 -19
- package/templates/module-presets/accounts/{apps/api/src/accounts/prisma-accounts-persistence.store.ts → packages/accounts-api/src/auth.store.ts} +44 -166
- package/templates/module-presets/accounts/packages/accounts-api/src/forgeon-accounts.module.ts +7 -1
- package/templates/module-presets/accounts/packages/accounts-api/src/index.ts +3 -4
- package/templates/module-presets/accounts/packages/accounts-api/src/users.service.ts +10 -11
- package/templates/module-presets/accounts/packages/accounts-api/src/users.store.ts +113 -0
- package/templates/module-presets/accounts/packages/accounts-api/src/users.types.ts +48 -0
- package/templates/module-presets/files/packages/files/package.json +1 -0
- package/templates/module-presets/files/packages/files/src/files.ports.ts +0 -95
- package/templates/module-presets/files/packages/files/src/files.service.ts +43 -36
- package/templates/module-presets/files/{apps/api/src/files/prisma-files-persistence.store.ts → packages/files/src/files.store.ts} +77 -13
- package/templates/module-presets/files/packages/files/src/forgeon-files.module.ts +7 -116
- package/templates/module-presets/files/packages/files/src/index.ts +1 -0
- package/templates/module-presets/files-quotas/packages/files-quotas/package.json +20 -20
- package/templates/module-presets/files-quotas/packages/files-quotas/src/files-quotas.service.ts +118 -118
- package/templates/module-presets/files-quotas/packages/files-quotas/src/forgeon-files-quotas.module.ts +18 -18
- package/templates/module-presets/accounts/packages/accounts-api/src/accounts-persistence.port.ts +0 -67
- package/templates/module-presets/files/apps/api/src/files/forgeon-files-db-prisma.module.ts +0 -17
|
@@ -1,102 +1,7 @@
|
|
|
1
1
|
import { Readable } from 'node:stream';
|
|
2
|
-
import type { FileVariantKey } from './files.types';
|
|
3
2
|
|
|
4
|
-
export const FILES_PERSISTENCE_PORT = 'FORGEON_FILES_PERSISTENCE_PORT';
|
|
5
3
|
export const FILES_STORAGE_ADAPTER = 'FORGEON_FILES_STORAGE_ADAPTER';
|
|
6
4
|
|
|
7
|
-
export type FilesBlobRecord = {
|
|
8
|
-
id: string;
|
|
9
|
-
hash: string;
|
|
10
|
-
size: number;
|
|
11
|
-
mimeType: string;
|
|
12
|
-
storageDriver: string;
|
|
13
|
-
storageKey: string;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export type FilesBlobRef = FilesBlobRecord & {
|
|
17
|
-
created: boolean;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export type FilesRecordVariant = {
|
|
21
|
-
variantKey: string;
|
|
22
|
-
blobId: string;
|
|
23
|
-
mimeType: string;
|
|
24
|
-
size: number;
|
|
25
|
-
status: string;
|
|
26
|
-
blob?: {
|
|
27
|
-
storageDriver: string;
|
|
28
|
-
storageKey: string;
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export type FilesRecordAggregate = {
|
|
33
|
-
id: string;
|
|
34
|
-
publicId: string;
|
|
35
|
-
storageKey: string;
|
|
36
|
-
originalName: string;
|
|
37
|
-
mimeType: string;
|
|
38
|
-
size: number;
|
|
39
|
-
storageDriver: string;
|
|
40
|
-
ownerType: string;
|
|
41
|
-
ownerId: string | null;
|
|
42
|
-
visibility: string;
|
|
43
|
-
createdById: string | null;
|
|
44
|
-
createdAt: Date;
|
|
45
|
-
updatedAt: Date;
|
|
46
|
-
variants?: FilesRecordVariant[];
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export type FilesRecordCreateInput = {
|
|
50
|
-
publicId: string;
|
|
51
|
-
storageKey: string;
|
|
52
|
-
originalName: string;
|
|
53
|
-
mimeType: string;
|
|
54
|
-
size: number;
|
|
55
|
-
storageDriver: string;
|
|
56
|
-
ownerType: string;
|
|
57
|
-
ownerId: string | null;
|
|
58
|
-
visibility: string;
|
|
59
|
-
createdById: string | null;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export type FilesBlobCreateInput = {
|
|
63
|
-
hash: string;
|
|
64
|
-
size: number;
|
|
65
|
-
mimeType: string;
|
|
66
|
-
storageDriver: string;
|
|
67
|
-
storageKey: string;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
export type FilesVariantCreateInput = {
|
|
71
|
-
fileId: string;
|
|
72
|
-
variantKey: FileVariantKey;
|
|
73
|
-
blobId: string;
|
|
74
|
-
mimeType: string;
|
|
75
|
-
size: number;
|
|
76
|
-
status: string;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
export interface FilesPersistencePort {
|
|
80
|
-
createFileRecord(data: FilesRecordCreateInput): Promise<{
|
|
81
|
-
id: string;
|
|
82
|
-
publicId: string;
|
|
83
|
-
}>;
|
|
84
|
-
deleteFileRecordById(id: string): Promise<void>;
|
|
85
|
-
deleteFileRecordByPublicId(publicId: string): Promise<void>;
|
|
86
|
-
findFileRecordWithVariantKeys(publicId: string): Promise<FilesRecordAggregate | null>;
|
|
87
|
-
findFileRecordForDelete(publicId: string): Promise<FilesRecordAggregate | null>;
|
|
88
|
-
findFileRecordForDownload(publicId: string): Promise<FilesRecordAggregate | null>;
|
|
89
|
-
countOwnerUsage(ownerType: string, ownerId: string): Promise<{
|
|
90
|
-
filesCount: number;
|
|
91
|
-
totalBytes: number;
|
|
92
|
-
}>;
|
|
93
|
-
findBlobRef(hash: string, size: number, mimeType: string, storageDriver: string): Promise<FilesBlobRef | null>;
|
|
94
|
-
createBlob(data: FilesBlobCreateInput): Promise<FilesBlobRecord>;
|
|
95
|
-
createVariants(data: FilesVariantCreateInput[]): Promise<void>;
|
|
96
|
-
findBlobById(id: string): Promise<FilesBlobRecord | null>;
|
|
97
|
-
deleteBlobIfUnreferenced(id: string): Promise<boolean>;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
5
|
export interface FilesStorageAdapter {
|
|
101
6
|
readonly driver: string;
|
|
102
7
|
put(buffer: Buffer, fileName: string): Promise<{
|
|
@@ -5,22 +5,15 @@ import {
|
|
|
5
5
|
BadRequestException,
|
|
6
6
|
Inject,
|
|
7
7
|
Injectable,
|
|
8
|
-
InternalServerErrorException,
|
|
9
8
|
NotFoundException,
|
|
9
|
+
Optional,
|
|
10
10
|
ServiceUnavailableException,
|
|
11
11
|
} from '@nestjs/common';
|
|
12
12
|
import { FilesConfigService } from './files-config.service';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} from './files.
|
|
17
|
-
import type {
|
|
18
|
-
FilesBlobRecord,
|
|
19
|
-
FilesBlobRef,
|
|
20
|
-
FilesPersistencePort,
|
|
21
|
-
FilesRecordAggregate,
|
|
22
|
-
FilesStorageAdapter,
|
|
23
|
-
} from './files.ports';
|
|
13
|
+
import { FILES_STORAGE_ADAPTER } from './files.ports';
|
|
14
|
+
import type { FilesStorageAdapter } from './files.ports';
|
|
15
|
+
import { FilesStore } from './files.store';
|
|
16
|
+
import type { FilesBlobRef, FilesRecordAggregate } from './files.store';
|
|
24
17
|
import type { FileRecordDto, FileVariantKey, StoredFileInput } from './files.types';
|
|
25
18
|
|
|
26
19
|
type PrismaLikeError = {
|
|
@@ -44,10 +37,8 @@ type PersistedVariant = {
|
|
|
44
37
|
@Injectable()
|
|
45
38
|
export class FilesService {
|
|
46
39
|
constructor(
|
|
47
|
-
|
|
48
|
-
private readonly
|
|
49
|
-
@Inject(FILES_STORAGE_ADAPTER)
|
|
50
|
-
private readonly storageAdapter: FilesStorageAdapter,
|
|
40
|
+
private readonly filesStore: FilesStore,
|
|
41
|
+
@Optional() @Inject(FILES_STORAGE_ADAPTER) private readonly storageAdapter: FilesStorageAdapter | undefined,
|
|
51
42
|
private readonly filesConfigService: FilesConfigService,
|
|
52
43
|
) {}
|
|
53
44
|
|
|
@@ -69,7 +60,7 @@ export class FilesService {
|
|
|
69
60
|
createdBlobIds.push(originalBlob.id);
|
|
70
61
|
}
|
|
71
62
|
|
|
72
|
-
const record = await this.
|
|
63
|
+
const record = await this.filesStore.createFileRecord({
|
|
73
64
|
publicId: this.generatePublicId(),
|
|
74
65
|
storageKey: originalBlob.storageKey,
|
|
75
66
|
originalName: input.originalName,
|
|
@@ -110,7 +101,7 @@ export class FilesService {
|
|
|
110
101
|
persistedVariantBlobIds.push(previewBlob.id);
|
|
111
102
|
}
|
|
112
103
|
|
|
113
|
-
await this.
|
|
104
|
+
await this.filesStore.createVariants(
|
|
114
105
|
persistedVariants.map((item, index) => ({
|
|
115
106
|
fileId: record.id,
|
|
116
107
|
variantKey: item.variantKey,
|
|
@@ -124,7 +115,7 @@ export class FilesService {
|
|
|
124
115
|
return this.getByPublicId(record.publicId);
|
|
125
116
|
} catch (error) {
|
|
126
117
|
if (recordId) {
|
|
127
|
-
await this.
|
|
118
|
+
await this.filesStore.deleteFileRecordById(recordId).catch(() => undefined);
|
|
128
119
|
}
|
|
129
120
|
await this.cleanupCreatedBlobs(createdBlobIds);
|
|
130
121
|
throw error;
|
|
@@ -144,13 +135,13 @@ export class FilesService {
|
|
|
144
135
|
}
|
|
145
136
|
|
|
146
137
|
async deleteByPublicId(publicId: string): Promise<{ deleted: boolean }> {
|
|
147
|
-
const record = await this.
|
|
138
|
+
const record = await this.filesStore.findFileRecordForDelete(publicId);
|
|
148
139
|
if (!record) {
|
|
149
140
|
throw new NotFoundException('File not found');
|
|
150
141
|
}
|
|
151
142
|
|
|
152
143
|
const blobIds = (record.variants ?? []).map((variant) => variant.blobId);
|
|
153
|
-
await this.
|
|
144
|
+
await this.filesStore.deleteFileRecordByPublicId(publicId);
|
|
154
145
|
await this.cleanupReferencedBlobs(blobIds);
|
|
155
146
|
|
|
156
147
|
if ((record.variants ?? []).length === 0) {
|
|
@@ -161,7 +152,7 @@ export class FilesService {
|
|
|
161
152
|
}
|
|
162
153
|
|
|
163
154
|
async getByPublicId(publicId: string): Promise<FileRecordDto> {
|
|
164
|
-
const record = await this.
|
|
155
|
+
const record = await this.filesStore.findFileRecordWithVariantKeys(publicId);
|
|
165
156
|
if (!record) {
|
|
166
157
|
throw new NotFoundException('File not found');
|
|
167
158
|
}
|
|
@@ -169,7 +160,7 @@ export class FilesService {
|
|
|
169
160
|
}
|
|
170
161
|
|
|
171
162
|
async getOwnerUsage(ownerType: string, ownerId: string): Promise<{ filesCount: number; totalBytes: number }> {
|
|
172
|
-
return this.
|
|
163
|
+
return this.filesStore.countOwnerUsage(ownerType, ownerId);
|
|
173
164
|
}
|
|
174
165
|
|
|
175
166
|
async openDownload(publicId: string, variant: FileVariantKey = 'original'): Promise<{
|
|
@@ -177,7 +168,7 @@ export class FilesService {
|
|
|
177
168
|
mimeType: string;
|
|
178
169
|
fileName: string;
|
|
179
170
|
}> {
|
|
180
|
-
const record = await this.
|
|
171
|
+
const record = await this.filesStore.findFileRecordForDownload(publicId);
|
|
181
172
|
if (!record) {
|
|
182
173
|
throw new NotFoundException('File not found');
|
|
183
174
|
}
|
|
@@ -215,6 +206,7 @@ export class FilesService {
|
|
|
215
206
|
supportedVariants: FileVariantKey[];
|
|
216
207
|
previewGenerationEnabled: boolean;
|
|
217
208
|
}> {
|
|
209
|
+
this.requireStorageAdapter();
|
|
218
210
|
return {
|
|
219
211
|
status: 'ok',
|
|
220
212
|
feature: 'files-variants',
|
|
@@ -250,14 +242,26 @@ export class FilesService {
|
|
|
250
242
|
}
|
|
251
243
|
}
|
|
252
244
|
|
|
245
|
+
private requireStorageAdapter(): FilesStorageAdapter {
|
|
246
|
+
if (this.storageAdapter) {
|
|
247
|
+
return this.storageAdapter;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
throw new ServiceUnavailableException(
|
|
251
|
+
'Files storage adapter is not configured. Install/add a files-storage-adapter provider.',
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
253
255
|
private async store(buffer: Buffer, originalName: string): Promise<{ storageKey: string }> {
|
|
256
|
+
const storageAdapter = this.requireStorageAdapter();
|
|
254
257
|
const extension = path.extname(originalName).toLowerCase();
|
|
255
258
|
const fileName = `${Date.now()}-${crypto.randomUUID()}${extension}`;
|
|
256
|
-
return
|
|
259
|
+
return storageAdapter.put(buffer, fileName);
|
|
257
260
|
}
|
|
258
261
|
|
|
259
262
|
private async getOrCreateBlob(input: PreparedStoredFile, dedupe: boolean): Promise<FilesBlobRef> {
|
|
260
|
-
const
|
|
263
|
+
const storageAdapter = this.requireStorageAdapter();
|
|
264
|
+
const storageDriver = storageAdapter.driver;
|
|
261
265
|
const hash = this.computeContentHash(input.buffer);
|
|
262
266
|
|
|
263
267
|
if (dedupe) {
|
|
@@ -269,7 +273,7 @@ export class FilesService {
|
|
|
269
273
|
|
|
270
274
|
const stored = await this.store(input.buffer, input.fileName);
|
|
271
275
|
try {
|
|
272
|
-
const created = await this.
|
|
276
|
+
const created = await this.filesStore.createBlob({
|
|
273
277
|
hash,
|
|
274
278
|
size: input.size,
|
|
275
279
|
mimeType: input.mimeType,
|
|
@@ -321,21 +325,23 @@ export class FilesService {
|
|
|
321
325
|
}
|
|
322
326
|
|
|
323
327
|
private async openStoredContent(storageDriver: string, storageKey: string): Promise<Readable> {
|
|
324
|
-
|
|
328
|
+
const storageAdapter = this.requireStorageAdapter();
|
|
329
|
+
if (storageDriver !== storageAdapter.driver) {
|
|
325
330
|
throw new ServiceUnavailableException(
|
|
326
|
-
`File was stored with driver "${storageDriver}", but current adapter is "${
|
|
331
|
+
`File was stored with driver "${storageDriver}", but current adapter is "${storageAdapter.driver}".`,
|
|
327
332
|
);
|
|
328
333
|
}
|
|
329
|
-
return
|
|
334
|
+
return storageAdapter.open(storageKey);
|
|
330
335
|
}
|
|
331
336
|
|
|
332
337
|
private async deleteStoredContent(storageDriver: string, storageKey: string): Promise<void> {
|
|
333
|
-
|
|
338
|
+
const storageAdapter = this.requireStorageAdapter();
|
|
339
|
+
if (storageDriver !== storageAdapter.driver) {
|
|
334
340
|
throw new ServiceUnavailableException(
|
|
335
|
-
`File was stored with driver "${storageDriver}", but current adapter is "${
|
|
341
|
+
`File was stored with driver "${storageDriver}", but current adapter is "${storageAdapter.driver}".`,
|
|
336
342
|
);
|
|
337
343
|
}
|
|
338
|
-
await
|
|
344
|
+
await storageAdapter.delete(storageKey);
|
|
339
345
|
}
|
|
340
346
|
|
|
341
347
|
private generatePublicId(): string {
|
|
@@ -353,12 +359,12 @@ export class FilesService {
|
|
|
353
359
|
private async cleanupReferencedBlobs(blobIds: string[]): Promise<void> {
|
|
354
360
|
const uniqueIds = [...new Set(blobIds.filter(Boolean))];
|
|
355
361
|
for (const blobId of uniqueIds) {
|
|
356
|
-
const blob = await this.
|
|
362
|
+
const blob = await this.filesStore.findBlobById(blobId);
|
|
357
363
|
if (!blob) {
|
|
358
364
|
continue;
|
|
359
365
|
}
|
|
360
366
|
|
|
361
|
-
const deleted = await this.
|
|
367
|
+
const deleted = await this.filesStore.deleteBlobIfUnreferenced(blob.id);
|
|
362
368
|
if (!deleted) {
|
|
363
369
|
continue;
|
|
364
370
|
}
|
|
@@ -372,7 +378,7 @@ export class FilesService {
|
|
|
372
378
|
mimeType: string,
|
|
373
379
|
storageDriver: string,
|
|
374
380
|
): Promise<FilesBlobRef | null> {
|
|
375
|
-
const existing = await this.
|
|
381
|
+
const existing = await this.filesStore.findBlobRef(hash, size, mimeType, storageDriver);
|
|
376
382
|
if (!existing) {
|
|
377
383
|
return null;
|
|
378
384
|
}
|
|
@@ -446,3 +452,4 @@ export class FilesService {
|
|
|
446
452
|
return `${basePath}/${publicId}/download`;
|
|
447
453
|
}
|
|
448
454
|
}
|
|
455
|
+
|
|
@@ -1,17 +1,81 @@
|
|
|
1
|
-
import {
|
|
2
|
-
FILES_PERSISTENCE_PORT,
|
|
3
|
-
type FilesBlobCreateInput,
|
|
4
|
-
type FilesBlobRef,
|
|
5
|
-
type FilesPersistencePort,
|
|
6
|
-
type FilesRecordAggregate,
|
|
7
|
-
type FilesRecordCreateInput,
|
|
8
|
-
type FilesVariantCreateInput,
|
|
9
|
-
} from '@forgeon/files';
|
|
10
|
-
import { PrismaService } from '@forgeon/db-prisma';
|
|
11
1
|
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { PrismaService } from '@forgeon/db-prisma';
|
|
3
|
+
import type { FileVariantKey } from './files.types';
|
|
4
|
+
|
|
5
|
+
export type FilesBlobRecord = {
|
|
6
|
+
id: string;
|
|
7
|
+
hash: string;
|
|
8
|
+
size: number;
|
|
9
|
+
mimeType: string;
|
|
10
|
+
storageDriver: string;
|
|
11
|
+
storageKey: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type FilesBlobRef = FilesBlobRecord & {
|
|
15
|
+
created: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type FilesRecordVariant = {
|
|
19
|
+
variantKey: string;
|
|
20
|
+
blobId: string;
|
|
21
|
+
mimeType: string;
|
|
22
|
+
size: number;
|
|
23
|
+
status: string;
|
|
24
|
+
blob?: {
|
|
25
|
+
storageDriver: string;
|
|
26
|
+
storageKey: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type FilesRecordAggregate = {
|
|
31
|
+
id: string;
|
|
32
|
+
publicId: string;
|
|
33
|
+
storageKey: string;
|
|
34
|
+
originalName: string;
|
|
35
|
+
mimeType: string;
|
|
36
|
+
size: number;
|
|
37
|
+
storageDriver: string;
|
|
38
|
+
ownerType: string;
|
|
39
|
+
ownerId: string | null;
|
|
40
|
+
visibility: string;
|
|
41
|
+
createdById: string | null;
|
|
42
|
+
createdAt: Date;
|
|
43
|
+
updatedAt: Date;
|
|
44
|
+
variants?: FilesRecordVariant[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type FilesRecordCreateInput = {
|
|
48
|
+
publicId: string;
|
|
49
|
+
storageKey: string;
|
|
50
|
+
originalName: string;
|
|
51
|
+
mimeType: string;
|
|
52
|
+
size: number;
|
|
53
|
+
storageDriver: string;
|
|
54
|
+
ownerType: string;
|
|
55
|
+
ownerId: string | null;
|
|
56
|
+
visibility: string;
|
|
57
|
+
createdById: string | null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type FilesBlobCreateInput = {
|
|
61
|
+
hash: string;
|
|
62
|
+
size: number;
|
|
63
|
+
mimeType: string;
|
|
64
|
+
storageDriver: string;
|
|
65
|
+
storageKey: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type FilesVariantCreateInput = {
|
|
69
|
+
fileId: string;
|
|
70
|
+
variantKey: FileVariantKey;
|
|
71
|
+
blobId: string;
|
|
72
|
+
mimeType: string;
|
|
73
|
+
size: number;
|
|
74
|
+
status: string;
|
|
75
|
+
};
|
|
12
76
|
|
|
13
77
|
@Injectable()
|
|
14
|
-
export class
|
|
78
|
+
export class FilesStore {
|
|
15
79
|
constructor(private readonly prisma: PrismaService) {}
|
|
16
80
|
|
|
17
81
|
async createFileRecord(data: FilesRecordCreateInput): Promise<{ id: string; publicId: string }> {
|
|
@@ -136,7 +200,7 @@ export class PrismaFilesPersistenceStore implements FilesPersistencePort {
|
|
|
136
200
|
};
|
|
137
201
|
}
|
|
138
202
|
|
|
139
|
-
async createBlob(data: FilesBlobCreateInput) {
|
|
203
|
+
async createBlob(data: FilesBlobCreateInput): Promise<FilesBlobRecord> {
|
|
140
204
|
return this.prisma.fileBlob.create({ data });
|
|
141
205
|
}
|
|
142
206
|
|
|
@@ -144,7 +208,7 @@ export class PrismaFilesPersistenceStore implements FilesPersistencePort {
|
|
|
144
208
|
await this.prisma.fileVariant.createMany({ data });
|
|
145
209
|
}
|
|
146
210
|
|
|
147
|
-
async findBlobById(id: string) {
|
|
211
|
+
async findBlobById(id: string): Promise<FilesBlobRecord | null> {
|
|
148
212
|
return this.prisma.fileBlob.findUnique({
|
|
149
213
|
where: { id },
|
|
150
214
|
});
|
|
@@ -1,136 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DynamicModule,
|
|
3
|
-
Injectable,
|
|
4
3
|
Module,
|
|
5
4
|
ModuleMetadata,
|
|
6
|
-
Provider,
|
|
7
|
-
ServiceUnavailableException,
|
|
8
5
|
} from '@nestjs/common';
|
|
9
|
-
import
|
|
10
|
-
FilesBlobCreateInput,
|
|
11
|
-
FilesPersistencePort,
|
|
12
|
-
FilesRecordCreateInput,
|
|
13
|
-
FilesStorageAdapter,
|
|
14
|
-
FilesVariantCreateInput,
|
|
15
|
-
} from './files.ports';
|
|
16
|
-
import { FILES_PERSISTENCE_PORT, FILES_STORAGE_ADAPTER } from './files.ports';
|
|
6
|
+
import { DbPrismaModule } from '@forgeon/db-prisma';
|
|
17
7
|
import { FilesController } from './files.controller';
|
|
18
8
|
import { FilesConfigModule } from './files-config.module';
|
|
19
9
|
import { FilesService } from './files.service';
|
|
10
|
+
import { FilesStore } from './files.store';
|
|
20
11
|
|
|
21
12
|
export interface ForgeonFilesModuleOptions {
|
|
22
13
|
imports?: ModuleMetadata['imports'];
|
|
23
|
-
persistenceProvider?: Provider;
|
|
24
|
-
storageAdapterProvider?: Provider;
|
|
25
14
|
}
|
|
26
15
|
|
|
27
|
-
@
|
|
28
|
-
class MissingFilesPersistencePort implements FilesPersistencePort {
|
|
29
|
-
private unconfigured(): never {
|
|
30
|
-
throw new ServiceUnavailableException(
|
|
31
|
-
'Files persistence provider is not configured. Install/add a db-adapter provider and wire FILES_PERSISTENCE_PORT.',
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async createFileRecord(_data: FilesRecordCreateInput): Promise<{ id: string; publicId: string }> {
|
|
36
|
-
return this.unconfigured();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async deleteFileRecordById(_id: string): Promise<void> {
|
|
40
|
-
return this.unconfigured();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async deleteFileRecordByPublicId(_publicId: string): Promise<void> {
|
|
44
|
-
return this.unconfigured();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async findFileRecordWithVariantKeys(_publicId: string) {
|
|
48
|
-
return this.unconfigured();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async findFileRecordForDelete(_publicId: string) {
|
|
52
|
-
return this.unconfigured();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async findFileRecordForDownload(_publicId: string) {
|
|
56
|
-
return this.unconfigured();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async countOwnerUsage(_ownerType: string, _ownerId: string) {
|
|
60
|
-
return this.unconfigured();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async findBlobRef(_hash: string, _size: number, _mimeType: string, _storageDriver: string) {
|
|
64
|
-
return this.unconfigured();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async createBlob(_data: FilesBlobCreateInput) {
|
|
68
|
-
return this.unconfigured();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async createVariants(_data: FilesVariantCreateInput[]): Promise<void> {
|
|
72
|
-
return this.unconfigured();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async findBlobById(_id: string) {
|
|
76
|
-
return this.unconfigured();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async deleteBlobIfUnreferenced(_id: string) {
|
|
80
|
-
return this.unconfigured();
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
@Injectable()
|
|
85
|
-
class MissingFilesStorageAdapter implements FilesStorageAdapter {
|
|
86
|
-
readonly driver = 'unconfigured';
|
|
87
|
-
|
|
88
|
-
private unconfigured(): never {
|
|
89
|
-
throw new ServiceUnavailableException(
|
|
90
|
-
'Files storage adapter is not configured. Install/add a files-storage-adapter provider and wire FILES_STORAGE_ADAPTER.',
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async put(_buffer: Buffer, _fileName: string): Promise<{ storageKey: string }> {
|
|
95
|
-
return this.unconfigured();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async open(_storageKey: string) {
|
|
99
|
-
return this.unconfigured();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async delete(_storageKey: string): Promise<void> {
|
|
103
|
-
return this.unconfigured();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
@Module({
|
|
108
|
-
imports: [FilesConfigModule],
|
|
109
|
-
controllers: [FilesController],
|
|
110
|
-
providers: [FilesService],
|
|
111
|
-
exports: [FilesConfigModule, FilesService],
|
|
112
|
-
})
|
|
16
|
+
@Module({})
|
|
113
17
|
export class ForgeonFilesModule {
|
|
114
18
|
static register(options: ForgeonFilesModuleOptions = {}): DynamicModule {
|
|
115
|
-
const persistenceProvider =
|
|
116
|
-
options.persistenceProvider ??
|
|
117
|
-
({
|
|
118
|
-
provide: FILES_PERSISTENCE_PORT,
|
|
119
|
-
useClass: MissingFilesPersistencePort,
|
|
120
|
-
} satisfies Provider);
|
|
121
|
-
|
|
122
|
-
const storageAdapterProvider =
|
|
123
|
-
options.storageAdapterProvider ??
|
|
124
|
-
({
|
|
125
|
-
provide: FILES_STORAGE_ADAPTER,
|
|
126
|
-
useClass: MissingFilesStorageAdapter,
|
|
127
|
-
} satisfies Provider);
|
|
128
|
-
|
|
129
19
|
return {
|
|
130
20
|
module: ForgeonFilesModule,
|
|
131
|
-
imports: [...(options.imports ?? [])],
|
|
132
|
-
|
|
133
|
-
|
|
21
|
+
imports: [FilesConfigModule, DbPrismaModule, ...(options.imports ?? [])],
|
|
22
|
+
controllers: [FilesController],
|
|
23
|
+
providers: [FilesStore, FilesService],
|
|
24
|
+
exports: [FilesConfigModule, FilesStore, FilesService],
|
|
134
25
|
};
|
|
135
26
|
}
|
|
136
27
|
}
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@forgeon/files-quotas",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"build": "tsc -p tsconfig.json"
|
|
9
|
-
},
|
|
10
|
-
"dependencies": {
|
|
11
|
-
"@forgeon/files": "workspace:*",
|
|
12
|
-
"@nestjs/common": "^11.0.1",
|
|
13
|
-
"@nestjs/config": "^4.0.0",
|
|
14
|
-
"zod": "^3.24.2"
|
|
15
|
-
},
|
|
16
|
-
"devDependencies": {
|
|
17
|
-
"@types/node": "^22.10.7",
|
|
18
|
-
"typescript": "^5.7.3"
|
|
19
|
-
}
|
|
20
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgeon/files-quotas",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -p tsconfig.json"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@forgeon/files": "workspace:*",
|
|
12
|
+
"@nestjs/common": "^11.0.1",
|
|
13
|
+
"@nestjs/config": "^4.0.0",
|
|
14
|
+
"zod": "^3.24.2"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^22.10.7",
|
|
18
|
+
"typescript": "^5.7.3"
|
|
19
|
+
}
|
|
20
|
+
}
|