Some checks failed
CI / skip-ci-check (push) Successful in 1m27s
CI / skip-ci-check (pull_request) Successful in 1m27s
CI / lint-and-type-check (pull_request) Has been cancelled
CI / python-lint (pull_request) Has been cancelled
CI / test-backend (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / secret-scanning (pull_request) Has been cancelled
CI / dependency-scan (pull_request) Has been cancelled
CI / sast-scan (pull_request) Has been cancelled
CI / workflow-summary (pull_request) Has been cancelled
CI / lint-and-type-check (push) Successful in 2m4s
CI / python-lint (push) Successful in 1m53s
CI / test-backend (push) Successful in 2m37s
CI / build (push) Failing after 2m13s
CI / secret-scanning (push) Successful in 1m40s
CI / dependency-scan (push) Successful in 1m34s
CI / sast-scan (push) Successful in 2m42s
CI / workflow-summary (push) Successful in 1m26s
This commit modifies the `.gitignore` file to exclude Python library directories while ensuring the viewer-frontend's `lib` directory is not ignored. It also updates the `package.json` to activate the virtual environment during backend tests, improving the testing process. Additionally, the CI workflow is enhanced to prevent duplicate runs for branches with open pull requests. Various components in the viewer frontend are updated to ensure consistent naming conventions and improve type safety. These changes contribute to a cleaner codebase and a more efficient development workflow.
234 lines
5.8 KiB
TypeScript
234 lines
5.8 KiB
TypeScript
/**
|
|
* Serialization utilities for converting Prisma objects to plain JavaScript objects
|
|
* that can be safely passed from Server Components to Client Components in Next.js.
|
|
*
|
|
* Handles:
|
|
* - Decimal objects -> numbers
|
|
* - Date objects -> ISO strings
|
|
* - Nested structures (photos, faces, people, tags)
|
|
*/
|
|
|
|
type Decimal = {
|
|
toNumber(): number;
|
|
toString(): string;
|
|
};
|
|
|
|
/**
|
|
* Checks if a value is a Prisma Decimal object
|
|
*/
|
|
function isDecimal(value: any): value is Decimal {
|
|
return (
|
|
value !== null &&
|
|
typeof value === 'object' &&
|
|
typeof value.toNumber === 'function' &&
|
|
typeof value.toString === 'function'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Converts a Decimal to a number, handling null/undefined
|
|
*/
|
|
function decimalToNumber(value: any): number | null {
|
|
if (value === null || value === undefined) {
|
|
return null;
|
|
}
|
|
if (isDecimal(value)) {
|
|
return value.toNumber();
|
|
}
|
|
if (typeof value === 'number') {
|
|
return value;
|
|
}
|
|
// Fallback: try to parse as number
|
|
const parsed = Number(value);
|
|
return isNaN(parsed) ? null : parsed;
|
|
}
|
|
|
|
/**
|
|
* Serializes a Date object to an ISO string
|
|
*/
|
|
function serializeDate(value: any): string | null {
|
|
if (value === null || value === undefined) {
|
|
return null;
|
|
}
|
|
if (value instanceof Date) {
|
|
return value.toISOString();
|
|
}
|
|
if (typeof value === 'string') {
|
|
return value;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Serializes a Person object
|
|
*/
|
|
function serializePerson(person: any): any {
|
|
if (!person) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: person.id,
|
|
first_name: person.first_name,
|
|
last_name: person.last_name,
|
|
middle_name: person.middle_name ?? null,
|
|
maiden_name: person.maiden_name ?? null,
|
|
date_of_birth: serializeDate(person.date_of_birth),
|
|
created_date: serializeDate(person.created_date),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Serializes a Face object, converting Decimal fields to numbers
|
|
*/
|
|
function serializeFace(face: any): any {
|
|
if (!face) {
|
|
return null;
|
|
}
|
|
|
|
const serialized: any = {
|
|
id: face.id,
|
|
photo_id: face.photo_id,
|
|
person_id: face.person_id ?? null,
|
|
location: face.location,
|
|
confidence: decimalToNumber(face.confidence) ?? 0,
|
|
quality_score: decimalToNumber(face.quality_score) ?? 0,
|
|
is_primary_encoding: face.is_primary_encoding ?? false,
|
|
detector_backend: face.detector_backend,
|
|
model_name: face.model_name,
|
|
face_confidence: decimalToNumber(face.face_confidence) ?? 0,
|
|
exif_orientation: face.exif_orientation ?? null,
|
|
pose_mode: face.pose_mode,
|
|
yaw_angle: decimalToNumber(face.yaw_angle),
|
|
pitch_angle: decimalToNumber(face.pitch_angle),
|
|
roll_angle: decimalToNumber(face.roll_angle),
|
|
landmarks: face.landmarks ?? null,
|
|
identified_by_user_id: face.identified_by_user_id ?? null,
|
|
excluded: face.excluded ?? false,
|
|
};
|
|
|
|
// Handle nested Person object (if present)
|
|
if (face.Person) {
|
|
serialized.Person = serializePerson(face.Person);
|
|
} else if (face.person) {
|
|
serialized.person = serializePerson(face.person);
|
|
}
|
|
|
|
return serialized;
|
|
}
|
|
|
|
/**
|
|
* Serializes a Tag object
|
|
*/
|
|
function serializeTag(tag: any): any {
|
|
if (!tag) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: tag.id,
|
|
tagName: tag.tag_name || tag.tagName,
|
|
created_date: serializeDate(tag.created_date),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Serializes a PhotoTagLinkage object
|
|
*/
|
|
function serializePhotoTagLinkage(linkage: any): any {
|
|
if (!linkage) {
|
|
return null;
|
|
}
|
|
|
|
const serialized: any = {
|
|
linkage_id: linkage.linkage_id ?? linkage.id,
|
|
photo_id: linkage.photo_id,
|
|
tag_id: linkage.tag_id,
|
|
linkage_type: linkage.linkage_type ?? 0,
|
|
created_date: serializeDate(linkage.created_date),
|
|
};
|
|
|
|
// Handle nested Tag object (if present)
|
|
if (linkage.Tag || linkage.tag) {
|
|
serialized.tag = serializeTag(linkage.Tag || linkage.tag);
|
|
// Also keep Tag for backward compatibility
|
|
serialized.Tag = serialized.tag;
|
|
}
|
|
if (linkage.Tag || linkage.tag) {
|
|
// Also keep Tag for backward compatibility
|
|
serialized.Tag = serialized.tag;
|
|
}
|
|
|
|
return serialized;
|
|
}
|
|
|
|
/**
|
|
* Serializes a single Photo object with all nested structures
|
|
*/
|
|
export function serializePhoto(photo: any): any {
|
|
if (!photo) {
|
|
return null;
|
|
}
|
|
|
|
const serialized: any = {
|
|
id: photo.id,
|
|
path: photo.path,
|
|
filename: photo.filename,
|
|
date_added: serializeDate(photo.date_added),
|
|
date_taken: serializeDate(photo.date_taken),
|
|
processed: photo.processed ?? false,
|
|
media_type: photo.media_type ?? null,
|
|
};
|
|
|
|
// Handle Face array (can be named Face or faces)
|
|
if (photo.Face && Array.isArray(photo.Face)) {
|
|
serialized.Face = photo.Face.map((face: any) => serializeFace(face));
|
|
} else if (photo.faces && Array.isArray(photo.faces)) {
|
|
serialized.faces = photo.faces.map((face: any) => serializeFace(face));
|
|
}
|
|
|
|
// Handle PhotoTagLinkage array (can be named PhotoTagLinkage or photoTags)
|
|
if (photo.PhotoTagLinkage && Array.isArray(photo.PhotoTagLinkage)) {
|
|
serialized.PhotoTagLinkage = photo.PhotoTagLinkage.map((linkage: any) =>
|
|
serializePhotoTagLinkage(linkage)
|
|
);
|
|
} else if (photo.photoTags && Array.isArray(photo.photoTags)) {
|
|
serialized.photoTags = photo.photoTags.map((linkage: any) =>
|
|
serializePhotoTagLinkage(linkage)
|
|
);
|
|
}
|
|
|
|
return serialized;
|
|
}
|
|
|
|
/**
|
|
* Serializes an array of Photo objects
|
|
*/
|
|
export function serializePhotos(photos: any[]): any[] {
|
|
if (!Array.isArray(photos)) {
|
|
return [];
|
|
}
|
|
|
|
return photos.map((photo) => serializePhoto(photo));
|
|
}
|
|
|
|
/**
|
|
* Serializes an array of Person objects
|
|
*/
|
|
export function serializePeople(people: any[]): any[] {
|
|
if (!Array.isArray(people)) {
|
|
return [];
|
|
}
|
|
return people.map((person) => serializePerson(person));
|
|
}
|
|
|
|
/**
|
|
* Serializes an array of Tag objects
|
|
*/
|
|
export function serializeTags(tags: any[]): any[] {
|
|
if (!Array.isArray(tags)) {
|
|
return [];
|
|
}
|
|
return tags.map((tag) => serializeTag(tag));
|
|
}
|