fix(canvas): capture schema migration data before lossy serialization
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { Capability, CapabilityHandler, ComponentDefinition, Data, ElementRecord, Feature, Registry, ReadonlyValueSet } from './types.ts';
|
||||
import { clone, fault, freeze, readMap, readSet, validJson } from './values.ts';
|
||||
import type { Capability, CapabilityHandler, ComponentDefinition, Data, ElementRecord, Feature, Json, Registry, ReadonlyValueSet } from './types.ts';
|
||||
import { clone, fault, freeze, readMap, readSet } from './values.ts';
|
||||
|
||||
export function defineFeature(id: string, handlers: Partial<Record<Capability, CapabilityHandler>>): Feature {
|
||||
if (!id || !Object.values(handlers).length || Object.values(handlers).some(value => typeof value !== 'function')) throw fault('invalid_feature');
|
||||
@@ -44,15 +44,36 @@ export function createRegistry(definitions: readonly ComponentDefinition[]): Reg
|
||||
return Object.freeze({ components: readMap(components), capabilities: readMap(capabilities), handlers: readMap(handlers), resolve,
|
||||
supports: (element: ElementRecord, capability: Capability) => resolve(element) === 'known' && !!capabilities.get(element.componentId)?.has(capability) });
|
||||
}
|
||||
/** Capture each field once; JSON.stringify would silently erase invalid values. */
|
||||
function captureMigrationData(input:unknown,id:string):Data {
|
||||
const invalid=():never=>{throw fault('invalid_migration',[id]);};
|
||||
const capture=(value:unknown,depth=0):Json=>{
|
||||
if(depth>64)return invalid();
|
||||
if(value===null||typeof value==='string'||typeof value==='boolean')return value;
|
||||
if(typeof value==='number')return Number.isFinite(value)?value:invalid();
|
||||
if(!value||typeof value!=='object')return invalid();
|
||||
if(Object.getOwnPropertySymbols(value).some(key=>Object.prototype.propertyIsEnumerable.call(value,key)))return invalid();
|
||||
if(Array.isArray(value)){
|
||||
if(Object.keys(value).length!==value.length)return invalid();
|
||||
const result:Json[]=[];for(let index=0;index<value.length;index++){if(!Object.hasOwn(value,index))return invalid();result.push(capture(value[index],depth+1));}return freeze(result);
|
||||
}
|
||||
if(Object.getPrototypeOf(value)!==Object.prototype)return invalid();
|
||||
const source=value as Record<string,unknown>;
|
||||
return freeze(Object.fromEntries(Object.keys(source).map(key=>[key,capture(source[key],depth+1)])));
|
||||
};
|
||||
const result=capture(input);
|
||||
if(!result||typeof result!=='object'||Array.isArray(result))return invalid();
|
||||
return result;
|
||||
}
|
||||
|
||||
export function migrateElement(element: ElementRecord, registry: Registry): ElementRecord {
|
||||
const definition = registry.components.get(element.componentId);
|
||||
if (!definition || element.dataVersion > definition.dataVersion) return element;
|
||||
let data: Data = freeze(clone(element.data)), version = element.dataVersion;
|
||||
let data = captureMigrationData(element.data,element.id), version = element.dataVersion;
|
||||
while (version < definition.dataVersion) {
|
||||
const migrate = definition.migrations?.[version];
|
||||
if (!migrate) throw fault('missing_migration', [element.id]);
|
||||
data = freeze(clone(migrate(data))); version++;
|
||||
if (!validJson(data)) throw fault('invalid_migration', [element.id]);
|
||||
data = captureMigrationData(migrate(data),element.id); version++;
|
||||
}
|
||||
if (!definition.validate(data)) throw fault('invalid_component_data', [element.id]);
|
||||
return freeze({ ...element, dataVersion: version, data });
|
||||
|
||||
Reference in New Issue
Block a user