@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
JavaScript
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;
}
}