deserialize.ts
1 import { SchemaError } from "./errors/SchemaError"; 2 import { getModelMetadata } from "./metadata"; 3 import { SchemaType } from "./schema/type"; 4 5 interface CompiledField { 6 defaultValue?: (() => any) | any; 7 deserializer?: (value: any, self: any) => any; 8 key: string; 9 rename?: string; 10 schema: SchemaType; 11 } 12 13 interface CompiledModel { 14 deserializer: (data: any) => any; 15 fields: Array<CompiledField>; 16 name: string; 17 prototype: unknown; 18 } 19 20 type Ctor<T> = new (...args: any[]) => T; 21 22 const cache = new WeakMap<Ctor<any>, CompiledModel>(); 23 24 interface Deserializer { 25 body: string; 26 helpers: { 27 arrays: Record<string, unknown>; 28 defaults: Record<string, unknown>; 29 deserializers: Record<string, unknown>; 30 references: Record<string, unknown>; 31 SchemaError: typeof SchemaError; 32 }; 33 } 34 35 /** 36 * Deserialize an object {@link data} into your a given {@link Model}. 37 * 38 * @param Model Destination model you want to deserialize to. 39 * @param data Original object, could be anything as long as it matches your model schema. 40 * 41 * @example 42 * class MyModel { 43 * \@rename("who") 44 * hello = t.string() 45 * } 46 * 47 * const model = deserialize(MyModel, { 48 * who: "world" 49 * }); 50 * 51 * console.log(model.hello) // "world" 52 */ 53 export function deserialize<T extends new (...args: any[]) => any>(Model: T, data: any): InstanceType<T> { 54 return getCompiledModel(Model).deserializer(data); 55 } 56 57 const decodeArray = ( 58 Model: CompiledModel, 59 field: string, 60 value: any, 61 schema: SchemaType 62 ): any => { 63 if (!Array.isArray(value)) { 64 throw new SchemaError(Model.name, field, `expected array but got "${typeof value}"`); 65 } 66 67 return value.map((item) => { 68 if (schema.optional && item == null) { 69 return null; 70 } 71 72 if (schema.array) { 73 return decodeArray(Model, field, item, schema.array); 74 } 75 else if (schema.reference) { 76 return deserialize(schema.reference, item); 77 } 78 79 return item; 80 }); 81 }; 82 83 function generateDeserializerFunction(compiled: CompiledModel): Deserializer { 84 // mod = model 85 // proto = prototype 86 // _ = helpers 87 // $ = data 88 // $[i] = model/data key/value 89 // 90 // We're doing this to prevent conflicting names. 91 92 let body = "const mod=Object.create(proto);"; 93 94 const _ = { 95 arrays: {} as Record<string, any>, 96 defaults: {} as Record<string, any>, 97 deserializers: {} as Record<string, any>, 98 references: {} as Record<string, any>, 99 SchemaError 100 }; 101 102 for (let i = 0; i < compiled.fields.length; i++) { 103 const { 104 defaultValue, 105 deserializer, 106 key: unsafeFieldKey, 107 rename: unsafeRenameFieldKey, 108 schema 109 } = compiled.fields[i]; 110 111 const varn = `$${i}`; 112 const safeFieldKey = JSON.stringify(unsafeFieldKey); 113 body += `let ${varn}=$[${unsafeRenameFieldKey ? JSON.stringify(unsafeRenameFieldKey) : safeFieldKey}];`; 114 115 if (defaultValue !== void 0) { 116 _.defaults[varn] = defaultValue; 117 118 if (typeof defaultValue === "function") { 119 body += `${varn}??=_.defaults.${varn}();`; 120 } 121 else { 122 body += `${varn}??=_.defaults.${varn};`; 123 } 124 125 body += `if(${varn}===null)throw new _.SchemaError("${compiled.name}",${safeFieldKey},"default value cannot be null");`; 126 127 if (schema.typeof) { 128 body += `if(typeof ${varn}!=="${schema.typeof}")throw new _.SchemaError("${compiled.name}",${safeFieldKey},"default value has incorrect type, got \\""+typeof ${varn}+"\\" and expected \\"${schema.typeof}\\"");`; 129 } 130 131 if (schema.instanceof) { 132 _.defaults[`${varn}$1`] = schema.instanceof; 133 body += `if(!(${varn} instanceof _.defaults["${varn}$1"]))throw new _.SchemaError("${compiled.name}",${safeFieldKey},"default value is not an instance of \\"${schema.instanceof.name}\\"");`; 134 } 135 136 if (schema.enum) { 137 _.defaults[`${varn}$2`] = Object.values(schema.enum); 138 body += `if(!_.defaults.${varn}$2.includes(${varn}))throw new _.SchemaError("${compiled.name}",${safeFieldKey},\`default value (\${safe}) does not match any value of provided enum\`);`; 139 } 140 141 if (schema.reference) { 142 body += `throw new _.SchemaError("${compiled.name}",${safeFieldKey},"default value is not allowed on reference fields (${safeFieldKey})");`; 143 } 144 } 145 else if (!schema.optional) { 146 body += `if(${varn}==null)throw new _.SchemaError("${compiled.name}",${safeFieldKey},\`not optional but got "\${${varn}}"\`);`; 147 } 148 else { 149 body += `if(${varn}==null)${varn}=null;`; 150 } 151 152 // Handle transformations (only for non-null values) 153 if (deserializer) { 154 _.deserializers[varn] = deserializer; 155 body += `if(${varn}!=null)${varn}=_.deserializers.${varn}(${varn},mod);`; 156 } 157 else if (schema.array) { 158 _.arrays[varn] = (value: any) => 159 decodeArray(compiled, unsafeFieldKey, value, schema.array!); 160 body += `if(${varn}!=null)${varn}=_.arrays.${varn}(${varn});`; 161 } 162 else if (schema.reference) { 163 _.references[varn] = (value: any) => 164 deserialize(schema.reference!, value); 165 body += `if(${varn}!=null)${varn}=_.references.${varn}(${varn});`; 166 } 167 168 body += `mod[${safeFieldKey}]=${varn};`; 169 } 170 171 body += "return mod"; 172 return { body, helpers: _ }; 173 } 174 175 function getCompiledModel<T extends Ctor<any>>(Model: T): CompiledModel { 176 let compiled = cache.get(Model); 177 if (compiled?.deserializer) return compiled; 178 179 if (!compiled) { 180 const instance = new Model(); 181 const metadata = new Map(getModelMetadata(instance).map((m) => [m.key, m] as const)); 182 183 const fields: Array<CompiledField> = []; 184 for (const [key, schema] of Object.entries(instance)) { 185 if (!(schema instanceof SchemaType)) continue; 186 187 const info = metadata.get(key); 188 fields.push({ 189 defaultValue: info?.defaultValue, 190 deserializer: info?.deserializer, 191 key, 192 rename: info?.rename, 193 schema 194 }); 195 } 196 197 compiled = { 198 fields, 199 name: Model.name, 200 prototype: Model.prototype 201 } as CompiledModel; 202 } 203 204 const { body, helpers } = generateDeserializerFunction(compiled); 205 const fn = new Function("$", "proto", "_", body); 206 compiled.deserializer = (data: unknown) => fn(data, compiled.prototype, helpers); 207 cache.set(Model, compiled); 208 return compiled; 209 }