inmemory to postgres
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { Column, CreateDateColumn, Entity, Index, PrimaryColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'avatar_generation_requests' })
|
||||
export class AvatarGenerationRequestOrmEntity {
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
id!: string;
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'text' })
|
||||
userId!: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
facePhotoUrl!: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
suitColor!: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
style!: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
status!: string;
|
||||
|
||||
@Column({ type: 'jsonb' })
|
||||
generatedAvatarUrls!: string[];
|
||||
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
selectedAvatarIndex!: number | null;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
errorMessage!: string | null;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt!: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Column, CreateDateColumn, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'avatars' })
|
||||
export class AvatarOrmEntity {
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
id!: string;
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'text' })
|
||||
driverId!: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
mediaUrl!: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
selectedAt!: Date;
|
||||
|
||||
@Column({ type: 'boolean', default: true })
|
||||
isActive!: boolean;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Column, CreateDateColumn, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'media_files' })
|
||||
export class MediaOrmEntity {
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
filename!: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
originalName!: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
mimeType!: string;
|
||||
|
||||
@Column({ type: 'integer' })
|
||||
size!: number;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
url!: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
type!: string;
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'text' })
|
||||
uploadedBy!: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
uploadedAt!: Date;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
metadata!: Record<string, unknown> | null;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
export type TypeOrmMediaSchemaErrorReason =
|
||||
| 'missing'
|
||||
| 'not_string'
|
||||
| 'empty_string'
|
||||
| 'not_number'
|
||||
| 'not_integer'
|
||||
| 'not_boolean'
|
||||
| 'not_date'
|
||||
| 'invalid_date'
|
||||
| 'not_iso_date'
|
||||
| 'not_array'
|
||||
| 'not_object'
|
||||
| 'invalid_enum_value'
|
||||
| 'invalid_shape';
|
||||
|
||||
export class TypeOrmMediaSchemaError extends Error {
|
||||
readonly entityName: string;
|
||||
readonly fieldName: string;
|
||||
readonly reason: TypeOrmMediaSchemaErrorReason | (string & {});
|
||||
|
||||
constructor(params: {
|
||||
entityName: string;
|
||||
fieldName: string;
|
||||
reason: TypeOrmMediaSchemaError['reason'];
|
||||
message?: string;
|
||||
}) {
|
||||
const message = params.message ?? `Invalid persisted ${params.entityName}.${params.fieldName}: ${params.reason}`;
|
||||
super(message);
|
||||
this.name = 'TypeOrmMediaSchemaError';
|
||||
this.entityName = params.entityName;
|
||||
this.fieldName = params.fieldName;
|
||||
this.reason = params.reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { AvatarGenerationRequest } from '@core/media/domain/entities/AvatarGenerationRequest';
|
||||
|
||||
import { AvatarGenerationRequestOrmEntity } from '../entities/AvatarGenerationRequestOrmEntity';
|
||||
import { TypeOrmMediaSchemaError } from '../errors/TypeOrmMediaSchemaError';
|
||||
import { AvatarGenerationRequestOrmMapper } from './AvatarGenerationRequestOrmMapper';
|
||||
|
||||
describe('AvatarGenerationRequestOrmMapper', () => {
|
||||
it('toDomain preserves persisted identity and uses reconstitute semantics', () => {
|
||||
const mapper = new AvatarGenerationRequestOrmMapper();
|
||||
|
||||
const entity = new AvatarGenerationRequestOrmEntity();
|
||||
entity.id = '00000000-0000-4000-8000-000000000001';
|
||||
entity.userId = 'user-123';
|
||||
entity.facePhotoUrl = 'https://cdn.example.com/faces/face-1.png';
|
||||
entity.suitColor = 'red';
|
||||
entity.style = 'realistic';
|
||||
entity.status = 'completed';
|
||||
entity.generatedAvatarUrls = ['https://cdn.example.com/avatars/av-1.png', 'https://cdn.example.com/avatars/av-2.png'];
|
||||
entity.selectedAvatarIndex = 0;
|
||||
entity.errorMessage = null;
|
||||
entity.createdAt = new Date('2025-01-01T00:00:00.000Z');
|
||||
entity.updatedAt = new Date('2025-01-01T01:00:00.000Z');
|
||||
|
||||
const reconstituteSpy = vi.spyOn(AvatarGenerationRequest as unknown as { reconstitute: (...args: unknown[]) => unknown }, 'reconstitute');
|
||||
|
||||
const domain = mapper.toDomain(entity);
|
||||
|
||||
expect(domain.id).toBe(entity.id);
|
||||
expect(domain.userId).toBe(entity.userId);
|
||||
expect(domain.facePhotoUrl.value).toBe(entity.facePhotoUrl);
|
||||
expect(domain.suitColor).toBe(entity.suitColor);
|
||||
expect(domain.status).toBe(entity.status);
|
||||
expect(domain.generatedAvatarUrls).toEqual(entity.generatedAvatarUrls);
|
||||
expect(domain.selectedAvatarIndex).toBe(entity.selectedAvatarIndex);
|
||||
|
||||
expect(reconstituteSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('toDomain validates persisted shape', () => {
|
||||
const mapper = new AvatarGenerationRequestOrmMapper();
|
||||
|
||||
const entity = new AvatarGenerationRequestOrmEntity();
|
||||
entity.id = '00000000-0000-4000-8000-000000000001';
|
||||
entity.userId = 123 as unknown as string;
|
||||
entity.facePhotoUrl = 'https://cdn.example.com/faces/face-1.png';
|
||||
entity.suitColor = 'red';
|
||||
entity.style = 'realistic';
|
||||
entity.status = 'completed';
|
||||
entity.generatedAvatarUrls = [];
|
||||
entity.selectedAvatarIndex = null;
|
||||
entity.errorMessage = null;
|
||||
entity.createdAt = new Date('2025-01-01T00:00:00.000Z');
|
||||
entity.updatedAt = new Date('2025-01-01T01:00:00.000Z');
|
||||
|
||||
try {
|
||||
mapper.toDomain(entity);
|
||||
throw new Error('expected-to-throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TypeOrmMediaSchemaError);
|
||||
expect(error).toMatchObject({
|
||||
entityName: 'AvatarGenerationRequest',
|
||||
fieldName: 'userId',
|
||||
reason: 'not_string',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('toOrmEntity converts domain entity to ORM entity', () => {
|
||||
const mapper = new AvatarGenerationRequestOrmMapper();
|
||||
|
||||
const domain = AvatarGenerationRequest.create({
|
||||
id: '00000000-0000-4000-8000-000000000001',
|
||||
userId: 'user-123',
|
||||
facePhotoUrl: 'https://cdn.example.com/faces/face-1.png',
|
||||
suitColor: 'blue',
|
||||
style: 'cartoon',
|
||||
});
|
||||
|
||||
// Simulate completion
|
||||
domain.completeWithAvatars(['https://cdn.example.com/avatars/av-1.png']);
|
||||
|
||||
const entity = mapper.toOrmEntity(domain);
|
||||
|
||||
expect(entity.id).toBe(domain.id);
|
||||
expect(entity.userId).toBe(domain.userId);
|
||||
expect(entity.facePhotoUrl).toBe(domain.facePhotoUrl.value);
|
||||
expect(entity.suitColor).toBe(domain.suitColor);
|
||||
expect(entity.style).toBe(domain.style);
|
||||
expect(entity.status).toBe(domain.status);
|
||||
expect(entity.generatedAvatarUrls).toEqual(domain.generatedAvatarUrls);
|
||||
expect(entity.selectedAvatarIndex).toBeNull();
|
||||
expect(entity.errorMessage).toBeNull();
|
||||
});
|
||||
|
||||
it('toDomain handles optional fields correctly', () => {
|
||||
const mapper = new AvatarGenerationRequestOrmMapper();
|
||||
|
||||
const entity = new AvatarGenerationRequestOrmEntity();
|
||||
entity.id = '00000000-0000-4000-8000-000000000001';
|
||||
entity.userId = 'user-123';
|
||||
entity.facePhotoUrl = 'https://cdn.example.com/faces/face-1.png';
|
||||
entity.suitColor = 'red';
|
||||
entity.style = 'realistic';
|
||||
entity.status = 'failed';
|
||||
entity.generatedAvatarUrls = [];
|
||||
entity.selectedAvatarIndex = null;
|
||||
entity.errorMessage = 'Generation failed';
|
||||
entity.createdAt = new Date('2025-01-01T00:00:00.000Z');
|
||||
entity.updatedAt = new Date('2025-01-01T01:00:00.000Z');
|
||||
|
||||
const domain = mapper.toDomain(entity);
|
||||
|
||||
expect(domain.errorMessage).toBe('Generation failed');
|
||||
expect(domain.selectedAvatarIndex).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { AvatarGenerationRequest } from '@core/media/domain/entities/AvatarGenerationRequest';
|
||||
import { AvatarGenerationRequestOrmEntity } from '../entities/AvatarGenerationRequestOrmEntity';
|
||||
import { TypeOrmMediaSchemaError } from '../errors/TypeOrmMediaSchemaError';
|
||||
import {
|
||||
assertNonEmptyString,
|
||||
assertDate,
|
||||
assertStringArray,
|
||||
assertOptionalIntegerOrNull,
|
||||
assertOptionalStringOrNull,
|
||||
assertRacingSuitColor,
|
||||
assertAvatarStyle,
|
||||
assertAvatarGenerationStatus,
|
||||
} from '../schema/TypeOrmMediaSchemaGuards';
|
||||
|
||||
export class AvatarGenerationRequestOrmMapper {
|
||||
toDomain(entity: AvatarGenerationRequestOrmEntity): AvatarGenerationRequest {
|
||||
const entityName = 'AvatarGenerationRequest';
|
||||
|
||||
try {
|
||||
assertNonEmptyString(entityName, 'id', entity.id);
|
||||
assertNonEmptyString(entityName, 'userId', entity.userId);
|
||||
assertNonEmptyString(entityName, 'facePhotoUrl', entity.facePhotoUrl);
|
||||
assertRacingSuitColor(entityName, 'suitColor', entity.suitColor);
|
||||
assertAvatarStyle(entityName, 'style', entity.style);
|
||||
assertAvatarGenerationStatus(entityName, 'status', entity.status);
|
||||
assertStringArray(entityName, 'generatedAvatarUrls', entity.generatedAvatarUrls);
|
||||
assertOptionalIntegerOrNull(entityName, 'selectedAvatarIndex', entity.selectedAvatarIndex);
|
||||
assertOptionalStringOrNull(entityName, 'errorMessage', entity.errorMessage);
|
||||
assertDate(entityName, 'createdAt', entity.createdAt);
|
||||
assertDate(entityName, 'updatedAt', entity.updatedAt);
|
||||
} catch (error) {
|
||||
if (error instanceof TypeOrmMediaSchemaError) {
|
||||
throw error;
|
||||
}
|
||||
const message = error instanceof Error ? error.message : 'Invalid persisted AvatarGenerationRequest';
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName: 'unknown', reason: 'invalid_shape', message });
|
||||
}
|
||||
|
||||
try {
|
||||
const props: any = {
|
||||
id: entity.id,
|
||||
userId: entity.userId,
|
||||
facePhotoUrl: entity.facePhotoUrl,
|
||||
suitColor: entity.suitColor,
|
||||
style: entity.style,
|
||||
status: entity.status,
|
||||
generatedAvatarUrls: entity.generatedAvatarUrls,
|
||||
createdAt: entity.createdAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
};
|
||||
|
||||
if (entity.selectedAvatarIndex !== null && entity.selectedAvatarIndex !== undefined) {
|
||||
props.selectedAvatarIndex = entity.selectedAvatarIndex;
|
||||
}
|
||||
|
||||
if (entity.errorMessage !== null && entity.errorMessage !== undefined) {
|
||||
props.errorMessage = entity.errorMessage;
|
||||
}
|
||||
|
||||
return AvatarGenerationRequest.reconstitute(props);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Invalid persisted AvatarGenerationRequest';
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName: 'unknown', reason: 'invalid_shape', message });
|
||||
}
|
||||
}
|
||||
|
||||
toOrmEntity(request: AvatarGenerationRequest): AvatarGenerationRequestOrmEntity {
|
||||
const entity = new AvatarGenerationRequestOrmEntity();
|
||||
const props = request.toProps();
|
||||
|
||||
entity.id = props.id;
|
||||
entity.userId = props.userId;
|
||||
entity.facePhotoUrl = props.facePhotoUrl;
|
||||
entity.suitColor = props.suitColor;
|
||||
entity.style = props.style;
|
||||
entity.status = props.status;
|
||||
entity.generatedAvatarUrls = props.generatedAvatarUrls;
|
||||
entity.selectedAvatarIndex = props.selectedAvatarIndex ?? null;
|
||||
entity.errorMessage = props.errorMessage ?? null;
|
||||
entity.createdAt = props.createdAt;
|
||||
entity.updatedAt = props.updatedAt;
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
toStored(entity: AvatarGenerationRequestOrmEntity): AvatarGenerationRequest {
|
||||
return this.toDomain(entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { Avatar } from '@core/media/domain/entities/Avatar';
|
||||
|
||||
import { AvatarOrmEntity } from '../entities/AvatarOrmEntity';
|
||||
import { TypeOrmMediaSchemaError } from '../errors/TypeOrmMediaSchemaError';
|
||||
import { AvatarOrmMapper } from './AvatarOrmMapper';
|
||||
|
||||
describe('AvatarOrmMapper', () => {
|
||||
it('toDomain preserves persisted identity and uses reconstitute semantics', () => {
|
||||
const mapper = new AvatarOrmMapper();
|
||||
|
||||
const entity = new AvatarOrmEntity();
|
||||
entity.id = '00000000-0000-4000-8000-000000000001';
|
||||
entity.driverId = 'driver-123';
|
||||
entity.mediaUrl = 'https://cdn.example.com/avatars/avatar-1.png';
|
||||
entity.selectedAt = new Date('2025-01-01T00:00:00.000Z');
|
||||
entity.isActive = true;
|
||||
|
||||
const reconstituteSpy = vi.spyOn(Avatar as unknown as { reconstitute: (...args: unknown[]) => unknown }, 'reconstitute');
|
||||
|
||||
const domain = mapper.toDomain(entity);
|
||||
|
||||
expect(domain.id).toBe(entity.id);
|
||||
expect(domain.driverId).toBe(entity.driverId);
|
||||
expect(domain.mediaUrl.value).toBe(entity.mediaUrl);
|
||||
expect(domain.isActive).toBe(entity.isActive);
|
||||
|
||||
expect(reconstituteSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('toDomain validates persisted shape', () => {
|
||||
const mapper = new AvatarOrmMapper();
|
||||
|
||||
const entity = new AvatarOrmEntity();
|
||||
entity.id = '00000000-0000-4000-8000-000000000001';
|
||||
entity.driverId = 123 as unknown as string;
|
||||
entity.mediaUrl = 'https://cdn.example.com/avatars/avatar-1.png';
|
||||
entity.selectedAt = new Date('2025-01-01T00:00:00.000Z');
|
||||
entity.isActive = true;
|
||||
|
||||
try {
|
||||
mapper.toDomain(entity);
|
||||
throw new Error('expected-to-throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TypeOrmMediaSchemaError);
|
||||
expect(error).toMatchObject({
|
||||
entityName: 'Avatar',
|
||||
fieldName: 'driverId',
|
||||
reason: 'not_string',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('toOrmEntity converts domain entity to ORM entity', () => {
|
||||
const mapper = new AvatarOrmMapper();
|
||||
|
||||
const domain = Avatar.create({
|
||||
id: '00000000-0000-4000-8000-000000000001',
|
||||
driverId: 'driver-123',
|
||||
mediaUrl: 'https://cdn.example.com/avatars/avatar-1.png',
|
||||
});
|
||||
|
||||
const entity = mapper.toOrmEntity(domain);
|
||||
|
||||
expect(entity.id).toBe(domain.id);
|
||||
expect(entity.driverId).toBe(domain.driverId);
|
||||
expect(entity.mediaUrl).toBe(domain.mediaUrl.value);
|
||||
expect(entity.isActive).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Avatar } from '@core/media/domain/entities/Avatar';
|
||||
import { AvatarOrmEntity } from '../entities/AvatarOrmEntity';
|
||||
import { TypeOrmMediaSchemaError } from '../errors/TypeOrmMediaSchemaError';
|
||||
import {
|
||||
assertNonEmptyString,
|
||||
assertDate,
|
||||
assertBoolean,
|
||||
} from '../schema/TypeOrmMediaSchemaGuards';
|
||||
|
||||
export class AvatarOrmMapper {
|
||||
toDomain(entity: AvatarOrmEntity): Avatar {
|
||||
const entityName = 'Avatar';
|
||||
|
||||
try {
|
||||
assertNonEmptyString(entityName, 'id', entity.id);
|
||||
assertNonEmptyString(entityName, 'driverId', entity.driverId);
|
||||
assertNonEmptyString(entityName, 'mediaUrl', entity.mediaUrl);
|
||||
assertDate(entityName, 'selectedAt', entity.selectedAt);
|
||||
assertBoolean(entityName, 'isActive', entity.isActive);
|
||||
} catch (error) {
|
||||
if (error instanceof TypeOrmMediaSchemaError) {
|
||||
throw error;
|
||||
}
|
||||
const message = error instanceof Error ? error.message : 'Invalid persisted Avatar';
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName: 'unknown', reason: 'invalid_shape', message });
|
||||
}
|
||||
|
||||
try {
|
||||
return Avatar.reconstitute({
|
||||
id: entity.id,
|
||||
driverId: entity.driverId,
|
||||
mediaUrl: entity.mediaUrl,
|
||||
selectedAt: entity.selectedAt,
|
||||
isActive: entity.isActive,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Invalid persisted Avatar';
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName: 'unknown', reason: 'invalid_shape', message });
|
||||
}
|
||||
}
|
||||
|
||||
toOrmEntity(avatar: Avatar): AvatarOrmEntity {
|
||||
const entity = new AvatarOrmEntity();
|
||||
const props = avatar.toProps();
|
||||
|
||||
entity.id = props.id;
|
||||
entity.driverId = props.driverId;
|
||||
entity.mediaUrl = props.mediaUrl;
|
||||
entity.selectedAt = props.selectedAt;
|
||||
entity.isActive = props.isActive;
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
toStored(entity: AvatarOrmEntity): Avatar {
|
||||
return this.toDomain(entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { Media } from '@core/media/domain/entities/Media';
|
||||
|
||||
import { MediaOrmEntity } from '../entities/MediaOrmEntity';
|
||||
import { TypeOrmMediaSchemaError } from '../errors/TypeOrmMediaSchemaError';
|
||||
import { MediaOrmMapper } from './MediaOrmMapper';
|
||||
|
||||
describe('MediaOrmMapper', () => {
|
||||
it('toDomain preserves persisted identity and uses reconstitute semantics (does not call create)', () => {
|
||||
const mapper = new MediaOrmMapper();
|
||||
|
||||
const entity = new MediaOrmEntity();
|
||||
entity.id = '00000000-0000-4000-8000-000000000001';
|
||||
entity.filename = 'test-image.png';
|
||||
entity.originalName = 'original.png';
|
||||
entity.mimeType = 'image/png';
|
||||
entity.size = 12345;
|
||||
entity.url = 'https://cdn.example.com/test-image.png';
|
||||
entity.type = 'image';
|
||||
entity.uploadedBy = 'user-123';
|
||||
entity.uploadedAt = new Date('2025-01-01T00:00:00.000Z');
|
||||
entity.metadata = { width: 800, height: 600 };
|
||||
|
||||
if (typeof (Media as unknown as { reconstitute?: unknown }).reconstitute !== 'function') {
|
||||
throw new Error('reconstitute-missing');
|
||||
}
|
||||
|
||||
const reconstituteSpy = vi.spyOn(Media as unknown as { reconstitute: (...args: unknown[]) => unknown }, 'reconstitute');
|
||||
|
||||
const domain = mapper.toDomain(entity);
|
||||
|
||||
expect(domain.id).toBe(entity.id);
|
||||
expect(domain.filename).toBe(entity.filename);
|
||||
expect(domain.url.value).toBe(entity.url);
|
||||
|
||||
expect(reconstituteSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('toDomain validates persisted shape and throws adapter-scoped base schema error type', () => {
|
||||
const mapper = new MediaOrmMapper();
|
||||
|
||||
const entity = new MediaOrmEntity();
|
||||
entity.id = '00000000-0000-4000-8000-000000000001';
|
||||
entity.filename = 123 as unknown as string;
|
||||
entity.originalName = 'original.png';
|
||||
entity.mimeType = 'image/png';
|
||||
entity.size = 12345;
|
||||
entity.url = 'https://cdn.example.com/test-image.png';
|
||||
entity.type = 'image';
|
||||
entity.uploadedBy = 'user-123';
|
||||
entity.uploadedAt = new Date('2025-01-01T00:00:00.000Z');
|
||||
entity.metadata = null;
|
||||
|
||||
try {
|
||||
mapper.toDomain(entity);
|
||||
throw new Error('expected-to-throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(TypeOrmMediaSchemaError);
|
||||
expect(error).toMatchObject({
|
||||
entityName: 'Media',
|
||||
fieldName: 'filename',
|
||||
reason: 'not_string',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('toOrmEntity converts domain entity to ORM entity', () => {
|
||||
const mapper = new MediaOrmMapper();
|
||||
|
||||
const domain = Media.create({
|
||||
id: '00000000-0000-4000-8000-000000000001',
|
||||
filename: 'test-image.png',
|
||||
originalName: 'original.png',
|
||||
mimeType: 'image/png',
|
||||
size: 12345,
|
||||
url: 'https://cdn.example.com/test-image.png',
|
||||
type: 'image',
|
||||
uploadedBy: 'user-123',
|
||||
metadata: { width: 800, height: 600 },
|
||||
});
|
||||
|
||||
const entity = mapper.toOrmEntity(domain);
|
||||
|
||||
expect(entity.id).toBe(domain.id);
|
||||
expect(entity.filename).toBe(domain.filename);
|
||||
expect(entity.url).toBe(domain.url.value);
|
||||
expect(entity.metadata).toEqual({ width: 800, height: 600 });
|
||||
});
|
||||
|
||||
it('toDomain handles null metadata', () => {
|
||||
const mapper = new MediaOrmMapper();
|
||||
|
||||
const entity = new MediaOrmEntity();
|
||||
entity.id = '00000000-0000-4000-8000-000000000001';
|
||||
entity.filename = 'test-image.png';
|
||||
entity.originalName = 'original.png';
|
||||
entity.mimeType = 'image/png';
|
||||
entity.size = 12345;
|
||||
entity.url = 'https://cdn.example.com/test-image.png';
|
||||
entity.type = 'image';
|
||||
entity.uploadedBy = 'user-123';
|
||||
entity.uploadedAt = new Date('2025-01-01T00:00:00.000Z');
|
||||
entity.metadata = null;
|
||||
|
||||
const domain = mapper.toDomain(entity);
|
||||
|
||||
expect(domain.id).toBe(entity.id);
|
||||
expect(domain.metadata).toBeUndefined();
|
||||
});
|
||||
});
|
||||
78
adapters/media/persistence/typeorm/mappers/MediaOrmMapper.ts
Normal file
78
adapters/media/persistence/typeorm/mappers/MediaOrmMapper.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Media } from '@core/media/domain/entities/Media';
|
||||
import { MediaOrmEntity } from '../entities/MediaOrmEntity';
|
||||
import { TypeOrmMediaSchemaError } from '../errors/TypeOrmMediaSchemaError';
|
||||
import {
|
||||
assertNonEmptyString,
|
||||
assertDate,
|
||||
assertInteger,
|
||||
assertMediaType,
|
||||
} from '../schema/TypeOrmMediaSchemaGuards';
|
||||
|
||||
export class MediaOrmMapper {
|
||||
toDomain(entity: MediaOrmEntity): Media {
|
||||
const entityName = 'Media';
|
||||
|
||||
try {
|
||||
assertNonEmptyString(entityName, 'id', entity.id);
|
||||
assertNonEmptyString(entityName, 'filename', entity.filename);
|
||||
assertNonEmptyString(entityName, 'originalName', entity.originalName);
|
||||
assertNonEmptyString(entityName, 'mimeType', entity.mimeType);
|
||||
assertInteger(entityName, 'size', entity.size);
|
||||
assertNonEmptyString(entityName, 'url', entity.url);
|
||||
assertMediaType(entityName, 'type', entity.type);
|
||||
assertNonEmptyString(entityName, 'uploadedBy', entity.uploadedBy);
|
||||
assertDate(entityName, 'uploadedAt', entity.uploadedAt);
|
||||
} catch (error) {
|
||||
if (error instanceof TypeOrmMediaSchemaError) {
|
||||
throw error;
|
||||
}
|
||||
const message = error instanceof Error ? error.message : 'Invalid persisted Media';
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName: 'unknown', reason: 'invalid_shape', message });
|
||||
}
|
||||
|
||||
try {
|
||||
const domainProps: any = {
|
||||
id: entity.id,
|
||||
filename: entity.filename,
|
||||
originalName: entity.originalName,
|
||||
mimeType: entity.mimeType,
|
||||
size: entity.size,
|
||||
url: entity.url,
|
||||
type: entity.type as 'image' | 'video' | 'document',
|
||||
uploadedBy: entity.uploadedBy,
|
||||
uploadedAt: entity.uploadedAt,
|
||||
};
|
||||
|
||||
if (entity.metadata !== null && entity.metadata !== undefined) {
|
||||
domainProps.metadata = entity.metadata as Record<string, unknown>;
|
||||
}
|
||||
|
||||
return Media.reconstitute(domainProps);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Invalid persisted Media';
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName: 'unknown', reason: 'invalid_shape', message });
|
||||
}
|
||||
}
|
||||
|
||||
toOrmEntity(media: Media): MediaOrmEntity {
|
||||
const entity = new MediaOrmEntity();
|
||||
const props = media.toProps();
|
||||
|
||||
entity.id = props.id;
|
||||
entity.filename = props.filename;
|
||||
entity.originalName = props.originalName;
|
||||
entity.mimeType = props.mimeType;
|
||||
entity.size = props.size;
|
||||
entity.url = props.url;
|
||||
entity.type = props.type;
|
||||
entity.uploadedBy = props.uploadedBy;
|
||||
entity.uploadedAt = props.uploadedAt;
|
||||
entity.metadata = props.metadata ?? null;
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
toStored(entity: MediaOrmEntity): Media {
|
||||
return this.toDomain(entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { TypeOrmAvatarGenerationRepository } from './TypeOrmAvatarGenerationRepository';
|
||||
|
||||
describe('TypeOrmAvatarGenerationRepository', () => {
|
||||
it('does not construct its own mapper dependencies', () => {
|
||||
const sourcePath = path.resolve(__dirname, 'TypeOrmAvatarGenerationRepository.ts');
|
||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||
|
||||
expect(source).not.toMatch(/new\s+AvatarGenerationRequestOrmMapper\s*\(/);
|
||||
expect(source).not.toMatch(/=\s*new\s+AvatarGenerationRequestOrmMapper\s*\(/);
|
||||
});
|
||||
|
||||
it('requires mapper injection via constructor (no default mapper)', () => {
|
||||
expect(TypeOrmAvatarGenerationRepository.length).toBe(2);
|
||||
});
|
||||
|
||||
it('uses the injected mapper at runtime (DB-free)', async () => {
|
||||
const ormRepo = {
|
||||
findOne: vi.fn().mockResolvedValue({ id: 'request-1' }),
|
||||
find: vi.fn().mockResolvedValue([{ id: 'request-1' }, { id: 'request-2' }]),
|
||||
save: vi.fn().mockResolvedValue({ id: 'request-1' }),
|
||||
delete: vi.fn().mockResolvedValue({ affected: 1 }),
|
||||
};
|
||||
|
||||
const dataSource = {
|
||||
getRepository: vi.fn().mockReturnValue(ormRepo),
|
||||
};
|
||||
|
||||
const mapper = {
|
||||
toStored: vi.fn().mockReturnValue({ id: 'stored-request-1' }),
|
||||
toDomain: vi.fn().mockReturnValue({ id: 'domain-request-1' }),
|
||||
toOrmEntity: vi.fn().mockReturnValue({ id: 'orm-request-1' }),
|
||||
};
|
||||
|
||||
const repo = new TypeOrmAvatarGenerationRepository(dataSource as any, mapper as any);
|
||||
|
||||
// Test findById
|
||||
const request = await repo.findById('request-1');
|
||||
expect(dataSource.getRepository).toHaveBeenCalledTimes(1);
|
||||
expect(ormRepo.findOne).toHaveBeenCalledWith({ where: { id: 'request-1' } });
|
||||
expect(mapper.toDomain).toHaveBeenCalledTimes(1);
|
||||
expect(request).toEqual({ id: 'domain-request-1' });
|
||||
|
||||
// Test findByUserId
|
||||
const requests = await repo.findByUserId('user-1');
|
||||
expect(ormRepo.find).toHaveBeenCalledWith({
|
||||
where: { userId: 'user-1' },
|
||||
order: { createdAt: 'DESC' }
|
||||
});
|
||||
expect(mapper.toDomain).toHaveBeenCalledTimes(3); // 1 from findById + 2 from findByUserId
|
||||
expect(requests).toHaveLength(2);
|
||||
|
||||
// Test findLatestByUserId
|
||||
await repo.findLatestByUserId('user-1');
|
||||
expect(ormRepo.findOne).toHaveBeenCalledWith({
|
||||
where: { userId: 'user-1' },
|
||||
order: { createdAt: 'DESC' }
|
||||
});
|
||||
|
||||
// Test save
|
||||
const domainRequest = { id: 'new-request', toProps: () => ({ id: 'new-request' }) };
|
||||
await repo.save(domainRequest as any);
|
||||
expect(mapper.toOrmEntity).toHaveBeenCalledWith(domainRequest);
|
||||
expect(ormRepo.save).toHaveBeenCalledWith({ id: 'orm-request-1' });
|
||||
|
||||
// Test delete
|
||||
await repo.delete('request-1');
|
||||
expect(ormRepo.delete).toHaveBeenCalledWith({ id: 'request-1' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { DataSource } from 'typeorm';
|
||||
import type { IAvatarGenerationRepository } from '@core/media/domain/repositories/IAvatarGenerationRepository';
|
||||
import type { AvatarGenerationRequest } from '@core/media/domain/entities/AvatarGenerationRequest';
|
||||
import { AvatarGenerationRequestOrmEntity } from '../entities/AvatarGenerationRequestOrmEntity';
|
||||
import { AvatarGenerationRequestOrmMapper } from '../mappers/AvatarGenerationRequestOrmMapper';
|
||||
|
||||
export class TypeOrmAvatarGenerationRepository implements IAvatarGenerationRepository {
|
||||
constructor(
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly mapper: AvatarGenerationRequestOrmMapper,
|
||||
) {}
|
||||
|
||||
async save(request: AvatarGenerationRequest): Promise<void> {
|
||||
const repo = this.dataSource.getRepository(AvatarGenerationRequestOrmEntity);
|
||||
const entity = this.mapper.toOrmEntity(request);
|
||||
await repo.save(entity);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<AvatarGenerationRequest | null> {
|
||||
const repo = this.dataSource.getRepository(AvatarGenerationRequestOrmEntity);
|
||||
const entity = await repo.findOne({ where: { id } });
|
||||
return entity ? this.mapper.toDomain(entity) : null;
|
||||
}
|
||||
|
||||
async findByUserId(userId: string): Promise<AvatarGenerationRequest[]> {
|
||||
const repo = this.dataSource.getRepository(AvatarGenerationRequestOrmEntity);
|
||||
const entities = await repo.find({
|
||||
where: { userId },
|
||||
order: { createdAt: 'DESC' }
|
||||
});
|
||||
return entities.map(entity => this.mapper.toDomain(entity));
|
||||
}
|
||||
|
||||
async findLatestByUserId(userId: string): Promise<AvatarGenerationRequest | null> {
|
||||
const repo = this.dataSource.getRepository(AvatarGenerationRequestOrmEntity);
|
||||
const entity = await repo.findOne({
|
||||
where: { userId },
|
||||
order: { createdAt: 'DESC' }
|
||||
});
|
||||
return entity ? this.mapper.toDomain(entity) : null;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
const repo = this.dataSource.getRepository(AvatarGenerationRequestOrmEntity);
|
||||
await repo.delete({ id });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { TypeOrmAvatarRepository } from './TypeOrmAvatarRepository';
|
||||
|
||||
describe('TypeOrmAvatarRepository', () => {
|
||||
it('does not construct its own mapper dependencies', () => {
|
||||
const sourcePath = path.resolve(__dirname, 'TypeOrmAvatarRepository.ts');
|
||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||
|
||||
expect(source).not.toMatch(/new\s+AvatarOrmMapper\s*\(/);
|
||||
expect(source).not.toMatch(/=\s*new\s+AvatarOrmMapper\s*\(/);
|
||||
});
|
||||
|
||||
it('requires mapper injection via constructor (no default mapper)', () => {
|
||||
expect(TypeOrmAvatarRepository.length).toBe(2);
|
||||
});
|
||||
|
||||
it('uses the injected mapper at runtime (DB-free)', async () => {
|
||||
const ormRepo = {
|
||||
findOne: vi.fn().mockResolvedValue({ id: 'avatar-1' }),
|
||||
find: vi.fn().mockResolvedValue([{ id: 'avatar-1' }, { id: 'avatar-2' }]),
|
||||
save: vi.fn().mockResolvedValue({ id: 'avatar-1' }),
|
||||
delete: vi.fn().mockResolvedValue({ affected: 1 }),
|
||||
};
|
||||
|
||||
const dataSource = {
|
||||
getRepository: vi.fn().mockReturnValue(ormRepo),
|
||||
};
|
||||
|
||||
const mapper = {
|
||||
toStored: vi.fn().mockReturnValue({ id: 'stored-avatar-1' }),
|
||||
toDomain: vi.fn().mockReturnValue({ id: 'domain-avatar-1' }),
|
||||
toOrmEntity: vi.fn().mockReturnValue({ id: 'orm-avatar-1' }),
|
||||
};
|
||||
|
||||
const repo = new TypeOrmAvatarRepository(dataSource as any, mapper as any);
|
||||
|
||||
// Test findById
|
||||
const avatar = await repo.findById('avatar-1');
|
||||
expect(dataSource.getRepository).toHaveBeenCalledTimes(1);
|
||||
expect(ormRepo.findOne).toHaveBeenCalledWith({ where: { id: 'avatar-1' } });
|
||||
expect(mapper.toDomain).toHaveBeenCalledTimes(1);
|
||||
expect(avatar).toEqual({ id: 'domain-avatar-1' });
|
||||
|
||||
// Test findActiveByDriverId
|
||||
await repo.findActiveByDriverId('driver-1');
|
||||
expect(ormRepo.findOne).toHaveBeenCalledWith({
|
||||
where: { driverId: 'driver-1', isActive: true },
|
||||
order: { selectedAt: 'DESC' }
|
||||
});
|
||||
|
||||
// Test findByDriverId
|
||||
const avatars = await repo.findByDriverId('driver-1');
|
||||
expect(ormRepo.find).toHaveBeenCalledWith({
|
||||
where: { driverId: 'driver-1' },
|
||||
order: { selectedAt: 'DESC' }
|
||||
});
|
||||
expect(mapper.toDomain).toHaveBeenCalledTimes(4); // 1 from findById + 1 from findActiveByDriverId + 2 from findByDriverId
|
||||
expect(avatars).toHaveLength(2);
|
||||
|
||||
// Test save
|
||||
const domainAvatar = { id: 'new-avatar', toProps: () => ({ id: 'new-avatar' }) };
|
||||
await repo.save(domainAvatar as any);
|
||||
expect(mapper.toOrmEntity).toHaveBeenCalledWith(domainAvatar);
|
||||
expect(ormRepo.save).toHaveBeenCalledWith({ id: 'orm-avatar-1' });
|
||||
|
||||
// Test delete
|
||||
await repo.delete('avatar-1');
|
||||
expect(ormRepo.delete).toHaveBeenCalledWith({ id: 'avatar-1' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { DataSource } from 'typeorm';
|
||||
import type { IAvatarRepository } from '@core/media/domain/repositories/IAvatarRepository';
|
||||
import type { Avatar } from '@core/media/domain/entities/Avatar';
|
||||
import { AvatarOrmEntity } from '../entities/AvatarOrmEntity';
|
||||
import { AvatarOrmMapper } from '../mappers/AvatarOrmMapper';
|
||||
|
||||
export class TypeOrmAvatarRepository implements IAvatarRepository {
|
||||
constructor(
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly mapper: AvatarOrmMapper,
|
||||
) {}
|
||||
|
||||
async save(avatar: Avatar): Promise<void> {
|
||||
const repo = this.dataSource.getRepository(AvatarOrmEntity);
|
||||
const entity = this.mapper.toOrmEntity(avatar);
|
||||
await repo.save(entity);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Avatar | null> {
|
||||
const repo = this.dataSource.getRepository(AvatarOrmEntity);
|
||||
const entity = await repo.findOne({ where: { id } });
|
||||
return entity ? this.mapper.toDomain(entity) : null;
|
||||
}
|
||||
|
||||
async findActiveByDriverId(driverId: string): Promise<Avatar | null> {
|
||||
const repo = this.dataSource.getRepository(AvatarOrmEntity);
|
||||
const entity = await repo.findOne({
|
||||
where: { driverId, isActive: true },
|
||||
order: { selectedAt: 'DESC' }
|
||||
});
|
||||
return entity ? this.mapper.toDomain(entity) : null;
|
||||
}
|
||||
|
||||
async findByDriverId(driverId: string): Promise<Avatar[]> {
|
||||
const repo = this.dataSource.getRepository(AvatarOrmEntity);
|
||||
const entities = await repo.find({
|
||||
where: { driverId },
|
||||
order: { selectedAt: 'DESC' }
|
||||
});
|
||||
return entities.map(entity => this.mapper.toDomain(entity));
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
const repo = this.dataSource.getRepository(AvatarOrmEntity);
|
||||
await repo.delete({ id });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { TypeOrmMediaRepository } from './TypeOrmMediaRepository';
|
||||
|
||||
describe('TypeOrmMediaRepository', () => {
|
||||
it('does not construct its own mapper dependencies', () => {
|
||||
const sourcePath = path.resolve(__dirname, 'TypeOrmMediaRepository.ts');
|
||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||
|
||||
expect(source).not.toMatch(/new\s+MediaOrmMapper\s*\(/);
|
||||
expect(source).not.toMatch(/=\s*new\s+MediaOrmMapper\s*\(/);
|
||||
});
|
||||
|
||||
it('requires mapper injection via constructor (no default mapper)', () => {
|
||||
expect(TypeOrmMediaRepository.length).toBe(2);
|
||||
});
|
||||
|
||||
it('uses the injected mapper at runtime (DB-free)', async () => {
|
||||
const ormRepo = {
|
||||
findOne: vi.fn().mockResolvedValue({ id: 'media-1' }),
|
||||
find: vi.fn().mockResolvedValue([{ id: 'media-1' }, { id: 'media-2' }]),
|
||||
save: vi.fn().mockResolvedValue({ id: 'media-1' }),
|
||||
delete: vi.fn().mockResolvedValue({ affected: 1 }),
|
||||
};
|
||||
|
||||
const dataSource = {
|
||||
getRepository: vi.fn().mockReturnValue(ormRepo),
|
||||
};
|
||||
|
||||
const mapper = {
|
||||
toStored: vi.fn().mockReturnValue({ id: 'stored-media-1' }),
|
||||
toDomain: vi.fn().mockReturnValue({ id: 'domain-media-1' }),
|
||||
toOrmEntity: vi.fn().mockReturnValue({ id: 'orm-media-1' }),
|
||||
};
|
||||
|
||||
const repo = new TypeOrmMediaRepository(dataSource as any, mapper as any);
|
||||
|
||||
// Test findById
|
||||
const media = await repo.findById('media-1');
|
||||
expect(dataSource.getRepository).toHaveBeenCalledTimes(1);
|
||||
expect(ormRepo.findOne).toHaveBeenCalledWith({ where: { id: 'media-1' } });
|
||||
expect(mapper.toDomain).toHaveBeenCalledTimes(1);
|
||||
expect(media).toEqual({ id: 'domain-media-1' });
|
||||
|
||||
// Test findByUploadedBy
|
||||
const medias = await repo.findByUploadedBy('user-1');
|
||||
expect(ormRepo.find).toHaveBeenCalledWith({ where: { uploadedBy: 'user-1' } });
|
||||
expect(mapper.toDomain).toHaveBeenCalledTimes(3); // 1 from findById + 2 from findByUploadedBy
|
||||
expect(medias).toHaveLength(2);
|
||||
|
||||
// Test save
|
||||
const domainMedia = { id: 'new-media', toProps: () => ({ id: 'new-media' }) };
|
||||
await repo.save(domainMedia as any);
|
||||
expect(mapper.toOrmEntity).toHaveBeenCalledWith(domainMedia);
|
||||
expect(ormRepo.save).toHaveBeenCalledWith({ id: 'orm-media-1' });
|
||||
|
||||
// Test delete
|
||||
await repo.delete('media-1');
|
||||
expect(ormRepo.delete).toHaveBeenCalledWith({ id: 'media-1' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { DataSource } from 'typeorm';
|
||||
import type { IMediaRepository } from '@core/media/domain/repositories/IMediaRepository';
|
||||
import type { Media } from '@core/media/domain/entities/Media';
|
||||
import { MediaOrmEntity } from '../entities/MediaOrmEntity';
|
||||
import { MediaOrmMapper } from '../mappers/MediaOrmMapper';
|
||||
|
||||
export class TypeOrmMediaRepository implements IMediaRepository {
|
||||
constructor(
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly mapper: MediaOrmMapper,
|
||||
) {}
|
||||
|
||||
async save(media: Media): Promise<void> {
|
||||
const repo = this.dataSource.getRepository(MediaOrmEntity);
|
||||
const entity = this.mapper.toOrmEntity(media);
|
||||
await repo.save(entity);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Media | null> {
|
||||
const repo = this.dataSource.getRepository(MediaOrmEntity);
|
||||
const entity = await repo.findOne({ where: { id } });
|
||||
return entity ? this.mapper.toDomain(entity) : null;
|
||||
}
|
||||
|
||||
async findByUploadedBy(uploadedBy: string): Promise<Media[]> {
|
||||
const repo = this.dataSource.getRepository(MediaOrmEntity);
|
||||
const entities = await repo.find({ where: { uploadedBy } });
|
||||
return entities.map(entity => this.mapper.toDomain(entity));
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
const repo = this.dataSource.getRepository(MediaOrmEntity);
|
||||
await repo.delete({ id });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { TypeOrmMediaSchemaError } from '../errors/TypeOrmMediaSchemaError';
|
||||
|
||||
export function assertNonEmptyString(entityName: string, fieldName: string, value: unknown): asserts value is string {
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_string' });
|
||||
}
|
||||
|
||||
if (value.trim().length === 0) {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'empty_string' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertDate(entityName: string, fieldName: string, value: unknown): asserts value is Date {
|
||||
if (!(value instanceof Date)) {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_date' });
|
||||
}
|
||||
if (Number.isNaN(value.getTime())) {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'invalid_date' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertNumber(entityName: string, fieldName: string, value: unknown): asserts value is number {
|
||||
if (typeof value !== 'number') {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_number' });
|
||||
}
|
||||
if (Number.isNaN(value)) {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_number' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertInteger(entityName: string, fieldName: string, value: unknown): asserts value is number {
|
||||
if (typeof value !== 'number') {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_number' });
|
||||
}
|
||||
if (!Number.isInteger(value)) {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_integer' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertBoolean(entityName: string, fieldName: string, value: unknown): asserts value is boolean {
|
||||
if (typeof value !== 'boolean') {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_boolean' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertOptionalStringOrNull(
|
||||
entityName: string,
|
||||
fieldName: string,
|
||||
value: unknown,
|
||||
): asserts value is string | null | undefined {
|
||||
if (value === null || value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_string' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertOptionalNumberOrNull(
|
||||
entityName: string,
|
||||
fieldName: string,
|
||||
value: unknown,
|
||||
): asserts value is number | null | undefined {
|
||||
if (value === null || value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value !== 'number') {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_number' });
|
||||
}
|
||||
if (Number.isNaN(value)) {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_number' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertStringArray(
|
||||
entityName: string,
|
||||
fieldName: string,
|
||||
value: unknown,
|
||||
): asserts value is string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_array' });
|
||||
}
|
||||
if (!value.every(item => typeof item === 'string')) {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_array' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertMediaType(entityName: string, fieldName: string, value: unknown): asserts value is 'image' | 'video' | 'document' {
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_string' });
|
||||
}
|
||||
if (!['image', 'video', 'document'].includes(value)) {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'invalid_enum_value' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertRacingSuitColor(
|
||||
entityName: string,
|
||||
fieldName: string,
|
||||
value: unknown,
|
||||
): asserts value is 'red' | 'blue' | 'green' | 'yellow' | 'orange' | 'purple' | 'black' | 'white' | 'pink' | 'cyan' {
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_string' });
|
||||
}
|
||||
if (
|
||||
!['red', 'blue', 'green', 'yellow', 'orange', 'purple', 'black', 'white', 'pink', 'cyan'].includes(value)
|
||||
) {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'invalid_enum_value' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertAvatarStyle(
|
||||
entityName: string,
|
||||
fieldName: string,
|
||||
value: unknown,
|
||||
): asserts value is 'realistic' | 'cartoon' | 'pixel-art' {
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_string' });
|
||||
}
|
||||
if (!['realistic', 'cartoon', 'pixel-art'].includes(value)) {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'invalid_enum_value' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertAvatarGenerationStatus(
|
||||
entityName: string,
|
||||
fieldName: string,
|
||||
value: unknown,
|
||||
): asserts value is 'pending' | 'validating' | 'generating' | 'completed' | 'failed' {
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_string' });
|
||||
}
|
||||
if (!['pending', 'validating', 'generating', 'completed', 'failed'].includes(value)) {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'invalid_enum_value' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertOptionalIntegerOrNull(
|
||||
entityName: string,
|
||||
fieldName: string,
|
||||
value: unknown,
|
||||
): asserts value is number | null | undefined {
|
||||
if (value === null || value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value !== 'number') {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_number' });
|
||||
}
|
||||
if (!Number.isInteger(value)) {
|
||||
throw new TypeOrmMediaSchemaError({ entityName, fieldName, reason: 'not_integer' });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user