@tomei/media 0.8.11-dev.1 → 0.10.1-test.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.
Files changed (41) hide show
  1. package/.gitlab-ci.yml +231 -13
  2. package/.husky/commit-msg +1 -1
  3. package/.husky/pre-commit +0 -0
  4. package/.vscode/settings.json +3 -0
  5. package/dist/__tests__/common.service.spec.d.ts +1 -0
  6. package/dist/__tests__/common.service.spec.js +160 -0
  7. package/dist/__tests__/common.service.spec.js.map +1 -0
  8. package/dist/__tests__/medias.repository.spec.d.ts +1 -0
  9. package/dist/__tests__/medias.repository.spec.js +140 -0
  10. package/dist/__tests__/medias.repository.spec.js.map +1 -0
  11. package/dist/__tests__/medias.spec.d.ts +1 -0
  12. package/dist/__tests__/medias.spec.js +347 -0
  13. package/dist/__tests__/medias.spec.js.map +1 -0
  14. package/dist/__tests__/pipes.spec.d.ts +1 -0
  15. package/dist/__tests__/pipes.spec.js +130 -0
  16. package/dist/__tests__/pipes.spec.js.map +1 -0
  17. package/dist/base/base.medias.js +2 -2
  18. package/dist/base/base.medias.js.map +1 -1
  19. package/dist/common/common.service.js +2 -2
  20. package/dist/common/common.service.js.map +1 -1
  21. package/dist/medias.d.ts +2 -2
  22. package/dist/medias.js +16 -10
  23. package/dist/medias.js.map +1 -1
  24. package/dist/medias.repository.js.map +1 -1
  25. package/dist/pipe/append-id.pipe.js +2 -2
  26. package/dist/pipe/append-id.pipe.js.map +1 -1
  27. package/dist/pipe/validate-id.pipe.js +2 -2
  28. package/dist/pipe/validate-id.pipe.js.map +1 -1
  29. package/dist/tsconfig.tsbuildinfo +1 -1
  30. package/jest.config.js +26 -0
  31. package/package.json +38 -34
  32. package/src/__tests__/common.service.spec.ts +203 -0
  33. package/src/__tests__/medias.repository.spec.ts +158 -0
  34. package/src/__tests__/medias.spec.ts +468 -0
  35. package/src/__tests__/pipes.spec.ts +154 -0
  36. package/src/base/base.medias.ts +2 -2
  37. package/src/common/common.service.ts +2 -2
  38. package/src/medias.repository.ts +9 -3
  39. package/src/medias.ts +20 -15
  40. package/src/pipe/append-id.pipe.ts +2 -2
  41. package/src/pipe/validate-id.pipe.ts +2 -2
@@ -0,0 +1,468 @@
1
+ // Environment variables must be set before any module is loaded
2
+ process.env.MEDIA_STORAGE_TYPE = 'local';
3
+ process.env.MEDIA_LOCAL_STORAGE_PATH = '/tmp/media-test';
4
+ process.env.MEDIA_ENCRYPT_KEY = '12345678901234567890123456789012'; // 32 bytes for AES-256
5
+
6
+ jest.mock('@tomei/general', () => ({
7
+ ObjectBase: class ObjectBase {},
8
+ }));
9
+
10
+ jest.mock('@nestjs/swagger', () => ({
11
+ ApiProperty: () => () => {},
12
+ ApiPropertyOptional: () => () => {},
13
+ }));
14
+
15
+ jest.mock('sequelize-typescript', () => ({
16
+ Table: () => () => {},
17
+ Column: () => () => {},
18
+ Model: class Model {
19
+ static findByPk = jest.fn();
20
+ static findOne = jest.fn();
21
+ static findAll = jest.fn();
22
+ static findAndCountAll = jest.fn();
23
+ static create = jest.fn();
24
+ get = jest.fn();
25
+ update = jest.fn();
26
+ destroy = jest.fn();
27
+ },
28
+ DataType: {
29
+ STRING: 'STRING',
30
+ TEXT: 'TEXT',
31
+ DATE: 'DATE',
32
+ BOOLEAN: 'BOOLEAN',
33
+ ENUM: () => 'ENUM',
34
+ },
35
+ PrimaryKey: () => () => {},
36
+ ForeignKey: () => () => {},
37
+ BelongsTo: () => () => {},
38
+ HasMany: () => () => {},
39
+ CreatedAt: () => () => {},
40
+ UpdatedAt: () => () => {},
41
+ Default: () => () => {},
42
+ AllowNull: () => () => {},
43
+ }));
44
+
45
+ jest.mock('@nestjs/common', () => ({
46
+ BadRequestException: class BadRequestException extends Error {
47
+ constructor(msg: string) {
48
+ super(msg);
49
+ this.name = 'BadRequestException';
50
+ }
51
+ },
52
+ NotFoundException: class NotFoundException extends Error {
53
+ constructor(msg: string) {
54
+ super(msg);
55
+ this.name = 'NotFoundException';
56
+ }
57
+ },
58
+ Injectable: () => (t: any) => t,
59
+ }));
60
+
61
+ jest.mock('@tomei/activity-history', () => ({
62
+ Activity: jest.fn().mockImplementation(() => ({
63
+ create: jest.fn().mockResolvedValue(undefined),
64
+ createId: jest.fn().mockReturnValue('activity-id'),
65
+ ActivityId: '',
66
+ Action: '',
67
+ Description: '',
68
+ EntityType: '',
69
+ EntityId: '',
70
+ EntityValueBefore: '',
71
+ EntityValueAfter: '',
72
+ })),
73
+ ActionEnum: { CREATE: 'Create', UPDATE: 'Update', DELETE: 'Delete' },
74
+ }));
75
+
76
+ jest.mock('@tomei/sso', () => ({
77
+ LoginUser: jest.fn(),
78
+ }));
79
+
80
+ jest.mock('@paralleldrive/cuid2', () => ({
81
+ createId: jest.fn().mockReturnValue('test-media-id'),
82
+ }));
83
+
84
+ // Variables prefixed with "mock" are hoisted by ts-jest alongside jest.mock()
85
+ const mockRepoMethods = {
86
+ create: jest.fn(),
87
+ findOne: jest.fn(),
88
+ findAll: jest.fn(),
89
+ findAndCountAll: jest.fn(),
90
+ };
91
+
92
+ jest.mock('../medias.repository', () => ({
93
+ MediasRepository: jest.fn().mockImplementation(() => mockRepoMethods),
94
+ }));
95
+
96
+ const mockUploadData = jest.fn().mockResolvedValue({});
97
+ const mockDeleteIfExists = jest.fn().mockResolvedValue({});
98
+ const mockDownload = jest.fn();
99
+ const mockGetBlockBlobClient = jest.fn().mockReturnValue({
100
+ uploadData: mockUploadData,
101
+ download: mockDownload,
102
+ deleteIfExists: mockDeleteIfExists,
103
+ });
104
+ const mockGetContainerClient = jest.fn().mockReturnValue({
105
+ getBlockBlobClient: mockGetBlockBlobClient,
106
+ });
107
+
108
+ jest.mock('@azure/storage-blob', () => ({
109
+ BlobServiceClient: {
110
+ fromConnectionString: jest.fn().mockReturnValue({
111
+ getContainerClient: mockGetContainerClient,
112
+ }),
113
+ },
114
+ }));
115
+
116
+ jest.mock('sequelize', () => ({
117
+ Op: { substring: Symbol('Op.substring') },
118
+ }));
119
+
120
+ jest.mock('fs');
121
+
122
+ import * as fs from 'fs';
123
+ import { Medias } from '../medias';
124
+ import { MediaType } from '../enum/medias.enum';
125
+ import { Readable } from 'stream';
126
+
127
+ const mockedFs = fs as jest.Mocked<typeof fs>;
128
+
129
+ const makeLoginUser = () => ({ ObjectId: 'user-001' }) as any;
130
+
131
+ const makeMediaAttr = (overrides: Partial<any> = {}) => ({
132
+ MediaId: 'media-001',
133
+ ObjectId: 'obj-001',
134
+ ObjectType: 'Rental',
135
+ Title: 'Test File',
136
+ Description: 'A test file',
137
+ Type: MediaType.Photo,
138
+ IsExternalYN: 'N',
139
+ ExternalSource: '',
140
+ IsEncryptedYN: 'N',
141
+ FileName: 'testfile',
142
+ FileExtension: 'jpg',
143
+ FilePath: '/tmp/media-test/Rental/obj-001/testfile.jpg',
144
+ URL: '/tmp/media-test/Rental/obj-001',
145
+ CreatedById: 'user-001',
146
+ CreatedAt: new Date('2026-01-01'),
147
+ UpdatedById: 'user-001',
148
+ UpdatedAt: new Date('2026-01-01'),
149
+ ...overrides,
150
+ });
151
+
152
+ const makeMockModel = (attr: any = makeMediaAttr()) => ({
153
+ get: jest.fn().mockReturnValue(attr),
154
+ update: jest.fn().mockResolvedValue({ get: jest.fn().mockReturnValue(attr) }),
155
+ destroy: jest.fn().mockResolvedValue(undefined),
156
+ CollectionId: 1,
157
+ ...attr,
158
+ });
159
+
160
+ describe('Medias', () => {
161
+ beforeEach(() => {
162
+ jest.clearAllMocks();
163
+ mockRepoMethods.create.mockResolvedValue(makeMockModel());
164
+ mockRepoMethods.findOne.mockResolvedValue(makeMockModel());
165
+ mockRepoMethods.findAll.mockResolvedValue([makeMockModel()]);
166
+ mockRepoMethods.findAndCountAll.mockResolvedValue({
167
+ count: 1,
168
+ rows: [makeMockModel()],
169
+ });
170
+ });
171
+
172
+ // ── Constructor ────────────────────────────────────────────────────────────
173
+
174
+ describe('constructor', () => {
175
+ it('creates an instance without media attributes', () => {
176
+ const medias = new Medias();
177
+ expect(medias).toBeInstanceOf(Medias);
178
+ });
179
+
180
+ it('creates an instance with media attributes', () => {
181
+ const medias = new Medias(undefined, makeMediaAttr());
182
+ expect(medias.Title).toBe('Test File');
183
+ // ObjectType class field 'Media' is applied after super()/init(), so it takes precedence
184
+ expect(medias.ObjectType).toBe('Media');
185
+ });
186
+
187
+ it('throws when storage type is azure but container is missing', () => {
188
+ process.env.MEDIA_STORAGE_TYPE = 'azure';
189
+ delete process.env.MEDIA_AZUREBLOB_CONTAINER_NAME;
190
+ expect(() => new Medias()).toThrow();
191
+ process.env.MEDIA_STORAGE_TYPE = 'local';
192
+ });
193
+ });
194
+
195
+ // ── BaseMedias: createSaveLocation ─────────────────────────────────────────
196
+
197
+ describe('createSaveLocation', () => {
198
+ it('returns local path when MEDIA_STORAGE_TYPE is local', () => {
199
+ const medias = new Medias(undefined, makeMediaAttr());
200
+ const location = medias.createSaveLocation();
201
+ // ObjectType class field overrides init(), so ObjectType is 'Media'
202
+ expect(location).toBe('/tmp/media-test/Media/obj-001');
203
+ });
204
+
205
+ it('throws when no storage path env var is set', () => {
206
+ const saved = process.env.MEDIA_LOCAL_STORAGE_PATH;
207
+ delete process.env.MEDIA_LOCAL_STORAGE_PATH;
208
+ const medias = new Medias();
209
+ medias.ObjectType = 'Test';
210
+ medias.ObjectId = 'id';
211
+ expect(() => medias.createSaveLocation()).toThrow();
212
+ process.env.MEDIA_LOCAL_STORAGE_PATH = saved;
213
+ });
214
+ });
215
+
216
+ // ── encrypt / decrypt ──────────────────────────────────────────────────────
217
+
218
+ describe('encrypt', () => {
219
+ it('returns an encrypted buffer with iv', async () => {
220
+ const medias = new Medias();
221
+ const plaintext = Buffer.from('hello world');
222
+ const result = await medias.encrypt(plaintext);
223
+ expect(result.isEncrypted).toBe(true);
224
+ expect(result.buffer).not.toEqual(plaintext);
225
+ expect(result.iv).toHaveLength(16);
226
+ });
227
+
228
+ it('throws when MEDIA_ENCRYPT_KEY is not set', async () => {
229
+ const saved = process.env.MEDIA_ENCRYPT_KEY;
230
+ delete process.env.MEDIA_ENCRYPT_KEY;
231
+ const medias = new Medias();
232
+ await expect(medias.encrypt(Buffer.from('test'))).rejects.toThrow();
233
+ process.env.MEDIA_ENCRYPT_KEY = saved;
234
+ });
235
+ });
236
+
237
+ describe('decrypt', () => {
238
+ it('round-trips correctly with encrypt', async () => {
239
+ const medias = new Medias();
240
+ const plaintext = Buffer.from('secret message');
241
+ const encrypted = await medias.encrypt(plaintext);
242
+ const decrypted = await medias.decrypt(encrypted);
243
+ expect(decrypted.buffer.toString()).toBe('secret message');
244
+ expect(decrypted.isEncrypted).toBe(false);
245
+ });
246
+ });
247
+
248
+ // ── getAll ─────────────────────────────────────────────────────────────────
249
+
250
+ describe('getAll', () => {
251
+ it('returns paginated results', async () => {
252
+ const medias = new Medias();
253
+ const result = await medias.getAll(10, 1, '');
254
+ expect(result).toEqual({ count: 1, rows: expect.any(Array) });
255
+ expect(mockRepoMethods.findAndCountAll).toHaveBeenCalled();
256
+ });
257
+
258
+ it('applies search filter when search string is provided', async () => {
259
+ const medias = new Medias();
260
+ await medias.getAll(10, 1, '{"Title":"test"}');
261
+ expect(mockRepoMethods.findAndCountAll).toHaveBeenCalledWith(
262
+ expect.objectContaining({ where: expect.any(Object) }),
263
+ );
264
+ });
265
+
266
+ it('throws BadRequestException for invalid search JSON', async () => {
267
+ const medias = new Medias();
268
+ await expect(medias.getAll(10, 1, '{bad json')).rejects.toThrow();
269
+ });
270
+ });
271
+
272
+ // ── getOne ─────────────────────────────────────────────────────────────────
273
+
274
+ describe('getOne', () => {
275
+ it('returns the media model when found', async () => {
276
+ const medias = new Medias();
277
+ const result = await medias.getOne('media-001');
278
+ expect(result).toBeDefined();
279
+ });
280
+
281
+ it('throws NotFoundException when not found', async () => {
282
+ mockRepoMethods.findOne.mockResolvedValue(null);
283
+ const medias = new Medias();
284
+ await expect(medias.getOne('missing-id')).rejects.toThrow();
285
+ });
286
+ });
287
+
288
+ // ── getFile ────────────────────────────────────────────────────────────────
289
+
290
+ describe('getFile', () => {
291
+ it('returns file buffer and response options for a local unencrypted file', async () => {
292
+ const fileContent = Buffer.from('file-data');
293
+ const stream = Readable.from([fileContent]);
294
+ mockedFs.createReadStream = jest.fn().mockReturnValue(stream);
295
+
296
+ const medias = new Medias();
297
+ const result = await medias.getFile('media-001');
298
+
299
+ expect(result).toHaveProperty('fileBuffer');
300
+ expect(result).toHaveProperty('resOption');
301
+ expect(result.resOption['Content-Type']).toBe('application/octet-stream');
302
+ });
303
+
304
+ it('throws NotFoundException when media record is not found', async () => {
305
+ mockRepoMethods.findOne.mockResolvedValue(null);
306
+ const medias = new Medias();
307
+ await expect(medias.getFile('missing-id')).rejects.toThrow();
308
+ });
309
+ });
310
+
311
+ // ── postExternal ───────────────────────────────────────────────────────────
312
+
313
+ describe('postExternal', () => {
314
+ it('creates an external media record', async () => {
315
+ const loginUser = makeLoginUser();
316
+ const dto = {
317
+ ObjectId: 'obj-001',
318
+ ObjectType: 'Rental',
319
+ Title: 'External Image',
320
+ Description: 'Hosted externally',
321
+ Type: MediaType.Photo,
322
+ IsEncryptedYN: 'N',
323
+ ExternalSource: 'http://example.com/img.jpg',
324
+ URL: 'http://example.com/img.jpg',
325
+ } as any;
326
+
327
+ const medias = new Medias();
328
+ const result = await medias.postExternal(dto, loginUser);
329
+ expect(result.IsExternalYN).toBe('Y');
330
+ expect(result.MediaId).toBeDefined();
331
+ });
332
+ });
333
+
334
+ // ── postInternal ───────────────────────────────────────────────────────────
335
+
336
+ describe('postInternal', () => {
337
+ it('creates an internal media record for local storage', async () => {
338
+ const loginUser = makeLoginUser();
339
+ const fileStream = { buffer: Buffer.from('file-bytes') } as any;
340
+ const dto = {
341
+ ObjectId: 'obj-001',
342
+ ObjectType: 'Rental',
343
+ Title: 'Internal File',
344
+ Description: 'Stored locally',
345
+ Type: MediaType.Document,
346
+ IsEncryptedYN: 'N',
347
+ FileName: 'report',
348
+ FileExtension: 'pdf',
349
+ } as any;
350
+
351
+ mockedFs.exists = jest.fn((_p, cb) => cb(null, true)) as any;
352
+ mockedFs.writeFile = jest.fn((_p, _d, cb) => cb(null)) as any;
353
+
354
+ const medias = new Medias();
355
+ const result = await medias.postInternal(fileStream, dto, loginUser);
356
+ expect(result).toBeDefined();
357
+ expect(mockRepoMethods.create).toHaveBeenCalled();
358
+ });
359
+ });
360
+
361
+ // ── remove ─────────────────────────────────────────────────────────────────
362
+
363
+ describe('remove', () => {
364
+ it('deletes the media and returns success message', async () => {
365
+ mockedFs.existsSync = jest.fn().mockReturnValue(false);
366
+
367
+ const medias = new Medias();
368
+ const loginUser = makeLoginUser();
369
+ const result = await medias.remove('media-001', loginUser);
370
+ expect(result).toEqual({ message: 'Media has been deleted.' });
371
+ });
372
+
373
+ it('throws NotFoundException when media is not found', async () => {
374
+ mockRepoMethods.findOne.mockResolvedValue(null);
375
+ const medias = new Medias();
376
+ await expect(medias.remove('bad-id', makeLoginUser())).rejects.toThrow();
377
+ });
378
+ });
379
+
380
+ // ── putExternal ────────────────────────────────────────────────────────────
381
+
382
+ describe('putExternal', () => {
383
+ it('updates an external media record', async () => {
384
+ // putExternal passes null loginUser to updateMedias; make model.update()
385
+ // return null so the activity block (which reads loginUser.ObjectId) is skipped
386
+ const nullUpdateModel = {
387
+ ...makeMockModel(),
388
+ update: jest.fn().mockResolvedValue(null),
389
+ };
390
+ mockRepoMethods.findOne.mockResolvedValue(nullUpdateModel);
391
+ const medias = new Medias();
392
+ const result = await medias.putExternal(
393
+ 'media-001',
394
+ { Title: 'Updated Title' } as any,
395
+ 'user-001',
396
+ );
397
+ expect(result).toBeNull();
398
+ });
399
+
400
+ it('throws NotFoundException when not found', async () => {
401
+ mockRepoMethods.findOne.mockResolvedValue(null);
402
+ const medias = new Medias();
403
+ await expect(
404
+ medias.putExternal('bad-id', {} as any, 'user-001'),
405
+ ).rejects.toThrow();
406
+ });
407
+ });
408
+
409
+ // ── putInternal ────────────────────────────────────────────────────────────
410
+
411
+ describe('putInternal', () => {
412
+ it('updates an internal media record for local storage', async () => {
413
+ const loginUser = makeLoginUser();
414
+ const fileStream = { buffer: Buffer.from('updated-bytes') } as any;
415
+
416
+ mockedFs.exists = jest.fn((_p, cb) => cb(null, true)) as any;
417
+ mockedFs.unlinkSync = jest.fn();
418
+ mockedFs.writeFile = jest.fn((_p, _d, cb) => cb(null)) as any;
419
+
420
+ const medias = new Medias();
421
+ const result = await medias.putInternal(
422
+ fileStream,
423
+ 'media-001',
424
+ { FileName: 'newfile', FileExtension: 'pdf' } as any,
425
+ loginUser,
426
+ );
427
+ expect(result).toBeDefined();
428
+ });
429
+
430
+ it('throws NotFoundException when not found', async () => {
431
+ mockRepoMethods.findOne.mockResolvedValue(null);
432
+ const medias = new Medias();
433
+ await expect(
434
+ medias.putInternal({} as any, 'bad-id', {} as any, makeLoginUser()),
435
+ ).rejects.toThrow();
436
+ });
437
+ });
438
+
439
+ // ── findFile (Azure fallback) ───────────────────────────────────────────────
440
+
441
+ describe('findFile via Azure storage', () => {
442
+ beforeEach(() => {
443
+ process.env.MEDIA_STORAGE_TYPE = 'azure';
444
+ process.env.MEDIA_AZUREBLOB_CONTAINER_NAME = 'test-container';
445
+ });
446
+
447
+ afterEach(() => {
448
+ process.env.MEDIA_STORAGE_TYPE = 'local';
449
+ delete process.env.MEDIA_AZUREBLOB_CONTAINER_NAME;
450
+ });
451
+
452
+ it('downloads a file from Azure blob storage', async () => {
453
+ const fileBuffer = Buffer.from('azure-file-content');
454
+ const readable = Readable.from([fileBuffer]);
455
+ mockDownload.mockResolvedValue({ readableStreamBody: readable });
456
+
457
+ const medias = new Medias(
458
+ undefined,
459
+ makeMediaAttr({ URL: '/azure/Rental/obj-001' }),
460
+ );
461
+ medias.IsEncryptedYN = 'N';
462
+
463
+ const result = await medias.findFile();
464
+ expect(result).toBeInstanceOf(Buffer);
465
+ expect(mockGetContainerClient).toHaveBeenCalled();
466
+ });
467
+ });
468
+ });
@@ -0,0 +1,154 @@
1
+ jest.mock('@nestjs/common', () => ({
2
+ BadRequestException: class BadRequestException extends Error {
3
+ constructor(message: string) {
4
+ super(message);
5
+ this.name = 'BadRequestException';
6
+ }
7
+ },
8
+ Injectable: () => (target: any) => target,
9
+ PipeTransform: class {},
10
+ }));
11
+
12
+ jest.mock('class-validator', () => ({
13
+ isDefined: (v: any) => v !== undefined && v !== null,
14
+ isEnum: (value: any, entity: any) => Object.values(entity).includes(value),
15
+ }));
16
+
17
+ jest.mock('@paralleldrive/cuid2', () => ({
18
+ createId: jest.fn().mockReturnValue('generated-cuid'),
19
+ isCuid: jest.fn((v: string) => v === 'valid-cuid-value'),
20
+ }));
21
+
22
+ import { AppendIdPipe } from '../pipe/append-id.pipe';
23
+ import { ValidateEnumPipe } from '../pipe/validate-enum.pipe';
24
+ import { ValidateIdPipe } from '../pipe/validate-id.pipe';
25
+ import { ValidateSearchPipe } from '../pipe/validate-search.pipe';
26
+
27
+ enum TestEnum {
28
+ Photo = 'Photo',
29
+ Video = 'Video',
30
+ Document = 'Document',
31
+ }
32
+
33
+ describe('AppendIdPipe', () => {
34
+ let pipe: AppendIdPipe;
35
+
36
+ beforeEach(() => {
37
+ pipe = new AppendIdPipe('MediaId');
38
+ });
39
+
40
+ it('appends the named id field to the value', () => {
41
+ const result = pipe.transform({ Title: 'Test' });
42
+ expect(result).toEqual({ Title: 'Test', MediaId: 'generated-cuid' });
43
+ });
44
+
45
+ it('preserves existing fields', () => {
46
+ const result = pipe.transform({ a: 1, b: 2 });
47
+ expect(result.a).toBe(1);
48
+ expect(result.b).toBe(2);
49
+ expect(result.MediaId).toBe('generated-cuid');
50
+ });
51
+
52
+ it('works with an empty object', () => {
53
+ const result = pipe.transform({});
54
+ expect(result).toHaveProperty('MediaId', 'generated-cuid');
55
+ });
56
+ });
57
+
58
+ describe('ValidateEnumPipe', () => {
59
+ let pipe: ValidateEnumPipe;
60
+
61
+ beforeEach(() => {
62
+ pipe = new ValidateEnumPipe(TestEnum);
63
+ });
64
+
65
+ it('returns the enum value when valid', () => {
66
+ const result = pipe.transform('Photo');
67
+ expect(result).toBe('Photo');
68
+ });
69
+
70
+ it('throws BadRequestException for an invalid enum value', () => {
71
+ expect(() => pipe.transform('Invalid')).toThrow();
72
+ });
73
+
74
+ it('returns the original value when undefined', () => {
75
+ const result = pipe.transform(undefined as any);
76
+ expect(result).toBeUndefined();
77
+ });
78
+
79
+ it('validates each item when value is an array', () => {
80
+ const result = pipe.transform(['Photo', 'Video'] as any);
81
+ expect(result).toEqual(['Photo', 'Video']);
82
+ });
83
+
84
+ it('throws when an array contains an invalid value', () => {
85
+ expect(() => pipe.transform(['Photo', 'Bad'] as any)).toThrow();
86
+ });
87
+ });
88
+
89
+ describe('ValidateIdPipe', () => {
90
+ let pipe: ValidateIdPipe;
91
+
92
+ beforeEach(() => {
93
+ pipe = new ValidateIdPipe();
94
+ });
95
+
96
+ it('passes through a valid cuid', () => {
97
+ const result = pipe.transform('valid-cuid-value');
98
+ expect(result).toBe('valid-cuid-value');
99
+ });
100
+
101
+ it('throws BadRequestException for an invalid id', () => {
102
+ expect(() => pipe.transform('not-a-cuid')).toThrow();
103
+ });
104
+
105
+ it('passes through when value is falsy (optional id)', () => {
106
+ const result = pipe.transform(undefined as any);
107
+ expect(result).toBeUndefined();
108
+ });
109
+
110
+ it('uses a custom error message', () => {
111
+ const customPipe = new ValidateIdPipe('Custom error');
112
+ try {
113
+ customPipe.transform('not-a-cuid');
114
+ } catch (e: any) {
115
+ expect(e.message).toBe('Custom error');
116
+ }
117
+ });
118
+ });
119
+
120
+ describe('ValidateSearchPipe', () => {
121
+ const mockModel = {
122
+ tableAttributes: { MediaId: {}, Title: {}, Type: {} },
123
+ };
124
+
125
+ let pipe: ValidateSearchPipe;
126
+
127
+ beforeEach(() => {
128
+ pipe = new ValidateSearchPipe(mockModel);
129
+ });
130
+
131
+ it('returns value when search is valid JSON with valid keys', () => {
132
+ const search = JSON.stringify({ Title: 'test' });
133
+ expect(pipe.transform(search)).toBe(search);
134
+ });
135
+
136
+ it('returns undefined when value is falsy', () => {
137
+ expect(pipe.transform(undefined as any)).toBeUndefined();
138
+ });
139
+
140
+ it('throws BadRequestException for invalid JSON', () => {
141
+ expect(() => pipe.transform('{bad json')).toThrow();
142
+ });
143
+
144
+ it('throws BadRequestException for unknown search key', () => {
145
+ const search = JSON.stringify({ UnknownField: 'value' });
146
+ expect(() => pipe.transform(search)).toThrow();
147
+ });
148
+
149
+ it('allows additionalAttributes alongside model attributes', () => {
150
+ const pipeWithExtra = new ValidateSearchPipe(mockModel, ['ExtraField']);
151
+ const search = JSON.stringify({ ExtraField: 'value' });
152
+ expect(pipeWithExtra.transform(search)).toBe(search);
153
+ });
154
+ });
@@ -1,5 +1,5 @@
1
1
  import { MediaType } from '../enum/medias.enum';
2
- import * as cuid from 'cuid';
2
+ import { createId } from '@paralleldrive/cuid2';
3
3
  import { BadRequestException } from '@nestjs/common';
4
4
  import { MediasModel } from '../entities/medias.entity';
5
5
  import { IBaseMediasAttr } from '../interfaces/base.medias-attr.interface';
@@ -36,7 +36,7 @@ export abstract class BaseMedias extends ObjectBase {
36
36
  }
37
37
 
38
38
  init(media: IBaseMediasAttr) {
39
- this.MediaId = media.MediaId ? media.MediaId : cuid();
39
+ this.MediaId = media.MediaId ? media.MediaId : createId();
40
40
  this.ObjectId = media.ObjectId;
41
41
  this.ObjectType = media.ObjectType;
42
42
  this.Title = media.Title;
@@ -5,7 +5,7 @@ import {
5
5
  } from '@nestjs/common';
6
6
  import { ConfigService } from '@nestjs/config';
7
7
  import axios from 'axios';
8
- import * as cuid from 'cuid';
8
+ import { createId } from '@paralleldrive/cuid2';
9
9
  import { omit, omitBy, keys, pick } from 'lodash';
10
10
  import { AddFieldTranslationDto } from './dto/add-field-translation.dto';
11
11
  import { GetMediaDto } from './dto/get-media.dto';
@@ -61,7 +61,7 @@ export class CommonService {
61
61
  EntityValueAfter: JSON.stringify(filteredRecord),
62
62
  PerformedById: record.PerformedById,
63
63
  PerformedAt: record.PerformedAt || Date.now(),
64
- EntityId: record.EntityId ? record.EntityId : cuid(),
64
+ EntityId: record.EntityId ? record.EntityId : createId(),
65
65
  };
66
66
 
67
67
  try {
@@ -21,7 +21,9 @@ export class MediasRepository
21
21
  const result = await MediasModel.findOne(options);
22
22
  return result;
23
23
  } catch (error) {
24
- throw new Error(`An Error occured when fetching : ${error.message}`);
24
+ throw new Error(
25
+ `An Error occured when fetching : ${(error as Error).message}`,
26
+ );
25
27
  }
26
28
  }
27
29
 
@@ -35,7 +37,9 @@ export class MediasRepository
35
37
  };
36
38
  await MediasModel.destroy(options);
37
39
  } catch (error) {
38
- throw new Error(`An Error occured when delete : ${error.message}`);
40
+ throw new Error(
41
+ `An Error occured when delete : ${(error as Error).message}`,
42
+ );
39
43
  }
40
44
  }
41
45
 
@@ -49,7 +53,9 @@ export class MediasRepository
49
53
  }
50
54
  return medias;
51
55
  } catch (error) {
52
- throw new Error(`An Error occured when retriving : ${error.message}`);
56
+ throw new Error(
57
+ `An Error occured when retriving : ${(error as Error).message}`,
58
+ );
53
59
  }
54
60
  }
55
61
  }