UNPKG

@statezero/core

Version:

The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate

218 lines (217 loc) 8.03 kB
import { configInstance } from "../../config.js"; import { isNil } from "lodash-es"; import { DateParsingHelpers } from "./dates.js"; /** * File field serializer - handles both camelCase (frontend) and snake_case (backend) formats */ export const fileFieldSerializer = { // Store backend snake_case format internally for consistency with API toInternal: (value, context = {}) => { // Handle plain strings - throw error if (typeof value === "string") { throw new Error("File field expects a file object, not a string path"); } if (isNil(value)) { return null; } value = { file_path: value.filePath || value.file_path, file_name: value.fileName || value.file_name, file_url: value.fileUrl || value.file_url, size: value.size, mime_type: value.mimeType || value.mime_type, }; return value; }, // Return object with both formats for maximum compatibility toLive: (value, context = {}) => { if (!value || typeof value !== "object") return value; // Build the proper file URL using the same logic as FileObject const backendKey = context.model?.constructor?.configKey || "default"; const fullFileUrl = value.file_url ? configInstance.buildFileUrl(value.file_url, backendKey) : null; return { // snake_case (backend format) file_path: value.file_path, file_name: value.file_name, file_url: fullFileUrl, // Use the built URL here size: value.size, mime_type: value.mime_type, // camelCase (frontend format) - for compatibility filePath: value.file_path, fileName: value.file_name, fileUrl: fullFileUrl, // Use the built URL here too mimeType: value.mime_type, }; }, }; /** * Date/DateTime field serializer - uses DateParsingHelpers like the model does */ export const dateFieldSerializer = { toInternal: (value, context = {}) => { if (isNil(value)) return value; // If it's already a string, keep it (from API) if (typeof value === "string") return value; // If it's a Date object, serialize it using DateParsingHelpers if (value instanceof Date) { const { model, field } = context; if (model?.schema) { return DateParsingHelpers.serializeDate(value, field, model.schema); } // Fallback if no schema context return value.toISOString(); } return value; }, toLive: (value, context = {}) => { if (isNil(value) || typeof value !== "string") return value; const { model, field } = context; if (model?.schema) { // Use DateParsingHelpers like the model does return DateParsingHelpers.parseDate(value, field, model.schema); } // Fallback parsing if no schema context try { return new Date(value); } catch (e) { console.warn(`Failed to parse date: ${value}`); return value; } }, }; /** * Foreign Key / One-to-One field serializer - follows existing relationship logic */ export const relationshipFieldSerializer = { toInternal: (value, context = {}) => { if (isNil(value)) return value; // Extract PK or use value directly const pk = value.pk || value; // Assert it's a valid PK type if (typeof pk !== "string" && typeof pk !== "number") { throw new Error(`Invalid primary key type for relationship field: expected string or number, got ${typeof pk}`); } return pk; }, toLive: (value, context = {}) => { if (isNil(value)) return value; // Follow the exact same pattern as the original getField code const { model, field } = context; if (model.relationshipFields.has(field) && value) { const fieldInfo = model.relationshipFields.get(field); // For foreign-key and one-to-one, value is guaranteed to be a plain PK return fieldInfo.ModelClass().fromPk(value); } return value; }, }; /** * Many-to-Many field serializer - follows existing m2m logic */ export const m2mFieldSerializer = { toInternal: (value, context = {}) => { if (isNil(value) || !Array.isArray(value)) return value; return value.map((item) => { // Extract PK or use item directly const pk = item.pk || item; // Assert it's a valid PK type if (typeof pk !== "string" && typeof pk !== "number") { throw new Error(`Invalid primary key type for m2m field: expected string or number, got ${typeof pk}`); } return pk; }); }, toLive: (value, context = {}) => { if (isNil(value)) return value; // Follow the exact same pattern as the original getField code const { model, field } = context; if (model.relationshipFields.has(field) && value) { const fieldInfo = model.relationshipFields.get(field); // Data corruption check like the original if (!Array.isArray(value) && value) { throw new Error(`Data corruption: m2m field for ${model.modelName} stored as ${value}`); } // Map each pk to the full model object - exactly like original return value.map((pk) => fieldInfo.ModelClass().fromPk(pk)); } return value; }, }; const serializers = { string: { "file-path": fileFieldSerializer, "image-path": fileFieldSerializer, "date": dateFieldSerializer, "date-time": dateFieldSerializer, "foreign-key": relationshipFieldSerializer, "one-to-one": relationshipFieldSerializer, }, integer: { "foreign-key": relationshipFieldSerializer, "one-to-one": relationshipFieldSerializer, }, // Add other PK types as needed uuid: { "foreign-key": relationshipFieldSerializer, "one-to-one": relationshipFieldSerializer, }, array: { "many-to-many": m2mFieldSerializer, }, }; export class ModelSerializer { constructor(modelClass) { this.modelClass = modelClass; } toInternalField(field, value, context = {}) { const fieldType = this.modelClass.schema?.properties[field]?.type; const fieldFormat = this.modelClass.schema?.properties[field]?.format; if (serializers[fieldType] && serializers[fieldType][fieldFormat]) { return serializers[fieldType][fieldFormat].toInternal(value, { model: this.modelClass, field, ...context, }); } // Fallback to default serialization return value; } toInternal(data) { const serializedData = {}; for (const field in data) { serializedData[field] = this.toInternalField(field, data[field]); } return serializedData; } toLiveField(field, value, context = {}) { const fieldType = this.modelClass.schema?.properties[field]?.type; const fieldFormat = this.modelClass.schema?.properties[field]?.format; if (serializers[fieldType] && serializers[fieldType][fieldFormat]) { return serializers[fieldType][fieldFormat].toLive(value, { model: this.modelClass, field, ...context, }); } // Fallback to default serialization return value; } toLive(data) { const serializedData = {}; for (const field in data) { serializedData[field] = this.toLiveField(field, data[field]); } return serializedData; } }