AstralView/packages/sdk/src/core/migrate.ts

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)}`);
}