90 lines
2.8 KiB
TypeScript
90 lines
2.8 KiB
TypeScript
import { ASTRALVIEW_SCHEMA_VERSION, type Screen } from './schema';
|
|
|
|
/**
|
|
* Placeholder for future schema migrations.
|
|
* Keep it pure and versioned.
|
|
*/
|
|
function toNumber(v: unknown, fallback: number): number {
|
|
return typeof v === 'number' && Number.isFinite(v) ? v : fallback;
|
|
}
|
|
|
|
function asString(v: unknown): string {
|
|
return typeof v === 'string' ? v : '';
|
|
}
|
|
|
|
function sanitizeScreenV1(input: Screen): Screen {
|
|
const allowedTypes = new Set(['text', 'image', 'iframe', 'video']);
|
|
|
|
const nodes = Array.isArray(input.nodes) ? input.nodes : [];
|
|
|
|
const sanitizedNodes = nodes
|
|
.filter((n): n is Screen['nodes'][number] => !!n && typeof n === 'object')
|
|
.filter((n) => allowedTypes.has((n as { type?: unknown }).type as string))
|
|
.map((n, i) => {
|
|
const anyNode = n as unknown as Record<string, unknown>;
|
|
const rect = (anyNode.rect ?? {}) as Record<string, unknown>;
|
|
|
|
const base = {
|
|
...n,
|
|
id: asString(anyNode.id) || `import_${Date.now()}_${i}`,
|
|
rect: {
|
|
x: toNumber(rect.x, 0),
|
|
y: toNumber(rect.y, 0),
|
|
w: toNumber(rect.w, 100),
|
|
h: toNumber(rect.h, 60),
|
|
},
|
|
};
|
|
|
|
// Ensure required props exist for media widgets to prevent runtime crashes.
|
|
const props = (base as unknown as { props?: unknown }).props;
|
|
const propsObj = (props && typeof props === 'object' ? (props as Record<string, unknown>) : {}) as Record<
|
|
string,
|
|
unknown
|
|
>;
|
|
|
|
if (base.type === 'image') {
|
|
return { ...base, props: { ...propsObj, src: asString(propsObj.src) } };
|
|
}
|
|
if (base.type === 'iframe') {
|
|
return { ...base, props: { ...propsObj, src: asString(propsObj.src) } };
|
|
}
|
|
if (base.type === 'video') {
|
|
return { ...base, props: { ...propsObj, src: asString(propsObj.src) } };
|
|
}
|
|
if (base.type === 'text') {
|
|
return { ...base, props: { ...propsObj, text: asString(propsObj.text) || '' } };
|
|
}
|
|
|
|
return base;
|
|
});
|
|
|
|
return {
|
|
...input,
|
|
id: input.id || `av_${Date.now()}`,
|
|
name: input.name || 'Untitled Screen',
|
|
width: toNumber(input.width, 1920),
|
|
height: toNumber(input.height, 1080),
|
|
nodes: sanitizedNodes,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Placeholder for future schema migrations.
|
|
* Keep it pure and versioned.
|
|
*/
|
|
export function migrateScreen(input: unknown): Screen {
|
|
const s = input as Partial<Screen>;
|
|
if (!s || typeof s !== 'object') {
|
|
throw new Error('Invalid screen: not an object');
|
|
}
|
|
const version = (s as Screen).version;
|
|
if (version === ASTRALVIEW_SCHEMA_VERSION) {
|
|
// Even for the current version, we defensively sanitize to avoid runtime
|
|
// "assertNever" crashes when users import malformed JSON.
|
|
return sanitizeScreenV1(s as Screen);
|
|
}
|
|
|
|
// Future: apply incremental migrations.
|
|
throw new Error(`Unsupported screen version: ${String(version)}`);
|
|
}
|