inmemory to postgres
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
import type { FeedItemType } from '@core/social/domain/types/FeedItemType';
|
||||
|
||||
@Entity({ name: 'social_feed_items' })
|
||||
export class FeedItemOrmEntity {
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'timestamptz' })
|
||||
timestamp!: Date;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
type!: FeedItemType;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
actorFriendId!: string | null;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
actorDriverId!: string | null;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
leagueId!: string | null;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
raceId!: string | null;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
teamId!: string | null;
|
||||
|
||||
@Column({ type: 'int', nullable: true })
|
||||
position!: number | null;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
headline!: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
body!: string | null;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
ctaLabel!: string | null;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
ctaHref!: string | null;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'social_friendships' })
|
||||
export class FriendshipOrmEntity {
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
driverId!: string;
|
||||
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
friendId!: string;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
createdAt!: Date | null;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
export type TypeOrmSocialSchemaErrorReason =
|
||||
| 'missing'
|
||||
| 'not_string'
|
||||
| 'empty_string'
|
||||
| 'not_number'
|
||||
| 'not_integer'
|
||||
| 'not_boolean'
|
||||
| 'not_date'
|
||||
| 'invalid_date'
|
||||
| 'invalid_enum_value'
|
||||
| 'invalid_shape';
|
||||
|
||||
export class TypeOrmSocialSchemaError extends Error {
|
||||
readonly entityName: string;
|
||||
readonly fieldName: string;
|
||||
readonly reason: TypeOrmSocialSchemaErrorReason | (string & {});
|
||||
|
||||
constructor(params: {
|
||||
entityName: string;
|
||||
fieldName: string;
|
||||
reason: TypeOrmSocialSchemaError['reason'];
|
||||
message?: string;
|
||||
}) {
|
||||
const { entityName, fieldName, reason, message } = params;
|
||||
super(message);
|
||||
|
||||
this.name = 'TypeOrmSocialSchemaError';
|
||||
this.entityName = entityName;
|
||||
this.fieldName = fieldName;
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { FeedItemOrmEntity } from '../entities/FeedItemOrmEntity';
|
||||
import { TypeOrmSocialSchemaError } from '../errors/TypeOrmSocialSchemaError';
|
||||
import { FeedItemOrmMapper } from './FeedItemOrmMapper';
|
||||
|
||||
describe('FeedItemOrmMapper', () => {
|
||||
it('maps orm entity to domain FeedItem', () => {
|
||||
const mapper = new FeedItemOrmMapper();
|
||||
|
||||
const orm = new FeedItemOrmEntity();
|
||||
orm.id = '00000000-0000-0000-0000-000000000001';
|
||||
orm.timestamp = new Date('2025-01-01T00:00:00.000Z');
|
||||
orm.type = 'new-result-posted';
|
||||
orm.actorFriendId = null;
|
||||
orm.actorDriverId = '00000000-0000-0000-0000-0000000000aa';
|
||||
orm.leagueId = null;
|
||||
orm.raceId = '00000000-0000-0000-0000-0000000000bb';
|
||||
orm.teamId = null;
|
||||
orm.position = 2;
|
||||
orm.headline = 'Hello';
|
||||
orm.body = 'World';
|
||||
orm.ctaLabel = null;
|
||||
orm.ctaHref = null;
|
||||
|
||||
const domain = mapper.toDomain(orm);
|
||||
|
||||
expect(domain).toEqual({
|
||||
id: orm.id,
|
||||
timestamp: orm.timestamp,
|
||||
type: orm.type,
|
||||
actorDriverId: orm.actorDriverId,
|
||||
raceId: orm.raceId,
|
||||
position: orm.position,
|
||||
headline: orm.headline,
|
||||
body: orm.body,
|
||||
});
|
||||
});
|
||||
|
||||
it('throws TypeOrmSocialSchemaError for invalid shape', () => {
|
||||
const mapper = new FeedItemOrmMapper();
|
||||
|
||||
const orm = new FeedItemOrmEntity();
|
||||
orm.id = '';
|
||||
orm.timestamp = new Date('2025-01-01T00:00:00.000Z');
|
||||
orm.type = 'new-result-posted';
|
||||
orm.actorFriendId = null;
|
||||
orm.actorDriverId = null;
|
||||
orm.leagueId = null;
|
||||
orm.raceId = null;
|
||||
orm.teamId = null;
|
||||
orm.position = null;
|
||||
orm.headline = 'Hello';
|
||||
orm.body = null;
|
||||
orm.ctaLabel = null;
|
||||
orm.ctaHref = null;
|
||||
|
||||
expect(() => mapper.toDomain(orm)).toThrow(TypeOrmSocialSchemaError);
|
||||
});
|
||||
});
|
||||
100
adapters/social/persistence/typeorm/mappers/FeedItemOrmMapper.ts
Normal file
100
adapters/social/persistence/typeorm/mappers/FeedItemOrmMapper.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { FeedItem } from '@core/social/domain/types/FeedItem';
|
||||
import type { FeedItemType } from '@core/social/domain/types/FeedItemType';
|
||||
|
||||
import { TypeOrmSocialSchemaError } from '../errors/TypeOrmSocialSchemaError';
|
||||
import { FeedItemOrmEntity } from '../entities/FeedItemOrmEntity';
|
||||
import {
|
||||
assertDate,
|
||||
assertEnumValue,
|
||||
assertNonEmptyString,
|
||||
assertOptionalIntegerOrNull,
|
||||
assertOptionalStringOrNull,
|
||||
} from '../schema/TypeOrmSocialSchemaGuards';
|
||||
|
||||
export class FeedItemOrmMapper {
|
||||
toOrmEntity(domain: FeedItem): FeedItemOrmEntity {
|
||||
const entity = new FeedItemOrmEntity();
|
||||
entity.id = domain.id;
|
||||
entity.timestamp = domain.timestamp;
|
||||
entity.type = domain.type;
|
||||
|
||||
entity.actorFriendId = domain.actorFriendId ?? null;
|
||||
entity.actorDriverId = domain.actorDriverId ?? null;
|
||||
entity.leagueId = domain.leagueId ?? null;
|
||||
entity.raceId = domain.raceId ?? null;
|
||||
entity.teamId = domain.teamId ?? null;
|
||||
|
||||
entity.position = domain.position ?? null;
|
||||
|
||||
entity.headline = domain.headline;
|
||||
entity.body = domain.body ?? null;
|
||||
entity.ctaLabel = domain.ctaLabel ?? null;
|
||||
entity.ctaHref = domain.ctaHref ?? null;
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
toDomain(entity: FeedItemOrmEntity): FeedItem {
|
||||
const entityName = 'SocialFeedItem';
|
||||
|
||||
try {
|
||||
assertNonEmptyString(entityName, 'id', entity.id);
|
||||
assertDate(entityName, 'timestamp', entity.timestamp);
|
||||
|
||||
assertEnumValue<FeedItemType>(
|
||||
entityName,
|
||||
'type',
|
||||
entity.type,
|
||||
[
|
||||
'friend-joined-league',
|
||||
'friend-joined-team',
|
||||
'friend-finished-race',
|
||||
'friend-new-personal-best',
|
||||
'new-race-scheduled',
|
||||
'new-result-posted',
|
||||
'league-highlight',
|
||||
] as const,
|
||||
);
|
||||
|
||||
assertOptionalStringOrNull(entityName, 'actorFriendId', entity.actorFriendId);
|
||||
assertOptionalStringOrNull(entityName, 'actorDriverId', entity.actorDriverId);
|
||||
assertOptionalStringOrNull(entityName, 'leagueId', entity.leagueId);
|
||||
assertOptionalStringOrNull(entityName, 'raceId', entity.raceId);
|
||||
assertOptionalStringOrNull(entityName, 'teamId', entity.teamId);
|
||||
|
||||
assertOptionalIntegerOrNull(entityName, 'position', entity.position);
|
||||
|
||||
assertNonEmptyString(entityName, 'headline', entity.headline);
|
||||
assertOptionalStringOrNull(entityName, 'body', entity.body);
|
||||
assertOptionalStringOrNull(entityName, 'ctaLabel', entity.ctaLabel);
|
||||
assertOptionalStringOrNull(entityName, 'ctaHref', entity.ctaHref);
|
||||
|
||||
return {
|
||||
id: entity.id,
|
||||
timestamp: entity.timestamp,
|
||||
type: entity.type,
|
||||
...(entity.actorFriendId !== null && entity.actorFriendId !== undefined
|
||||
? { actorFriendId: entity.actorFriendId }
|
||||
: {}),
|
||||
...(entity.actorDriverId !== null && entity.actorDriverId !== undefined
|
||||
? { actorDriverId: entity.actorDriverId }
|
||||
: {}),
|
||||
...(entity.leagueId !== null && entity.leagueId !== undefined ? { leagueId: entity.leagueId } : {}),
|
||||
...(entity.raceId !== null && entity.raceId !== undefined ? { raceId: entity.raceId } : {}),
|
||||
...(entity.teamId !== null && entity.teamId !== undefined ? { teamId: entity.teamId } : {}),
|
||||
...(entity.position !== null && entity.position !== undefined ? { position: entity.position } : {}),
|
||||
headline: entity.headline,
|
||||
...(entity.body !== null && entity.body !== undefined ? { body: entity.body } : {}),
|
||||
...(entity.ctaLabel !== null && entity.ctaLabel !== undefined ? { ctaLabel: entity.ctaLabel } : {}),
|
||||
...(entity.ctaHref !== null && entity.ctaHref !== undefined ? { ctaHref: entity.ctaHref } : {}),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof TypeOrmSocialSchemaError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : 'Invalid persisted SocialFeedItem';
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName: 'unknown', reason: 'invalid_shape', message });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { Repository } from 'typeorm';
|
||||
|
||||
import type { FeedItem } from '@core/social/domain/types/FeedItem';
|
||||
|
||||
import { FeedItemOrmEntity } from '../entities/FeedItemOrmEntity';
|
||||
import { FriendshipOrmEntity } from '../entities/FriendshipOrmEntity';
|
||||
import { FeedItemOrmMapper } from '../mappers/FeedItemOrmMapper';
|
||||
import { TypeOrmFeedRepository } from './TypeOrmFeedRepository';
|
||||
|
||||
describe('TypeOrmFeedRepository', () => {
|
||||
it('getFeedForDriver returns mapped items for friend actor ids', async () => {
|
||||
const feedRepo = {
|
||||
find: vi.fn(),
|
||||
} as unknown as Repository<FeedItemOrmEntity>;
|
||||
|
||||
const friendshipRepo = {
|
||||
find: vi.fn(),
|
||||
} as unknown as Repository<FriendshipOrmEntity>;
|
||||
|
||||
const mapper = {
|
||||
toDomain: vi.fn(),
|
||||
} as unknown as FeedItemOrmMapper;
|
||||
|
||||
const repo = new TypeOrmFeedRepository(feedRepo, friendshipRepo, mapper);
|
||||
|
||||
const friendship = new FriendshipOrmEntity();
|
||||
friendship.driverId = '00000000-0000-0000-0000-000000000001';
|
||||
friendship.friendId = '00000000-0000-0000-0000-000000000002';
|
||||
friendship.createdAt = null;
|
||||
|
||||
friendshipRepo.find = vi.fn().mockResolvedValue([friendship]);
|
||||
|
||||
const ormItem = new FeedItemOrmEntity();
|
||||
ormItem.id = '00000000-0000-0000-0000-0000000000aa';
|
||||
ormItem.timestamp = new Date('2025-01-01T00:00:00.000Z');
|
||||
ormItem.type = 'new-result-posted';
|
||||
ormItem.actorFriendId = null;
|
||||
ormItem.actorDriverId = friendship.friendId;
|
||||
ormItem.leagueId = null;
|
||||
ormItem.raceId = null;
|
||||
ormItem.teamId = null;
|
||||
ormItem.position = null;
|
||||
ormItem.headline = 'Headline';
|
||||
ormItem.body = null;
|
||||
ormItem.ctaLabel = null;
|
||||
ormItem.ctaHref = null;
|
||||
|
||||
feedRepo.find = vi.fn().mockResolvedValue([ormItem]);
|
||||
|
||||
const mapped: FeedItem = {
|
||||
id: ormItem.id,
|
||||
timestamp: ormItem.timestamp,
|
||||
type: ormItem.type,
|
||||
actorDriverId: ormItem.actorDriverId ?? undefined,
|
||||
headline: ormItem.headline,
|
||||
};
|
||||
|
||||
mapper.toDomain = vi.fn().mockReturnValue(mapped);
|
||||
|
||||
const result = await repo.getFeedForDriver(friendship.driverId);
|
||||
|
||||
expect(friendshipRepo.find).toHaveBeenCalledWith({ where: { driverId: friendship.driverId } });
|
||||
expect(feedRepo.find).toHaveBeenCalledTimes(1);
|
||||
expect(mapper.toDomain).toHaveBeenCalledWith(ormItem);
|
||||
expect(result).toEqual([mapped]);
|
||||
});
|
||||
|
||||
it('getFeedForDriver returns [] when driver has no friends', async () => {
|
||||
const feedRepo = {
|
||||
find: vi.fn(),
|
||||
} as unknown as Repository<FeedItemOrmEntity>;
|
||||
|
||||
const friendshipRepo = {
|
||||
find: vi.fn().mockResolvedValue([]),
|
||||
} as unknown as Repository<FriendshipOrmEntity>;
|
||||
|
||||
const mapper = {
|
||||
toDomain: vi.fn(),
|
||||
} as unknown as FeedItemOrmMapper;
|
||||
|
||||
const repo = new TypeOrmFeedRepository(feedRepo, friendshipRepo, mapper);
|
||||
|
||||
const result = await repo.getFeedForDriver('00000000-0000-0000-0000-000000000001');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(feedRepo.find).not.toHaveBeenCalled();
|
||||
expect(mapper.toDomain).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { In, type Repository } from 'typeorm';
|
||||
|
||||
import type { IFeedRepository } from '@core/social/domain/repositories/IFeedRepository';
|
||||
import type { FeedItem } from '@core/social/domain/types/FeedItem';
|
||||
|
||||
import { FeedItemOrmEntity } from '../entities/FeedItemOrmEntity';
|
||||
import { FriendshipOrmEntity } from '../entities/FriendshipOrmEntity';
|
||||
import { FeedItemOrmMapper } from '../mappers/FeedItemOrmMapper';
|
||||
|
||||
export class TypeOrmFeedRepository implements IFeedRepository {
|
||||
constructor(
|
||||
private readonly feedRepo: Repository<FeedItemOrmEntity>,
|
||||
private readonly friendshipRepo: Repository<FriendshipOrmEntity>,
|
||||
private readonly mapper: FeedItemOrmMapper,
|
||||
) {}
|
||||
|
||||
async getFeedForDriver(driverId: string, limit?: number): Promise<FeedItem[]> {
|
||||
const friendships = await this.friendshipRepo.find({ where: { driverId } });
|
||||
const friendIds = friendships.map((f) => f.friendId);
|
||||
|
||||
if (friendIds.length === 0) return [];
|
||||
|
||||
const entities = await this.feedRepo.find({
|
||||
where: {
|
||||
actorDriverId: In(friendIds),
|
||||
},
|
||||
order: { timestamp: 'DESC' },
|
||||
...(typeof limit === 'number' ? { take: limit } : {}),
|
||||
});
|
||||
|
||||
return entities.map((e) => this.mapper.toDomain(e));
|
||||
}
|
||||
|
||||
async getGlobalFeed(limit?: number): Promise<FeedItem[]> {
|
||||
const entities = await this.feedRepo.find({
|
||||
order: { timestamp: 'DESC' },
|
||||
...(typeof limit === 'number' ? { take: limit } : {}),
|
||||
});
|
||||
|
||||
return entities.map((e) => this.mapper.toDomain(e));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { In, type Repository } from 'typeorm';
|
||||
|
||||
import type { ISocialGraphRepository } from '@core/social/domain/repositories/ISocialGraphRepository';
|
||||
import type { Driver } from '@core/racing/domain/entities/Driver';
|
||||
|
||||
import { DriverOrmEntity } from '@adapters/racing/persistence/typeorm/entities/DriverOrmEntity';
|
||||
import { DriverOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/DriverOrmMapper';
|
||||
|
||||
import { FriendshipOrmEntity } from '../entities/FriendshipOrmEntity';
|
||||
import { TypeOrmSocialSchemaError } from '../errors/TypeOrmSocialSchemaError';
|
||||
import { assertNonEmptyString } from '../schema/TypeOrmSocialSchemaGuards';
|
||||
|
||||
export class TypeOrmSocialGraphRepository implements ISocialGraphRepository {
|
||||
constructor(
|
||||
private readonly friendshipRepo: Repository<FriendshipOrmEntity>,
|
||||
private readonly driverRepo: Repository<DriverOrmEntity>,
|
||||
private readonly driverMapper: DriverOrmMapper,
|
||||
) {}
|
||||
|
||||
async getFriends(driverId: string): Promise<Driver[]> {
|
||||
const friendIds = await this.getFriendIds(driverId);
|
||||
if (friendIds.length === 0) return [];
|
||||
|
||||
const drivers = await this.driverRepo.find({ where: { id: In(friendIds) } });
|
||||
const byId = new Map(drivers.map((d) => [d.id, d]));
|
||||
|
||||
return friendIds
|
||||
.map((id) => byId.get(id))
|
||||
.filter((d): d is DriverOrmEntity => Boolean(d))
|
||||
.map((d) => this.driverMapper.toDomain(d));
|
||||
}
|
||||
|
||||
async getFriendIds(driverId: string): Promise<string[]> {
|
||||
const entityName = 'SocialFriendship';
|
||||
|
||||
const friendships = await this.friendshipRepo.find({ where: { driverId } });
|
||||
|
||||
return friendships.map((f) => {
|
||||
try {
|
||||
assertNonEmptyString(entityName, 'driverId', f.driverId);
|
||||
assertNonEmptyString(entityName, 'friendId', f.friendId);
|
||||
return f.friendId;
|
||||
} catch (error) {
|
||||
if (error instanceof TypeOrmSocialSchemaError) throw error;
|
||||
const message = error instanceof Error ? error.message : 'Invalid persisted SocialFriendship';
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName: 'unknown', reason: 'invalid_shape', message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getSuggestedFriends(driverId: string, limit?: number): Promise<Driver[]> {
|
||||
const directFriendIds = new Set(await this.getFriendIds(driverId));
|
||||
if (directFriendIds.size === 0) return [];
|
||||
|
||||
const friendships = await this.friendshipRepo.find({
|
||||
where: { driverId: In(Array.from(directFriendIds)) },
|
||||
});
|
||||
|
||||
const suggestions = new Map<string, number>();
|
||||
|
||||
for (const friendship of friendships) {
|
||||
const friendOfFriendId = friendship.friendId;
|
||||
if (friendOfFriendId === driverId) continue;
|
||||
if (directFriendIds.has(friendOfFriendId)) continue;
|
||||
|
||||
suggestions.set(friendOfFriendId, (suggestions.get(friendOfFriendId) ?? 0) + 1);
|
||||
}
|
||||
|
||||
const rankedIds = Array.from(suggestions.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([id]) => id);
|
||||
|
||||
const topIds = typeof limit === 'number' ? rankedIds.slice(0, limit) : rankedIds;
|
||||
if (topIds.length === 0) return [];
|
||||
|
||||
const drivers = await this.driverRepo.find({ where: { id: In(topIds) } });
|
||||
const byId = new Map(drivers.map((d) => [d.id, d]));
|
||||
|
||||
return topIds
|
||||
.map((id) => byId.get(id))
|
||||
.filter((d): d is DriverOrmEntity => Boolean(d))
|
||||
.map((d) => this.driverMapper.toDomain(d));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { TypeOrmSocialSchemaError } from '../errors/TypeOrmSocialSchemaError';
|
||||
|
||||
export function assertNonEmptyString(entityName: string, fieldName: string, value: unknown): asserts value is string {
|
||||
if (value === undefined || value === null) {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'missing' });
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'not_string' });
|
||||
}
|
||||
if (value.trim().length === 0) {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'empty_string' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertOptionalStringOrNull(
|
||||
entityName: string,
|
||||
fieldName: string,
|
||||
value: unknown,
|
||||
): asserts value is string | null | undefined {
|
||||
if (value === undefined || value === null) return;
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'not_string' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertNumber(entityName: string, fieldName: string, value: unknown): asserts value is number {
|
||||
if (value === undefined || value === null) {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'missing' });
|
||||
}
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'not_number' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertOptionalNumberOrNull(
|
||||
entityName: string,
|
||||
fieldName: string,
|
||||
value: unknown,
|
||||
): asserts value is number | null | undefined {
|
||||
if (value === undefined || value === null) return;
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'not_number' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertInteger(entityName: string, fieldName: string, value: unknown): asserts value is number {
|
||||
if (value === undefined || value === null) {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'missing' });
|
||||
}
|
||||
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'not_integer' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertOptionalIntegerOrNull(
|
||||
entityName: string,
|
||||
fieldName: string,
|
||||
value: unknown,
|
||||
): asserts value is number | null | undefined {
|
||||
if (value === undefined || value === null) return;
|
||||
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'not_integer' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertBoolean(entityName: string, fieldName: string, value: unknown): asserts value is boolean {
|
||||
if (value === undefined || value === null) {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'missing' });
|
||||
}
|
||||
if (typeof value !== 'boolean') {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'not_boolean' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertDate(entityName: string, fieldName: string, value: unknown): asserts value is Date {
|
||||
if (value === undefined || value === null) {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'missing' });
|
||||
}
|
||||
if (!(value instanceof Date)) {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'not_date' });
|
||||
}
|
||||
if (Number.isNaN(value.getTime())) {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'invalid_date' });
|
||||
}
|
||||
}
|
||||
|
||||
export function assertEnumValue<TAllowed extends string>(
|
||||
entityName: string,
|
||||
fieldName: string,
|
||||
value: unknown,
|
||||
allowed: readonly TAllowed[],
|
||||
): asserts value is TAllowed {
|
||||
if (value === undefined || value === null) {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'missing' });
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'not_string' });
|
||||
}
|
||||
if (!allowed.includes(value as TAllowed)) {
|
||||
throw new TypeOrmSocialSchemaError({ entityName, fieldName, reason: 'invalid_enum_value' });
|
||||
}
|
||||
}
|
||||
|
||||
export const TypeOrmSocialSchemaGuards = {
|
||||
assertNonEmptyString,
|
||||
assertOptionalStringOrNull,
|
||||
assertNumber,
|
||||
assertOptionalNumberOrNull,
|
||||
assertInteger,
|
||||
assertOptionalIntegerOrNull,
|
||||
assertBoolean,
|
||||
assertDate,
|
||||
assertEnumValue,
|
||||
};
|
||||
Reference in New Issue
Block a user