diff --git a/packages/bson/src/bson-deserializer-templates.ts b/packages/bson/src/bson-deserializer-templates.ts index b14e392ca..c4d0f80d9 100644 --- a/packages/bson/src/bson-deserializer-templates.ts +++ b/packages/bson/src/bson-deserializer-templates.ts @@ -1,9 +1,6 @@ -import { isPropertyMemberType } from '@deepkit/type'; import { binaryBigIntAnnotation, BinaryBigIntType, - buildFunction, - callExtractedFunctionIfAvailable, collapsePath, ContainerAccessor, createTypeGuardFunction, @@ -12,13 +9,13 @@ import { excludedAnnotation, executeTemplates, extendTemplateLiteral, - extractStateToFunctionAndCallIt, getIndexCheck, getNameExpression, getStaticDefaultCodeForProperty, hasDefaultValue, isNullable, isOptional, + isPropertyMemberType, mongoIdAnnotation, ReflectionClass, ReflectionKind, @@ -27,6 +24,7 @@ import { sortSignatures, TemplateState, Type, + TypeArray, TypeClass, TypeGuardRegistry, TypeIndexSignature, @@ -37,7 +35,7 @@ import { TypeTemplateLiteral, TypeTuple, TypeUnion, - uuidAnnotation + uuidAnnotation, } from '@deepkit/type'; import { seekElementSize } from './continuation.js'; import { BSONType, digitByteSize, isSerializable } from './utils.js'; @@ -531,7 +529,8 @@ export function bsonTypeGuardTuple(type: TypeTuple, state: TemplateState) { `); } -export function deserializeArray(elementType: Type, state: TemplateState) { +export function deserializeArray(type: TypeArray, state: TemplateState) { + const elementType = type.type; const result = state.compilerContext.reserveName('result'); const v = state.compilerContext.reserveName('v'); const i = state.compilerContext.reserveName('i'); @@ -567,7 +566,8 @@ export function deserializeArray(elementType: Type, state: TemplateState) { * This array type guard goes through all array elements in order to determine the correct type. * This is only necessary when a union has at least 2 array members, otherwise a simple array check is enough. */ -export function bsonTypeGuardArray(elementType: Type, state: TemplateState) { +export function bsonTypeGuardArray(type: TypeArray, state: TemplateState) { + const elementType = type.type; const v = state.compilerContext.reserveName('v'); const i = state.compilerContext.reserveName('i'); state.setContext({ digitByteSize, seekElementSize }); @@ -643,11 +643,6 @@ export function deserializeObjectLiteral(type: TypeClass | TypeObjectLiteral, st // return; // } // } - - if (callExtractedFunctionIfAvailable(state, type)) return; - const extract = extractStateToFunctionAndCallIt(state, type); - state = extract.state; - const lines: string[] = []; const signatures: TypeIndexSignature[] = []; const object = state.compilerContext.reserveName('object'); @@ -843,8 +838,6 @@ export function deserializeObjectLiteral(type: TypeClass | TypeObjectLiteral, st state.elementType = oldElementType; } `); - - extract.setFunction(buildFunction(state, type)); } export function bsonTypeGuardObjectLiteral(type: TypeClass | TypeObjectLiteral, state: TemplateState) { @@ -862,10 +855,6 @@ export function bsonTypeGuardObjectLiteral(type: TypeClass | TypeObjectLiteral, // return; // } - if (callExtractedFunctionIfAvailable(state, type)) return; - const extract = extractStateToFunctionAndCallIt(state, type); - state = extract.state; - const lines: string[] = []; const signatures: TypeIndexSignature[] = []; const valid = state.compilerContext.reserveName('valid'); @@ -987,8 +976,6 @@ export function bsonTypeGuardObjectLiteral(type: TypeClass | TypeObjectLiteral, ${state.setter} = ${valid}; } `); - - extract.setFunction(buildFunction(state, type)); } export function bsonTypeGuardForBsonTypes(types: BSONType[]): (type: Type, state: TemplateState) => void { diff --git a/packages/bson/src/bson-deserializer.ts b/packages/bson/src/bson-deserializer.ts index 92514e2ae..f19f13032 100644 --- a/packages/bson/src/bson-deserializer.ts +++ b/packages/bson/src/bson-deserializer.ts @@ -40,5 +40,6 @@ export function getBSONDeserializer(serializer: BSONBinarySerializer = bsonBi } export function deserializeBSON(data: Uint8Array, offset?: number, serializer: BSONBinarySerializer = bsonBinarySerializer, receiveType?: ReceiveType): T { - return getBSONDeserializer(serializer, receiveType)(data, offset) as T; + const deserialize = getBSONDeserializer(serializer, receiveType); + return deserialize(data, offset) as T; } diff --git a/packages/bson/src/bson-serializer.ts b/packages/bson/src/bson-serializer.ts index 7870dd02a..f2c0e8f2a 100644 --- a/packages/bson/src/bson-serializer.ts +++ b/packages/bson/src/bson-serializer.ts @@ -8,36 +8,34 @@ * You should have received a copy of the MIT License along with this program. */ -import { hasProperty } from '@deepkit/core'; -import { CompilerContext, isArray, isIterable, isObject, toFastProperties } from '@deepkit/core'; -import { isPropertyMemberType } from '@deepkit/type'; +import { CompilerContext, hasProperty, isArray, isIterable, isObject, toFastProperties } from '@deepkit/core'; import { binaryBigIntAnnotation, BinaryBigIntType, - buildFunction, - callExtractedFunctionIfAvailable, - collapsePath, ContainerAccessor, - copyAndSetParent, createReference, excludedAnnotation, executeTemplates, - extractStateToFunctionAndCallIt, + forwardMapToArray, + forwardSetToArray, getIndexCheck, getNameExpression, + getPropertyNameString, getTypeJitContainer, handleUnion, - hasCircularReference, + hasDefaultValue, isBackReferenceType, isBinaryBigIntType, isMongoIdType, isNullable, isOptional, + isPropertyMemberType, isReferenceHydrated, isReferenceInstance, isReferenceType, isUUIDType, - JitStack, memberNameToString, + JitStack, + memberNameToString, mongoIdAnnotation, NamingStrategy, ReceiveType, @@ -52,6 +50,7 @@ import { TemplateRegistry, TemplateState, Type, + TypeArray, TypeBigInt, TypeClass, TypeGuardRegistry, @@ -62,7 +61,7 @@ import { TypeTuple, UnpopulatedCheck, unpopulatedSymbol, - uuidAnnotation + uuidAnnotation, } from '@deepkit/type'; import { bsonTypeGuardArray, @@ -87,7 +86,7 @@ import { deserializeTemplateLiteral, deserializeTuple, deserializeUndefined, - deserializeUnion + deserializeUnion, } from './bson-deserializer-templates.js'; import { seekElementSize } from './continuation.js'; import { BSONError } from './model.js'; @@ -244,11 +243,35 @@ export class ValueWithBSONSerializer { export class Writer { public dataView: DataView; + public typeOffset: number = 0; constructor(public buffer: Uint8Array, public offset: number = 0) { this.dataView = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); } + /** + * If typeOffset is defined, the type will be written at this offset. + * Useful for writing type information for members in array/object literals. + */ + writeType(v: number) { + if (this.typeOffset !== 0) { + this.buffer[this.typeOffset] = v; + this.typeOffset = 0; + } + } + + resetWriteType() { + this.typeOffset = 0; + } + + prepareWriteType() { + this.typeOffset = this.offset; + // It might be that a subsequent content writer is omitted (circular reference), + // then we want undefined to be written as type. + this.buffer[this.offset] = BSONType.UNDEFINED; + this.offset += 1; + } + writeUint32(v: number) { this.dataView.setUint32(this.offset, v, true); this.offset += 4; @@ -427,28 +450,19 @@ export class Writer { this.offset += 12; } - write(value: any, nameWriter?: () => void): void { + write(value: any): void { if (value instanceof ValueWithBSONSerializer) { if (value.value !== undefined && value.value !== null) { if (isUUIDType(value.type)) { - if (nameWriter) { - this.writeByte(BSONType.BINARY); - nameWriter(); - } + this.writeType(BSONType.BINARY); this.writeUUID(value.value); return; } else if (isMongoIdType(value.type)) { - if (nameWriter) { - this.writeByte(BSONType.OID); - nameWriter(); - } + this.writeType(BSONType.OID); this.writeObjectId(value.value); return; } else if (isBinaryBigIntType(value.type)) { - if (nameWriter) { - this.writeByte(BSONType.BINARY); - nameWriter(); - } + this.writeType(BSONType.BINARY); const binary = binaryBigIntAnnotation.getFirst(value.type)!; if (binary === BinaryBigIntType.signed) { this.writeSignedBigIntBinary(value.value); @@ -458,18 +472,12 @@ export class Writer { return; } } - this.write(value.value, nameWriter); + this.write(value.value); } else if ('boolean' === typeof value) { - if (nameWriter) { - this.writeByte(BSONType.BOOLEAN); - nameWriter(); - } + this.writeType(BSONType.BOOLEAN); this.writeByte(value ? 1 : 0); } else if (value instanceof RegExp) { - if (nameWriter) { - this.writeByte(BSONType.REGEXP); - nameWriter(); - } + this.writeType(BSONType.REGEXP); this.writeString(value.source); this.writeNull(); if (value.ignoreCase) this.writeString('i'); @@ -478,10 +486,7 @@ export class Writer { this.writeNull(); } else if ('string' === typeof value) { //size + content + null - if (nameWriter) { - this.writeByte(BSONType.STRING); - nameWriter(); - } + this.writeType(BSONType.STRING); const start = this.offset; this.offset += 4; //size placeholder this.writeString(value); @@ -490,89 +495,58 @@ export class Writer { } else if ('number' === typeof value) { if (Math.floor(value) === value && value >= BSON_INT32_MIN && value <= BSON_INT32_MAX) { //32bit int - if (nameWriter) { - this.writeByte(BSONType.INT); - nameWriter(); - } + this.writeType(BSONType.INT); this.writeInt32(value); } else { //double - if (nameWriter) { - this.writeByte(BSONType.NUMBER); - nameWriter(); - } + this.writeType(BSONType.NUMBER); this.writeDouble(value); } } else if (value instanceof Date) { - if (nameWriter) { - this.writeByte(BSONType.DATE); - nameWriter(); - } - + this.writeType(BSONType.DATE); this.writeLong(value.valueOf()); } else if ('bigint' === typeof value) { //this is only called for bigint in any structures. //to make sure the deserializing yields a bigint as well, we have to always use binary representation - if (nameWriter) { - this.writeByte(BSONType.BINARY); - nameWriter(); - } + this.writeType(BSONType.BINARY); this.writeBigIntBinary(value); } else if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) { - if (nameWriter) { - this.writeByte(BSONType.BINARY); - nameWriter(); - } + this.writeType(BSONType.BINARY); this.writeArrayBuffer(value); } else if (isArray(value)) { - if (nameWriter) { - this.writeByte(BSONType.ARRAY); - nameWriter(); - } + this.writeType(BSONType.ARRAY); const start = this.offset; this.offset += 4; //size for (let i = 0; i < value.length; i++) { - this.write(value[i], () => { - this.writeAsciiString('' + i); - this.writeByte(0); - }); + this.prepareWriteType(); + this.writeAsciiString('' + i); + this.writeByte(0); + this.write(value[i]); } this.writeNull(); this.writeDelayedSize(this.offset - start, start); } else if (value === undefined) { - if (nameWriter) { - this.writeByte(BSONType.UNDEFINED); - nameWriter(); - } + this.writeType(BSONType.UNDEFINED); } else if (value === null) { - if (nameWriter) { - this.writeByte(BSONType.NULL); - nameWriter(); - } + this.writeType(BSONType.NULL); } else if (isObject(value)) { - if (nameWriter) { - this.writeByte(BSONType.OBJECT); - nameWriter(); - } + this.writeType(BSONType.OBJECT); const start = this.offset; this.offset += 4; //size for (let i in value) { if (!hasProperty(value, i)) continue; - this.write(value[i], () => { - this.writeString(i); - this.writeByte(0); - }); + this.prepareWriteType(); + this.writeString(i); + this.writeByte(0); + this.write(value[i]); } this.writeNull(); this.writeDelayedSize(this.offset - start, start); } else { //the sizer incldues the type and name, so we have to write that - if (nameWriter) { - this.writeByte(BSONType.UNDEFINED); - nameWriter(); - } + this.writeType(BSONType.UNDEFINED); } } @@ -616,7 +590,7 @@ function handleObjectLiteral( type: TypeClass | TypeObjectLiteral, state: TemplateState, target: 'serialization' | 'sizer', - options: BSONSerializerOptions + options: BSONSerializerOptions, ) { let before: string = 'state.size += 4; //object size'; let after: string = 'state.size += 1; //null'; @@ -676,44 +650,14 @@ function handleObjectLiteral( // return; // } - const existingCalled = callExtractedFunctionIfAvailable(state, type); - const extract = existingCalled ? undefined : extractStateToFunctionAndCallIt(state, type); - + //following line resets propertyName, so we have to store it before + const propertyName = state.propertyName; if (target === 'serialization') { serializePropertyNameAware(type, state, BSONType.OBJECT, `'object' === typeof ${state.accessor}`, ''); } else { sizerPropertyNameAware(type, state, `'object' === typeof ${state.accessor}`, ''); } - if (type.kind === ReflectionKind.class && referenceAnnotation.hasAnnotations(type)) { - state.setContext({ isObject, isReferenceInstance, isReferenceHydrated }); - const reflection = ReflectionClass.from(type.classType); - //the primary key is serialised for unhydrated references - const index = getNameExpression(reflection.getPrimary().getName(), state); - const primaryKey = reflection.getPrimary().getType(); - //if a reference or forMongoDatabase=true only the foreign primary key is serialized - state.replaceTemplate(` - if ((${options.forMongoDatabase === true}) || (isReferenceInstance(${state.accessor}) && !isReferenceHydrated(${state.accessor}))) { - ${executeTemplates(state.fork(state.setter, `${state.accessor}[${index}]`).forPropertyName(state.propertyName), primaryKey)} - } else { - ${state.template} - } - `); - } - - //wrap circular check if necessary - if (hasCircularReference(type)) { - state.replaceTemplate(` - if (!state._stack || !state._stack.includes(${state.accessor})) { - ${state.template} - } - `); - } - - if (!extract) return; - - state = extract.state; - const lines: string[] = []; const signatures: TypeIndexSignature[] = []; const existing: string[] = []; @@ -728,7 +672,7 @@ function handleObjectLiteral( if (!isPropertyMemberType(member)) continue; if (!isSerializable(member.type)) continue; - const writeName = String(state.namingStrategy.getPropertyName(member, state.registry.serializer.name)); + const propertyName = String(state.namingStrategy.getPropertyName(member, state.registry.serializer.name)); const readName = getNameExpression(memberNameToString(member.name), state); existing.push(readName); @@ -738,28 +682,44 @@ function handleObjectLiteral( if (excludedAnnotation.isExcluded(member.type, state.registry.serializer.name)) continue; const accessor = `${state.accessor}[${readName}]`; - const propertyState = state.fork('', accessor).extendPath(writeName); + const propertyState = state.fork('', accessor).extendPath(propertyName); const setUndefined = isOptional(member) - ? executeTemplates(propertyState.fork().forPropertyName(writeName), { kind: ReflectionKind.undefined }) - : isNullable(member) ? executeTemplates(propertyState.fork().forPropertyName(writeName), { kind: ReflectionKind.null }) : ''; + ? executeTemplates(propertyState.fork(), { kind: ReflectionKind.undefined }) + : isNullable(member) ? executeTemplates(propertyState.fork(), { kind: ReflectionKind.null }) : ''; - const template = executeTemplates(propertyState.fork().forPropertyName(writeName), member.type); + const template = executeTemplates(propertyState.fork(), member.type); if (!template) { console.error('missing template for member', member.name, 'of', type); throw new BSONError(`No template found for ${String(member.name)}: ${member.type.kind}`); } + let nameWriter = ``; + if (target === 'serialization') { + nameWriter = ` + state.writer.prepareWriteType(); + ${propertyNameWrite(propertyName)} + `; + } else if (target === 'sizer') { + nameWriter = ` + //type + name + null + state.size += 1 + ${stringByteLength(propertyName)} + 1; + `; + } + + const optional = isOptional(member) || hasDefaultValue(member); + let converter = ` + ${nameWriter} if (${accessor} === unpopulatedSymbol) { //don't do anything since not loaded - } else if (${accessor} === undefined) { + } else if (${optional} && ${accessor} === undefined) { ${setUndefined} } else { ${template} } `; - if (isOptional(member)) { + if (optional) { lines.push(` if (${readName} in ${state.accessor}) { ${converter} @@ -774,17 +734,32 @@ function handleObjectLiteral( const i = state.compilerContext.reserveName('i'); const existingCheck = existing.map(v => `${i} === ${v}`).join(' || ') || 'false'; const signatureLines: string[] = []; + state.setContext({ stringByteLength }); sortSignatures(signatures); for (const signature of signatures) { const accessor = new ContainerAccessor(state.accessor, i); - const propertyState = state.fork(undefined, accessor).extendPath(new RuntimeCode(i)).forPropertyName(new RuntimeCode(i)); + const propertyState = state.fork(undefined, accessor).extendPath(new RuntimeCode(i)); const setUndefined = isOptional(signature.type) - ? executeTemplates(propertyState.fork().forPropertyName(new RuntimeCode(i)), { kind: ReflectionKind.undefined }) - : isNullable(signature.type) ? executeTemplates(propertyState.fork().forPropertyName(new RuntimeCode(i)), { kind: ReflectionKind.null }) : ''; - + ? executeTemplates(propertyState.fork(), { kind: ReflectionKind.undefined }) + : isNullable(signature.type) ? executeTemplates(propertyState.fork(), { kind: ReflectionKind.null }) : ''; + + let nameWriter = ``; + if (target === 'serialization') { + nameWriter = ` + state.writer.prepareWriteType(); + state.writer.writeAsciiString(${i}); + state.writer.writeByte(0); + `; + } else if (target === 'sizer') { + nameWriter = ` + //type + name + null + state.size += 1 + stringByteLength(${i}) + 1; + `; + } signatureLines.push(`else if (${getIndexCheck(state.compilerContext, i, signature.index)}) { + ${nameWriter} if (${accessor} === undefined) { ${setUndefined} } else { @@ -793,7 +768,7 @@ function handleObjectLiteral( }`); } - state.setContext({hasProperty}); + state.setContext({ hasProperty }); //the index signature type could be: string, number, symbol. //or a literal when it was constructed by a mapped type. lines.push(` @@ -806,40 +781,48 @@ function handleObjectLiteral( } state.addCode(` - //handle objectLiteral via propertyName ${state.propertyName ? collapsePath([state.propertyName]) : ''} + //handle objectLiteral via propertyName="${getPropertyNameString(propertyName)}" ${before} ${lines.join('\n')} ${after} `); - extract.setFunction(buildFunction(state, type)); + if (type.kind === ReflectionKind.class && referenceAnnotation.hasAnnotations(type)) { + state.setContext({ isObject, isReferenceInstance, isReferenceHydrated }); + const reflection = ReflectionClass.from(type.classType); + //the primary key is serialised for unhydrated references + const index = getNameExpression(reflection.getPrimary().getName(), state); + const primaryKey = reflection.getPrimary().getType(); + //if a reference or forMongoDatabase=true only the foreign primary key is serialized + state.replaceTemplate(` + if ((${options.forMongoDatabase === true}) || (isReferenceInstance(${state.accessor}) && !isReferenceHydrated(${state.accessor}))) { + ${executeTemplates(state.fork(state.setter, `${state.accessor}[${index}]`).forPropertyName(propertyName), primaryKey)} + } else { + ${state.template} + } + `); + } } -function propertyNameWriter(state: TemplateState) { - if (state.propertyName) { - if (state.propertyName instanceof RuntimeCode) { +function propertyNameWrite(propertyName?: string | RuntimeCode) { + if (propertyName) { + if (propertyName instanceof RuntimeCode) { return ` - state.writer.writeAsciiString(${state.propertyName.code}); + state.writer.writeAsciiString(${propertyName.code}); state.writer.writeByte(0); `; } else { - return getNameWriterCode(state.propertyName); + return getNameWriterCode(propertyName); } } return ''; } function serializePropertyNameAware(type: Type, state: TemplateState, bsonType: BSONType, typeChecker: string, code: string): void { - //when this call is reached first, and it's an object, then no type byte is needed. - //todo: that does not work when arbitrary offset and already prefilled buffer is given - const isInitialObject = `${bsonType === BSONType.OBJECT} && state.writer.offset === 0`; - state.template = ` - //serializer for ${type.kind} + //serializer for ${type.kind}, via propertyName="${getPropertyNameString(state.propertyName)}" ${typeChecker ? `if (!(${typeChecker})) ${state.throwCode(type)}` : ''} - if (${!!state.propertyName}) state.writer.writeByte(${bsonType}); - ${propertyNameWriter(state)} - ${state.template} + state.writer.writeType(${bsonType}); //BSON type = ${BSONType[bsonType]} ${code} `; } @@ -851,29 +834,6 @@ export class DigitByteRuntimeCode extends RuntimeCode { } function sizerPropertyNameAware(type: Type, state: TemplateState, typeChecker: string, code: string): void { - if (state.propertyName) { - if (state.propertyName instanceof DigitByteRuntimeCode) { - state.setContext({ digitByteSize }); - //type + string size + null - code = ` - state.size += 1 + digitByteSize(${state.propertyName.code}); //type + byte of ${state.propertyName.code} - ${code} - `; - } else if (state.propertyName instanceof RuntimeCode) { - state.setContext({ stringByteLength }); - //type + string size + null - code = ` - state.size += 1 + stringByteLength(${state.propertyName.code}) + 1; //type + string size of ${state.propertyName.code} + null - ${code} - `; - } else { - //type + string size + null - code = ` - state.size += 1 + ${stringByteLength(state.propertyName)} + 1; //type + string size of ${state.propertyName} + null - ${code} - `; - } - } const checker = typeChecker ? `if (!(${typeChecker})) ${state.throwCode(type)}` : ''; state.template = ` ${checker} @@ -889,9 +849,7 @@ function sizerAny(type: Type, state: TemplateState) { function serializeAny(type: Type, state: TemplateState) { state.addCode(` - state.writer.write(${state.accessor}, () => { - ${propertyNameWriter(state)} - }); + state.writer.write(${state.accessor}); `); } @@ -955,23 +913,19 @@ function sizeString(type: Type, state: TemplateState) { } function serializeNumber(type: Type, state: TemplateState) { - const nameWriter = propertyNameWriter(state); state.addCode(` if ('bigint' === typeof ${state.accessor}) { //long - state.writer.writeByte(${BSONType.LONG}); - ${nameWriter} + state.writer.writeType(${BSONType.LONG}); state.writer.writeBigIntLong(${state.accessor}); } else if ('number' === typeof ${state.accessor} && !Number.isNaN(${state.accessor})) { if (Math.floor(${state.accessor}) === ${state.accessor} && ${state.accessor} >= ${BSON_INT32_MIN} && ${state.accessor} <= ${BSON_INT32_MAX}) { //32bit int - state.writer.writeByte(${BSONType.INT}); - ${nameWriter} + state.writer.writeType(${BSONType.INT}); state.writer.writeInt32(${state.accessor}); } else { //double, 64bit - state.writer.writeByte(${BSONType.NUMBER}); - ${nameWriter} + state.writer.writeType(${BSONType.NUMBER}); state.writer.writeDouble(${state.accessor}); } } @@ -998,13 +952,11 @@ function serializeBigInt(type: TypeBigInt, state: TemplateState) { const binaryBigInt = binaryBigIntAnnotation.getFirst(type); if (binaryBigInt !== undefined) { - const nameWriter = propertyNameWriter(state); const writeBigInt = binaryBigInt === BinaryBigIntType.unsigned ? 'writeBigIntBinary' : 'writeSignedBigIntBinary'; state.addCode(` if (('bigint' === typeof ${state.accessor} || 'number' === typeof ${state.accessor}) && !Number.isNaN(${state.accessor})) { //long - state.writer.writeByte(${BSONType.BINARY}); - ${nameWriter} + state.writer.writeType(${BSONType.BINARY}); state.writer.${writeBigInt}(${state.accessor}); }`); } else { @@ -1073,17 +1025,23 @@ function serializeBinary(type: TypeClass, state: TemplateState) { `); } -function sizerArray(elementType: Type, state: TemplateState) { +function sizerArray(type: TypeArray, state: TemplateState) { + const elementType = type.type; state.setContext({ isIterable }); - const i = state.compilerContext.reserveName('i'); + const i = state.compilerContext.reserveName('arrayItemIndex'); const item = state.compilerContext.reserveName('item'); + state.setContext({ digitByteSize }); + + const memberState = state.fork('', item).extendPath(new RuntimeCode(i)); sizerPropertyNameAware(elementType, state, `isIterable(${state.accessor})`, ` state.size += 4; //array size let ${i} = 0; for (const ${item} of ${state.accessor}) { - ${executeTemplates(state.fork('', item).extendPath(new RuntimeCode(i)).forPropertyName(new DigitByteRuntimeCode(i)), elementType)} + //type + index name + null + state.size += 1 + digitByteSize(${i}); + ${executeTemplates(memberState, elementType)} ${i}++; } @@ -1095,7 +1053,7 @@ function serializeArray(elementType: Type, state: TemplateState) { state.setContext({ isIterable }); const start = state.compilerContext.reserveName('start'); - const i = state.compilerContext.reserveName('i'); + const i = state.compilerContext.reserveName('arrayIndex'); const item = state.compilerContext.reserveName('item'); serializePropertyNameAware(elementType, state, BSONType.ARRAY, `isIterable(${state.accessor})`, ` var ${start} = state.writer.offset; @@ -1103,7 +1061,13 @@ function serializeArray(elementType: Type, state: TemplateState) { let ${i} = 0; for (const ${item} of ${state.accessor}) { - ${executeTemplates(state.fork('', item).extendPath(new RuntimeCode(i)).forPropertyName(new DigitByteRuntimeCode(i)), elementType)} + state.writer.prepareWriteType(); + + state.writer.writeAsciiString(${i} + ''); + state.writer.writeByte(0); + + ${executeTemplates(state.fork('', item).extendPath(new RuntimeCode(i)), elementType)} + state.writer.resetWriteType(); ${i}++; } @@ -1119,7 +1083,7 @@ function serializeTuple(type: TypeTuple, state: TemplateState) { //[number, ...string, number, string], medium const lines: string[] = []; let restEndOffset = 0; - const i = state.compilerContext.reserveName('i'); + const i = state.compilerContext.reserveName('tupleIndex'); for (let i = 0; i < type.types.length; i++) { if (type.types[i].type.kind === ReflectionKind.rest) { @@ -1132,14 +1096,20 @@ function serializeTuple(type: TypeTuple, state: TemplateState) { if (member.type.kind === ReflectionKind.rest) { lines.push(` for (; ${i} < ${state.accessor}.length - ${restEndOffset}; ${i}++) { - ${executeTemplates(state.fork('', `${state.accessor}[${i}]`).extendPath(member.name || new RuntimeCode(i)).forPropertyName(new DigitByteRuntimeCode(i)), member.type.type)} + state.writer.prepareWriteType(); + state.writer.writeAsciiString(${i} + ''); + state.writer.writeByte(0); + ${executeTemplates(state.fork('', `${state.accessor}[${i}]`).extendPath(member.name || new RuntimeCode(i)), member.type.type)} } `); } else { const optionalCheck = member.optional ? `${state.accessor}[${i}] !== undefined` : 'true'; lines.push(` if (${optionalCheck}) { - ${executeTemplates(state.fork('', `${state.accessor}[${i}]`).extendPath(member.name || new RuntimeCode(i)).forPropertyName(new DigitByteRuntimeCode(i)), member.type)} + state.writer.prepareWriteType(); + state.writer.writeAsciiString(${i} + ''); + state.writer.writeByte(0); + ${executeTemplates(state.fork('', `${state.accessor}[${i}]`).extendPath(member.name || new RuntimeCode(i)), member.type)} } ${i}++; `); @@ -1148,6 +1118,7 @@ function serializeTuple(type: TypeTuple, state: TemplateState) { const start = state.compilerContext.reserveName('start'); state.setContext({ isArray }); + serializePropertyNameAware(type, state, BSONType.ARRAY, `isArray(${state.accessor})`, ` let ${i} = 0; var ${start} = state.writer.offset; @@ -1167,7 +1138,7 @@ function sizerTuple(type: TypeTuple, state: TemplateState) { //[number, ...string, number, string], medium const lines: string[] = []; let restEndOffset = 0; - const i = state.compilerContext.reserveName('i'); + const i = state.compilerContext.reserveName('tupleElement'); for (let i = 0; i < type.types.length; i++) { if (type.types[i].type.kind === ReflectionKind.rest) { @@ -1175,19 +1146,23 @@ function sizerTuple(type: TypeTuple, state: TemplateState) { break; } } + state.setContext({ digitByteSize }); for (const member of type.types) { if (member.type.kind === ReflectionKind.rest) { lines.push(` for (; ${i} < ${state.accessor}.length - ${restEndOffset}; ${i}++) { - ${executeTemplates(state.fork('', `${state.accessor}[${i}]`).extendPath(member.name || new RuntimeCode(i)).forPropertyName(new DigitByteRuntimeCode(i)), member.type.type)} + //type + index name + null + state.size += 1 + digitByteSize(${i}); + ${executeTemplates(state.fork('', `${state.accessor}[${i}]`).extendPath(member.name || new RuntimeCode(i)), member.type.type)} } `); } else { const optionalCheck = member.optional ? `${state.accessor}[${i}] !== undefined` : 'true'; lines.push(` if (${optionalCheck}) { - ${executeTemplates(state.fork('', `${state.accessor}[${i}]`).extendPath(member.name || new RuntimeCode(i)).forPropertyName(new DigitByteRuntimeCode(i)), member.type)} + state.size += 1 + digitByteSize(${i}); + ${executeTemplates(state.fork('', `${state.accessor}[${i}]`).extendPath(member.name || new RuntimeCode(i)), member.type)} } ${i}++; `); @@ -1197,7 +1172,7 @@ function sizerTuple(type: TypeTuple, state: TemplateState) { state.setContext({ isArray }); sizerPropertyNameAware(type, state, `isArray(${state.accessor})`, ` let ${i} = 0; - state.size += 4; //array size + state.size += 4; //array size, for tuple ${lines.join('\n')} @@ -1251,15 +1226,10 @@ export class BSONBinarySerializer extends Serializer { this.sizerRegistry.register(ReflectionKind.bigint, sizerBigInt); this.sizerRegistry.register(ReflectionKind.literal, sizerLiteral); this.sizerRegistry.register(ReflectionKind.regexp, sizerRegExp); - this.sizerRegistry.register(ReflectionKind.array, (type, state) => sizerArray(type.type as Type, state)); + this.sizerRegistry.register(ReflectionKind.array, sizerArray); this.sizerRegistry.register(ReflectionKind.tuple, sizerTuple); - this.sizerRegistry.registerClass(Map, (type, state) => sizerArray(copyAndSetParent({ - kind: ReflectionKind.tuple, types: [ - { kind: ReflectionKind.tupleMember, name: 'key', type: type.arguments![0] }, - { kind: ReflectionKind.tupleMember, name: 'value', type: type.arguments![1] }, - ] - }), state)); - this.sizerRegistry.registerClass(Set, (type, state) => sizerArray(type.arguments![0] as Type, state)); + this.sizerRegistry.registerClass(Map, forwardMapToArray); + this.sizerRegistry.registerClass(Set, forwardSetToArray); this.sizerRegistry.registerClass(Date, (type, state) => sizerPropertyNameAware(type, state, `${state.accessor} instanceof Date`, `state.size += 8;`)); this.sizerRegistry.register(ReflectionKind.undefined, (type, state) => sizerPropertyNameAware(type, state, `${state.accessor} === undefined || ${state.accessor} === null`, ``)); this.sizerRegistry.register(ReflectionKind.void, (type, state) => sizerPropertyNameAware(type, state, `${state.accessor} === undefined || ${state.accessor} === null`, ``)); @@ -1289,13 +1259,8 @@ export class BSONBinarySerializer extends Serializer { this.bsonSerializeRegistry.register(ReflectionKind.tuple, serializeTuple); this.bsonSerializeRegistry.register(ReflectionKind.promise, (type, state) => executeTemplates(state, type.type)); this.bsonSerializeRegistry.register(ReflectionKind.enum, (type, state) => executeTemplates(state, type.indexType)); - this.bsonSerializeRegistry.registerClass(Map, (type, state) => serializeArray(copyAndSetParent({ - kind: ReflectionKind.tuple, types: [ - { kind: ReflectionKind.tupleMember, type: type.arguments![0] }, - { kind: ReflectionKind.tupleMember, type: type.arguments![1] }, - ] - }), state)); - this.bsonSerializeRegistry.registerClass(Set, (type, state) => serializeArray(type.arguments![0] as Type, state)); + this.bsonSerializeRegistry.registerClass(Map, forwardMapToArray); + this.bsonSerializeRegistry.registerClass(Set, forwardSetToArray); this.bsonSerializeRegistry.registerClass(Date, (type, state) => { serializePropertyNameAware(type, state, BSONType.DATE, `${state.accessor} instanceof Date`, `state.writer.writeLong(${state.accessor}.valueOf());`); }); @@ -1335,20 +1300,15 @@ export class BSONBinarySerializer extends Serializer { this.bsonTypeGuards.register(1, ReflectionKind.regexp, bsonTypeGuardForBsonTypes([BSONType.REGEXP])); this.bsonTypeGuards.register(1, ReflectionKind.union, (type, state) => bsonTypeGuardUnion(this.bsonTypeGuards, type, state)); - this.bsonTypeGuards.register(1, ReflectionKind.array, (type, state) => bsonTypeGuardArray(type.type as Type, state)); + this.bsonTypeGuards.register(1, ReflectionKind.array, bsonTypeGuardArray); this.bsonTypeGuards.register(1, ReflectionKind.tuple, bsonTypeGuardTuple); this.bsonTypeGuards.register(1, ReflectionKind.promise, (type, state) => executeTemplates(state, type.type)); this.bsonTypeGuards.register(1, ReflectionKind.enum, (type, state) => executeTemplates(state, type.indexType)); this.bsonTypeGuards.registerClass(1, Date, bsonTypeGuardForBsonTypes([...numberTypes, BSONType.DATE, BSONType.TIMESTAMP])); this.bsonTypeGuards.registerBinary(1, bsonTypeGuardForBsonTypes([BSONType.BINARY])); - this.bsonTypeGuards.registerClass(1, Map, (type, state) => bsonTypeGuardArray(copyAndSetParent({ - kind: ReflectionKind.tuple, types: [ - { kind: ReflectionKind.tupleMember, name: 'key', type: type.arguments![0] }, - { kind: ReflectionKind.tupleMember, name: 'value', type: type.arguments![1] }, - ] - }), state)); - this.bsonTypeGuards.registerClass(1, Set, (type, state) => bsonTypeGuardArray(type.arguments![0] as Type, state)); + this.bsonTypeGuards.registerClass(1, Map, forwardMapToArray); + this.bsonTypeGuards.registerClass(1, Set, forwardSetToArray); //many deserializes support other types as well as fallback, we register them under specificality > 1 this.bsonTypeGuards.register(1.5, ReflectionKind.undefined, bsonTypeGuardForBsonTypes([BSONType.NULL])); @@ -1394,23 +1354,17 @@ export class BSONBinarySerializer extends Serializer { this.bsonDeserializeRegistry.register(ReflectionKind.regexp, deserializeRegExp); this.bsonDeserializeRegistry.register(ReflectionKind.tuple, deserializeTuple); this.bsonDeserializeRegistry.register(ReflectionKind.union, (type, state) => deserializeUnion(this.bsonTypeGuards, type, state)); - this.bsonDeserializeRegistry.register(ReflectionKind.array, (type, state) => deserializeArray(type.type as Type, state)); + this.bsonDeserializeRegistry.register(ReflectionKind.array, deserializeArray); this.bsonDeserializeRegistry.register(ReflectionKind.promise, (type, state) => executeTemplates(state, type.type)); this.bsonDeserializeRegistry.register(ReflectionKind.enum, (type, state) => executeTemplates(state, type.indexType)); this.bsonDeserializeRegistry.registerClass(Date, deserializeDate); this.bsonDeserializeRegistry.registerBinary(deserializeBinary); this.bsonDeserializeRegistry.registerClass(Map, (type, state) => { - deserializeArray(copyAndSetParent({ - kind: ReflectionKind.tuple, types: [ - { kind: ReflectionKind.tupleMember, type: type.arguments![0] }, - { kind: ReflectionKind.tupleMember, type: type.arguments![1] }, - ] - }), state); - + forwardMapToArray(type, state); state.addSetter(`new Map(${state.setter})`); }); this.bsonDeserializeRegistry.registerClass(Set, (type, state) => { - deserializeArray(type.arguments![0] as Type, state); + forwardSetToArray(type, state); state.addSetter(`new Set(${state.setter})`); }); @@ -1526,5 +1480,6 @@ export function getBSONSizer(serializer: BSONBinarySerializer = bsonBinarySer } export function serializeBSON(data: T, serializer: BSONBinarySerializer = bsonBinarySerializer, receiveType?: ReceiveType): Uint8Array { - return getBSONSerializer(serializer, receiveType)(data); + const serialize = getBSONSerializer(serializer, receiveType); + return serialize(data); } diff --git a/packages/bson/tests/bson-serialize.spec.ts b/packages/bson/tests/bson-serialize.spec.ts index 6ebf9cb56..35b52c368 100644 --- a/packages/bson/tests/bson-serialize.spec.ts +++ b/packages/bson/tests/bson-serialize.spec.ts @@ -1,11 +1,24 @@ import { expect, test } from '@jest/globals'; -import { getBSONSerializer, getBSONSizer, getValueSize, hexToByte, uuidStringToByte } from '../src/bson-serializer.js'; -import { BinaryBigInt, createReference, Excluded, MongoId, nodeBufferToArrayBuffer, PrimaryKey, Reference, SignedBinaryBigInt, typeOf, uuid, UUID } from '@deepkit/type'; +import { getBSONSerializer, getBSONSizer, getValueSize, hexToByte, serializeBSONWithoutOptimiser, uuidStringToByte } from '../src/bson-serializer.js'; +import { + BinaryBigInt, + createReference, + Excluded, + hasCircularReference, + MongoId, + nodeBufferToArrayBuffer, + PrimaryKey, + Reference, + SignedBinaryBigInt, + typeOf, + uuid, + UUID, +} from '@deepkit/type'; import bson from 'bson'; import { randomBytes } from 'crypto'; import { BSON_BINARY_SUBTYPE_DEFAULT, BSONType } from '../src/utils.js'; import { deserializeBSONWithoutOptimiser } from '../src/bson-parser.js'; -import { deserializeBSON } from '../src/bson-deserializer.js'; +import { deserializeBSON, getBSONDeserializer } from '../src/bson-deserializer.js'; const { Binary, calculateObjectSize, deserialize, Long, ObjectId: OfficialObjectId, serialize } = bson; @@ -581,7 +594,7 @@ test('basic Buffer', () => { test('basic uuid', () => { const uuidRandomBinary = new Binary( Buffer.allocUnsafe(16), - Binary.SUBTYPE_UUID + Binary.SUBTYPE_UUID, ); const object = { uuid: '75ed2328-89f2-4b89-9c49-1498891d616d' }; @@ -610,7 +623,7 @@ test('basic uuid', () => { const uuidPlain = Buffer.from([0x75, 0xed, 0x23, 0x28, 0x89, 0xf2, 0x4b, 0x89, 0x9c, 0x49, 0x14, 0x98, 0x89, 0x1d, 0x61, 0x6d]); const uuidBinary = new Binary(uuidPlain, 4); const objectBinary = { - uuid: uuidBinary + uuid: uuidBinary, }; expect(getBSONSerializer(undefined, schema)(object).byteLength).toBe(expectedSize); @@ -716,7 +729,8 @@ test('basic map', () => { name: Map }>(); - expect(getBSONSizer(undefined, schema)(object)).toBe(expectedSize); + const sizer = getBSONSizer(undefined, schema); + expect(sizer(object)).toBe(expectedSize); expect(getBSONSerializer(undefined, schema)(object).byteLength).toBe(expectedSize); expect(getBSONSerializer(undefined, schema)(object)).toEqual(serialize({ name: [['abc', 'Peter']] })); }); @@ -748,6 +762,7 @@ test('basic set', () => { ; expect(calculateObjectSize({ name: ['abc', 'Peter'] })).toBe(expectedSize); + expect(getValueSize({ name: ['abc', 'Peter'] })).toBe(expectedSize); const schema = typeOf<{ name: Set @@ -779,13 +794,16 @@ test('basic array', () => { ; expect(calculateObjectSize(object)).toBe(expectedSize); + expect(getValueSize(object)).toBe(expectedSize); const schema = typeOf<{ name: string[] }>(); - expect(getBSONSerializer(undefined, schema)(object).byteLength).toBe(expectedSize); - expect(getBSONSizer(undefined, schema)(object)).toBe(expectedSize); + const sizer = getBSONSizer(undefined, schema); + const serialize = getBSONSerializer(undefined, schema); + expect(sizer(object)).toBe(expectedSize); + expect(serialize(object).byteLength).toBe(expectedSize); expect(getBSONSerializer(undefined, schema)(object)).toEqual(serialize(object)); }); @@ -971,7 +989,8 @@ test('reference', () => { v: Entity & Reference }>(); - expect(getBSONSizer(undefined, schema)(object)).toBe(expectedSize); + const sizer = getBSONSizer(undefined, schema); + expect(sizer(object)).toBe(expectedSize); const bson = getBSONSerializer(undefined, schema)(object); const officialDeserialize = deserialize(Buffer.from(bson)); @@ -1034,7 +1053,7 @@ test('bson length', () => { mechanism: 'SCRAM-SHA-1', payload: Buffer.concat([Buffer.from('n,,', 'utf8'), Buffer.from(`n=Peter,r=${nonce.toString('base64')}`, 'utf8')]), autoAuthorize: 1, - options: { skipEmptyExchange: true } + options: { skipEmptyExchange: true }, }; expect(message.payload.byteLength).toBe(13 + nonce.toString('base64').length); @@ -1057,7 +1076,7 @@ test('arrayBuffer', () => { const message = { name: 'myName', secondId: '5bf4a1ccce060e0b38864c9e', - preview: nodeBufferToArrayBuffer(Buffer.from('Baar', 'utf8')) + preview: nodeBufferToArrayBuffer(Buffer.from('Baar', 'utf8')), }; expect(Buffer.from(message.preview).toString('utf8')).toBe('Baar'); @@ -1144,6 +1163,7 @@ test('index signature', () => { [name: string]: number }>(); + expect(getValueSize({ a: 5 })).toBe(calculateObjectSize({ a: 5 })); expect(getBSONSizer(undefined, schema)({ a: 5 })).toBe(calculateObjectSize({ a: 5 })); expect(getBSONSizer(undefined, schema)({ a: 5, b: 6 })).toBe(calculateObjectSize({ a: 5, b: 6 })); @@ -1275,9 +1295,9 @@ test('complex recursive', () => { { imports: [], name: 'c', - } + }, ], - } + }, ], }; const fn = getBSONSerializer(); @@ -1305,3 +1325,106 @@ test('complex recursive', () => { expect(back1).toEqual(data); } }); + +test('circular', () => { + interface Model { + id: number; + another?: Model; + } + + expect(hasCircularReference(typeOf())).toBe(true); + const schema = typeOf(); + + { + const model: Model = { id: 1 }; + const model2: Model = { id: 2 }; + model.another = model2; + + const sizer = getBSONSizer(undefined, schema); + const serialize = getBSONSerializer(undefined, schema); + const bson = serialize(model); + const back = deserializeBSONWithoutOptimiser(bson); + expect(back).toEqual(model); + } +}); + +test('string', () => { + { + const value = { v: 'a' }; + type T = { v: string }; + const sizer = getBSONSizer(); + expect(sizer(value)).toBe(getValueSize(value)); + const serialize = getBSONSerializer(); + const bson = serialize(value); + const back = deserializeBSONWithoutOptimiser(bson); + expect(back).toEqual(value); + } +}); + +test('array', () => { + { + const value = { v: ['a', 'b'] }; + type T = { v: string[] }; + const sizer = getBSONSizer(); + expect(sizer(value)).toBe(getValueSize(value)); + const serialize = getBSONSerializer(); + const bson = serialize(value); + const back = deserializeBSONWithoutOptimiser(bson); + expect(back).toEqual(value); + } +}); + +test('set', () => { + { + const value = { v: new Set(['a', 'b']) }; + type T = { v: Set }; + const sizer = getBSONSizer(); + expect(sizer(value)).toBe(getValueSize({ v: ['a', 'b'] })); + const serialize = getBSONSerializer(); + const bson = serialize(value); + const back = deserializeBSONWithoutOptimiser(bson); + expect(back).toEqual({ v: ['a', 'b'] }); + const back2 = getBSONDeserializer()(bson); + expect(back2).toEqual(value); + } +}); + +test('undefined for required string', () => { + type T = { name: string }; + + const user = { name: undefined }; + + const serialize = getBSONSerializer(); + expect(() => serialize(user)).toThrow('Cannot convert undefined to string'); + + const deserialize = getBSONDeserializer(); + const bson = serializeBSONWithoutOptimiser(user); + expect(deserialize(bson)).toEqual({ name: '' }); +}); + +test('undefined for required number', () => { + type T = { id: number }; + + const user = { id: undefined }; + + const serialize = getBSONSerializer(); + expect(() => serialize(user)).toThrow('Cannot convert undefined to number'); + + const deserialize = getBSONDeserializer(); + const bson = serializeBSONWithoutOptimiser(user); + expect(deserialize(bson)).toEqual({ id: 0 }); +}); + + +test('undefined for required object', () => { + type T = { set: { id: number } }; + + const user = { set: undefined }; + + const serialize = getBSONSerializer(); + expect(() => serialize(user)).toThrow('Cannot convert undefined to {id: number}'); + + const deserialize = getBSONDeserializer(); + const bson = serializeBSONWithoutOptimiser(user); + expect(() => deserialize(bson)).toThrow('Cannot convert bson type UNDEFINED to {id: number}'); +}); diff --git a/packages/core/src/compiler.ts b/packages/core/src/compiler.ts index e8e730be0..469d0825a 100644 --- a/packages/core/src/compiler.ts +++ b/packages/core/src/compiler.ts @@ -11,6 +11,10 @@ import { indent } from './indent.js'; import { hasProperty } from './core.js'; +declare var process: any; + +const indentCode = (process?.env.DEBUG || '').includes('deepkit'); + export class CompilerContext { public readonly context = new Map(); protected constVariables = new Map(); @@ -92,8 +96,8 @@ export class CompilerContext { } protected format(code: string): string { - if (!this.config.indent) return code; - return indent.js(code, { tabString: ' ' }); + if (indentCode || this.config.indent) return indent.js(code, { tabString: ' ' }); + return code; } build(functionCode: string, ...args: string[]): any { diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index ee06bac3b..c0d199a93 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -244,6 +244,18 @@ export function isObject(obj: any): obj is { [key: string]: any } { return (typeof obj === 'object' && !isArray(obj)); } +/** + * Returns true if given obj is a plain object, and no Date, Array, Map, Set, etc. + * + * This is different to isObject and used in the type system to differentiate + * between JS objects in general and what we define as ReflectionKind.objectLiteral. + * Since we have Date, Set, Map, etc. in the type system, we need to differentiate + * between them and all other object literals. + */ +export function isObjectLiteral(obj: any): obj is { [key: string]: any } { + return isObject(obj) && !(obj instanceof Date) && !(obj instanceof Map) && !(obj instanceof Set); +} + /** * @public */ diff --git a/packages/framework-integration/tests/controller-basic.spec.ts b/packages/framework-integration/tests/controller-basic.spec.ts index 3b1036cfd..0e72cd2ea 100644 --- a/packages/framework-integration/tests/controller-basic.spec.ts +++ b/packages/framework-integration/tests/controller-basic.spec.ts @@ -116,7 +116,7 @@ test('basic setup and methods', async () => { } catch (error) { expect(error).toBeInstanceOf(ValidationError); expect((error as ValidationError).errors[0]).toBeInstanceOf(ValidationErrorItem); - expect((error as ValidationError).errors[0]).toEqual({ code: 'type', message: 'Cannot convert undefined value to string', path: 'user.name' }); + expect((error as ValidationError).errors[0]).toEqual({ code: 'type', message: 'Cannot convert undefined to string', path: 'args.user.name' }); } } diff --git a/packages/rpc/src/server/action.ts b/packages/rpc/src/server/action.ts index 5267b2d85..eb61d6d47 100644 --- a/packages/rpc/src/server/action.ts +++ b/packages/rpc/src/server/action.ts @@ -400,10 +400,6 @@ export class RpcServerAction { try { value = message.parseBody(types.actionCallSchema); } catch (error: any) { - if (error instanceof ValidationError) { - //remove `.args` from path - error = ValidationError.from(error.errors.map(v => ({ ...v, path: v.path.replace('args.', '') }))); - } return response.error(error); } diff --git a/packages/sql/tests/serializer.spec.ts b/packages/sql/tests/serializer.spec.ts new file mode 100644 index 000000000..af56f60ec --- /dev/null +++ b/packages/sql/tests/serializer.spec.ts @@ -0,0 +1,34 @@ +import { expect, test } from '@jest/globals'; +import { AutoIncrement, deserialize, PrimaryKey, serialize } from '@deepkit/type'; +import { sqlSerializer } from '../src/serializer/sql-serializer.js'; + +test('array', () => { + class Address { + name: string = ''; + street: string = ''; + zip: string = ''; + } + + class Person { + id: number & PrimaryKey & AutoIncrement = 0; + tags: string[] = []; + addresses: Address[] = []; + } + + const runtime = { id: 1, tags: ['a', 'b'], addresses: [{ name: 'a', street: 'b', zip: 'c' }] }; + const res = serialize( + runtime, + undefined, sqlSerializer, + ); + const record = { + id: 1, + tags: JSON.stringify(['a', 'b']), + addresses: JSON.stringify([{ name: 'a', street: 'b', zip: 'c' }]), + }; + expect(res).toEqual(record); + + const back = deserialize(res, undefined, sqlSerializer); + expect(back).toEqual(runtime); +}); + + diff --git a/packages/type/src/serializer.ts b/packages/type/src/serializer.ts index a33540878..dfbd44646 100644 --- a/packages/type/src/serializer.ts +++ b/packages/type/src/serializer.ts @@ -20,8 +20,9 @@ import { isIterable, isNumeric, isObject, + isObjectLiteral, stringifyValueWithType, - toFastProperties + toFastProperties, } from '@deepkit/core'; import { AnnotationDefinition, @@ -41,6 +42,7 @@ import { hasDefaultValue, hasEmbedded, isBackReferenceType, + isCustomTypeClass, isMongoIdType, isNullable, isOptional, @@ -56,6 +58,7 @@ import { stringifyResolvedType, stringifyType, Type, + TypeArray, TypeClass, TypeIndexSignature, TypeObjectLiteral, @@ -65,7 +68,7 @@ import { typeToObject, TypeTuple, TypeUnion, - validationAnnotation + validationAnnotation, } from './reflection/type.js'; import { TypeNumberBrand } from '@deepkit/type-spec'; import { hasCircularReference, ReflectionClass, ReflectionProperty } from './reflection/reflection.js'; @@ -234,30 +237,24 @@ export function createSerializeFunction(type: Type, registry: TemplateRegistry, export type Guard = (data: any, state?: { errors?: ValidationErrorItem[] }) => data is T; -export function createTypeGuardFunction(type: Type, state?: TemplateState, serializerToUse?: Serializer, strict: boolean = false): undefined | Guard { +export function createTypeGuardFunction(type: Type, stateIn?: Partial, serializerToUse?: Serializer, withLoose = true): undefined | Guard { const compiler = new CompilerContext(); - if (state) { - state = state.fork('result'); + let state: TemplateState; + if (stateIn instanceof TemplateState) { + state = stateIn.fork('result'); state.compilerContext = compiler; } else { state = new TemplateState('result', 'data', compiler, (serializerToUse || serializer).typeGuards.getRegistry(1)); + if (stateIn) Object.assign(state, stateIn); } state.path = [new RuntimeCode('_path')]; - if (strict) state.validation = 'strict'; state.setterDisabled = false; const templates = state.registry.get(type); - if (!templates.length) return undefined; - for (const hook of state.registry.preHooks) hook(type, state); - for (const template of templates) { - template(type, state); - if (state.ended) break; - } - for (const hook of state.registry.postHooks) hook(type, state); - for (const hook of state.registry.getDecorator(type)) hook(type, state); + executeTemplates(state, type, withLoose); compiler.context.set('typeSettings', typeSettings); //set unpopulatedCheck to ReturnSymbol to jump over those properties @@ -291,17 +288,22 @@ export function collapsePath(path: (string | RuntimeCode)[], prefix?: string): s return path.filter(v => !!v).map(v => v instanceof RuntimeCode ? v.code : JSON.stringify(v)).join(`+'.'+`) || `''`; } +export function getPropertyNameString(propertyName?: string | RuntimeCode) { + return propertyName ? collapsePath([propertyName]) : ''; +} + /** * internal: The jit stack cache is used in both serializer and guards, so its cache key needs to be aware of it */ export class JitStack { - protected stacks: { registry?: TemplateRegistry, map: Map }[] = []; + protected stacks: { registry?: TemplateRegistry, map: Map }[] = []; + protected id: number = 0; getStack(registry?: TemplateRegistry) { for (const stack of this.stacks) { if (stack.registry === registry) return stack.map; } - const map = new Map(); + const map = new Map(); this.stacks.push({ registry, map }); return map; } @@ -314,25 +316,27 @@ export class JitStack { return this.getStack(registry).get(type); } - prepare(registry: TemplateRegistry, type: Type): (fn: Function) => { fn: Function | undefined } { + prepare(registry: TemplateRegistry, type: Type): { id: number, prepare: (fn: Function) => { fn: Function | undefined } } { if (this.getStack(registry).has(type)) { throw new Error('Circular jit building detected: ' + stringifyType(type)); } - const entry: { fn: Function | undefined } = { fn: undefined }; + const entry: { fn: Function | undefined, id: number } = { fn: undefined, id: this.id++ }; this.getStack(registry).set(type, entry); - return (fn: Function) => { - entry.fn = fn; - return entry; + return { + id: entry.id, prepare: (fn: Function) => { + entry.fn = fn; + return entry; + }, }; } - getOrCreate(registry: TemplateRegistry | undefined, type: Type, create: () => Function): { fn: Function | undefined } { + getOrCreate(registry: TemplateRegistry | undefined, type: Type, create: () => Function): { fn: Function | undefined, id: number } { const stack = this.getStack(registry); const existing = stack.get(type); if (existing) return existing; - const entry: { fn: Function | undefined } = { fn: undefined }; + const entry: { fn: Function | undefined, id: number } = { fn: undefined, id: this.id++ }; stack.set(type, entry); entry.fn = create(); return entry; @@ -386,7 +390,7 @@ export class TemplateState { public registry: TemplateRegistry, public namingStrategy: NamingStrategy = new NamingStrategy, public jitStack: JitStack = new JitStack(), - public path: (string | RuntimeCode)[] = [] + public path: (string | RuntimeCode)[] = [], ) { this.setter = originalSetter; this.accessor = originalAccessor; @@ -695,17 +699,19 @@ export class TemplateRegistry { export function callExtractedFunctionIfAvailable(state: TemplateState, type: Type): boolean { const jit = state.jitStack.get(state.registry, type); if (!jit) return false; + const withSetter = !state.setterDisabled && state.setter; state.addCode(` - //call jit for ${state.setter} via propertyName ${state.propertyName ? collapsePath([state.propertyName]) : ''} - ${state.setterDisabled || !state.setter ? '' : `${state.setter} = `}${state.setVariable('jit', jit)}.fn(${state.accessor || 'undefined'}, state, ${collapsePath(state.path)}); + //call jit=${jit.id} for setter="${state.setter}" via propertyName ${state.propertyName ? collapsePath([state.propertyName]) : ''} + ${withSetter ? `${state.setter} = ` : ''}${state.setVariable('jit', jit)}.fn(${state.accessor || 'undefined'}, state, ${collapsePath(state.path)}); `); + if (withSetter) state.accessor = state.setter; return true; } export function extractStateToFunctionAndCallIt(state: TemplateState, type: Type) { const prepare = state.jitStack.prepare(state.registry, type); callExtractedFunctionIfAvailable(state, type); - return { setFunction: prepare, state: state.fork('result', 'data', [new RuntimeCode('_path')]) }; + return { setFunction: prepare.prepare, id: prepare.id, state: state.fork('result', 'data', [new RuntimeCode('_path')]).forPropertyName(state.propertyName) }; } export function buildFunction(state: TemplateState, type: Type): Function { @@ -741,25 +747,35 @@ export function buildFunction(state: TemplateState, type: Type): Function { export function executeTemplates( state: TemplateState, type: Type, + withLoose: boolean = true, + withCache: boolean = true, ): string { state.parentTypes.push(type); - if (state.validation === 'loose' && state.allSpecificalities) { + + let originalState = state; + + if (withLoose && state.validation === 'loose' && state.allSpecificalities) { + // check if the particular type has multiple specificalities + // if so, we need to generate a type guard that checks all specificalities. + // e.g. string supports `'string' === typeof' but as last resort also anything else. + // or Date supports `instanceof Date` but also `'string' === typeof` as last resort. + // This is not part of a union check. We have to do it for each type. const typeGuards = state.allSpecificalities.getSortedTemplateRegistries(); const lines: string[] = []; for (const [specificality, typeGuard] of typeGuards) { - const fn = createTypeGuardFunction(type, state.fork(undefined, 'data').forRegistry(typeGuard)); + const next = state.fork(undefined, 'data').forRegistry(typeGuard); + const fn = createTypeGuardFunction(type, next, undefined, false); if (!fn) continue; const guard = state.setVariable('guard_' + ReflectionKind[type.kind], fn); const looseCheck = specificality <= 0 ? `state.loosely !== false && ` : ''; lines.push(`else if (${looseCheck}${guard}(${state.accessor})) { - //type = ${ReflectionKind[type.kind]}, specificality=${specificality} - ${state.setter} = true; - }`); + //type = ${ReflectionKind[type.kind]}, specificality=${specificality} + ${state.setter} = true; + }`); } - state.parentTypes.pop(); - return ` + state.template = ` //type guard with multiple specificalities if (false) {} ${lines.join(' ')} else { @@ -767,6 +783,29 @@ export function executeTemplates( } `; } else { + let setFunction: undefined | ((fn: Function) => void); + if (withCache && (type.kind === ReflectionKind.objectLiteral + || type.kind === ReflectionKind.array || type.kind === ReflectionKind.tuple + || (type.kind === ReflectionKind.class && (type.classType === Set || type.classType === Map)) + || isCustomTypeClass(type)) && !embeddedAnnotation.getFirst(type) + ) { + //wrap circular check if necessary + if (callExtractedFunctionIfAvailable(state, type)) { + state.parentTypes.pop(); + return state.template; + } else { + const extract = extractStateToFunctionAndCallIt(state, type); + state.replaceTemplate(` + // extracted jit=${extract.id} function via state.propertyName="${getPropertyNameString(state.propertyName)}" + if (!state._stack || !state._stack.includes(${state.accessor})) { + ${state.template} + } + `); + state = extract.state; + setFunction = extract.setFunction; + } + } + const templates = state.registry.get(type); for (const hook of state.registry.preHooks) hook(type, state); for (const template of templates) { @@ -775,9 +814,15 @@ export function executeTemplates( } for (const hook of state.registry.postHooks) hook(type, state); for (const template of state.registry.getDecorator(type)) template(type, state); - state.parentTypes.pop(); - return state.template; + + if (setFunction) { + setFunction(buildFunction(state, type)); + } } + + state.parentTypes.pop(); + + return originalState.template; } export function createConverterJSForMember( @@ -1046,7 +1091,7 @@ export function serializeObjectLiteral(type: TypeObjectLiteral | TypeClass, stat const setter = getEmbeddedAccessor(type, false, state.setter, state.registry.serializer, state.namingStrategy, first, embedded); state.addCode(` if (${inAccessor(state.accessor)}) { - ${executeTemplates(state.fork(setter, new ContainerAccessor(state.accessor, name)), first.type)} + ${executeTemplates(state.fork(setter, new ContainerAccessor(state.accessor, name)), first.type, false, false)} }`); } else { const lines: string[] = []; @@ -1078,9 +1123,6 @@ export function serializeObjectLiteral(type: TypeObjectLiteral | TypeClass, stat return; } - if (callExtractedFunctionIfAvailable(state, type)) return; - const extract = extractStateToFunctionAndCallIt(state, type); - state = extract.state; state.setContext({ isGroupAllowed }); const v = state.compilerContext.reserveName('v'); @@ -1235,8 +1277,6 @@ export function serializeObjectLiteral(type: TypeObjectLiteral | TypeClass, stat } `); } - - extract.setFunction(buildFunction(state, type)); } export function typeGuardEmbedded(type: TypeClass | TypeObjectLiteral, state: TemplateState, embedded: EmbeddedOptions) { @@ -1274,10 +1314,6 @@ export function typeGuardObjectLiteral(type: TypeObjectLiteral | TypeClass, stat } } - if (callExtractedFunctionIfAvailable(state, type)) return; - const extract = extractStateToFunctionAndCallIt(state, type); - state = extract.state; - const lines: string[] = []; const signatures: TypeIndexSignature[] = []; const existing: string[] = []; @@ -1380,9 +1416,11 @@ export function typeGuardObjectLiteral(type: TypeObjectLiteral | TypeClass, stat } } + state.setContext({ isObjectLiteral }); + state.addCodeForSetter(` ${state.setter} = true; - if (${state.accessor} && 'object' === typeof ${state.accessor}) { + if (${state.accessor} && isObjectLiteral(${state.accessor})) { ${lines.join('\n')} ${customValidatorCall} } else { @@ -1390,11 +1428,9 @@ export function typeGuardObjectLiteral(type: TypeObjectLiteral | TypeClass, stat ${state.setter} = false; } `); - - extract.setFunction(buildFunction(state, type)); } -export function serializeArray(elementType: Type, state: TemplateState) { +export function serializeArray(type: TypeArray, state: TemplateState) { state.setContext({ isIterable }); const v = state.compilerContext.reserveName('v'); const i = state.compilerContext.reserveName('i'); @@ -1407,7 +1443,7 @@ export function serializeArray(elementType: Type, state: TemplateState) { let ${i} = 0; for (const ${item} of ${state.accessor}) { let ${v}; - ${executeTemplates(state.fork(v, item).extendPath(new RuntimeCode(i)), elementType)} + ${executeTemplates(state.fork(v, item).extendPath(new RuntimeCode(i)), type.type)} ${state.setter}.push(${v}); ${i}++; } @@ -1417,7 +1453,6 @@ export function serializeArray(elementType: Type, state: TemplateState) { export function typeGuardArray(elementType: Type, state: TemplateState) { state.setContext({ isIterable }); - const v = state.compilerContext.reserveName('v'); const i = state.compilerContext.reserveName('i'); const item = state.compilerContext.reserveName('item'); @@ -1543,65 +1578,44 @@ function typeGuardTuple(type: TypeTuple, state: TemplateState) { `); } -function typeGuardClassMap(type: TypeClass, state: TemplateState) { - if (!type.arguments || type.arguments.length !== 2) return; +export function getSetTypeToArray(type: TypeClass): TypeArray { + const jit = getTypeJitContainer(type); + if (jit.forwardSetToArray) return jit.forwardSetToArray; - typeGuardArray(copyAndSetParent({ - kind: ReflectionKind.tuple, types: [ - { kind: ReflectionKind.tupleMember, parent: Object as any, type: type.arguments[0] }, - { kind: ReflectionKind.tupleMember, parent: Object as any, type: type.arguments[1] }, - ] - }), state); -} + const value = type.arguments?.[0] || { kind: ReflectionKind.any }; -function typeGuardClassSet(type: TypeClass, state: TemplateState) { - if (!type.arguments || type.arguments.length !== 1) return; + jit.forwardSetToArray = { + kind: ReflectionKind.array, type: value, + }; - typeGuardArray(type.arguments[0], state); + return jit.forwardSetToArray; } -/** - * Set is simply serialized as array. - */ -function deserializeTypeClassSet(type: TypeClass, state: TemplateState) { - if (!type.arguments || type.arguments.length !== 1) return; +export function getMapTypeToArray(type: TypeClass): TypeArray { + const jit = getTypeJitContainer(type); + if (jit.forwardMapToArray) return jit.forwardMapToArray; - serializeArray(type.arguments[0], state); - state.addSetter(`new Set(${state.accessor})`); -} + const index = type.arguments?.[0] || { kind: ReflectionKind.any }; + const value = type.arguments?.[1] || { kind: ReflectionKind.any }; -/** - * Set is simply serialized as array. - */ -function serializeTypeClassSet(type: TypeClass, state: TemplateState) { - if (!type.arguments || type.arguments.length !== 1) return; + jit.forwardMapToArray = { + kind: ReflectionKind.array, type: copyAndSetParent({ + kind: ReflectionKind.tuple, types: [ + { kind: ReflectionKind.tupleMember, name: 'key', type: index }, + { kind: ReflectionKind.tupleMember, name: 'value', type: value }, + ], + }), + }; - serializeArray(type.arguments[0], state); + return jit.forwardMapToArray; } -function deserializeTypeClassMap(type: TypeClass, state: TemplateState) { - if (!type.arguments || type.arguments.length !== 2) return; - serializeArray(copyAndSetParent({ - kind: ReflectionKind.tuple, types: [ - { kind: ReflectionKind.tupleMember, type: type.arguments[0] }, - { kind: ReflectionKind.tupleMember, type: type.arguments[1] }, - ] - }), state); - state.addSetter(`new Map(${state.accessor})`); +export function forwardSetToArray(type: TypeClass, state: TemplateState) { + executeTemplates(state, getSetTypeToArray(type), true, false); } -/** - * Map is simply serialized as array of tuples. - */ -function serializeTypeClassMap(type: TypeClass, state: TemplateState) { - if (!type.arguments || type.arguments.length !== 2) return; - - serializeArray(copyAndSetParent({ - kind: ReflectionKind.tuple, types: [ - { kind: ReflectionKind.tupleMember, type: type.arguments[0] }, - { kind: ReflectionKind.tupleMember, type: type.arguments[1] }, - ] - }), state); +export function forwardMapToArray(type: TypeClass, state: TemplateState) { + executeTemplates(state, getMapTypeToArray(type), true, false); } export function serializePropertyOrParameter(type: TypePropertySignature | TypeProperty | TypeParameter, state: TemplateState) { @@ -1692,7 +1706,8 @@ export function handleUnion(type: TypeUnion, state: TemplateState) { //if validation is not set, we are in deserialize mode, so we need to activate validation //for this state. .withValidation(!state.validation ? 'loose' : state.validation) - .includeAllSpecificalities(state.registry.serializer.typeGuards) + .includeAllSpecificalities(state.registry.serializer.typeGuards), + undefined, false, ); if (!fn) continue; const guard = state.setVariable('guard_' + ReflectionKind[t.kind], fn); @@ -1855,8 +1870,8 @@ export class Serializer { this.deserializeRegistry.register(ReflectionKind.objectLiteral, serializeObjectLiteral); this.serializeRegistry.register(ReflectionKind.objectLiteral, serializeObjectLiteral); - this.deserializeRegistry.register(ReflectionKind.array, (type, state) => serializeArray(type.type, state)); - this.serializeRegistry.register(ReflectionKind.array, (type, state) => serializeArray(type.type, state)); + this.deserializeRegistry.register(ReflectionKind.array, serializeArray); + this.serializeRegistry.register(ReflectionKind.array, serializeArray); this.deserializeRegistry.register(ReflectionKind.tuple, serializeTuple); this.serializeRegistry.register(ReflectionKind.tuple, serializeTuple); @@ -2013,11 +2028,17 @@ export class Serializer { state.addSetter(`${state.accessor} instanceof ${typedArrayVar} ? ${state.accessor} : base64ToTypedArray(${state.accessor}, ${typedArrayVar})`); }); - this.serializeRegistry.registerClass(Set, serializeTypeClassSet); - this.serializeRegistry.registerClass(Map, serializeTypeClassMap); + this.serializeRegistry.registerClass(Set, forwardSetToArray); + this.serializeRegistry.registerClass(Map, forwardMapToArray); - this.deserializeRegistry.registerClass(Set, deserializeTypeClassSet); - this.deserializeRegistry.registerClass(Map, deserializeTypeClassMap); + this.deserializeRegistry.registerClass(Set, (type, state) => { + forwardSetToArray(type, state); + state.addSetter(`new Set(${state.accessor})`); + }); + this.deserializeRegistry.registerClass(Map, (type, state) => { + forwardMapToArray(type, state); + state.addSetter(`new Map(${state.accessor})`); + }); this.deserializeRegistry.addDecorator( type => isReferenceType(type) || isBackReferenceType(type) || (type.parent !== undefined && isBackReferenceType(type.parent)), @@ -2188,8 +2209,8 @@ export class Serializer { this.typeGuards.register(0.5, ReflectionKind.regexp, ((type, state) => state.addSetter(`'string' === typeof ${state.accessor} && ${state.accessor}[0] === '/'`))); - this.typeGuards.getRegistry(1).registerClass(Set, typeGuardClassSet); - this.typeGuards.getRegistry(1).registerClass(Map, typeGuardClassMap); + this.typeGuards.getRegistry(1).registerClass(Set, forwardSetToArray); + this.typeGuards.getRegistry(1).registerClass(Map, forwardMapToArray); this.typeGuards.getRegistry(1).registerClass(Date, (type, state) => state.addSetterAndReportErrorIfInvalid('type', 'Not a Date', `${state.accessor} instanceof Date`)); this.typeGuards.getRegistry(0.5).registerClass(Date, (type, state) => { state.addSetter(`'string' === typeof ${state.accessor} && new Date(${state.accessor}).toString() !== 'Invalid Date'`); @@ -2328,8 +2349,8 @@ export class EmptySerializer extends Serializer { this.deserializeRegistry.register(ReflectionKind.objectLiteral, serializeObjectLiteral); this.serializeRegistry.register(ReflectionKind.objectLiteral, serializeObjectLiteral); - this.deserializeRegistry.register(ReflectionKind.array, (type, state) => serializeArray(type.type, state)); - this.serializeRegistry.register(ReflectionKind.array, (type, state) => serializeArray(type.type, state)); + this.deserializeRegistry.register(ReflectionKind.array, serializeArray); + this.serializeRegistry.register(ReflectionKind.array, serializeArray); this.deserializeRegistry.register(ReflectionKind.tuple, serializeTuple); this.serializeRegistry.register(ReflectionKind.tuple, serializeTuple); diff --git a/packages/type/src/typeguard.ts b/packages/type/src/typeguard.ts index 8c0dd5c82..f9dc4cc20 100644 --- a/packages/type/src/typeguard.ts +++ b/packages/type/src/typeguard.ts @@ -1,5 +1,5 @@ import { ReceiveType, resolveReceiveType } from './reflection/reflection.js'; -import { createTypeGuardFunction, Guard, serializer, Serializer } from './serializer.js'; +import { createTypeGuardFunction, Guard, serializer, Serializer, TemplateState } from './serializer.js'; import { NoTypeReceived } from './utils.js'; import { ValidationError, ValidationErrorItem } from './validator.js'; import { getTypeJitContainer } from './reflection/type.js'; @@ -21,7 +21,9 @@ export function getValidatorFunction(serializerToUse: Serializer = serializer if (jit.__is) { return jit.__is; } - const fn = createTypeGuardFunction(type, undefined, serializerToUse, true) || (() => undefined); + const fn = createTypeGuardFunction(type, { + validation: 'strict' + }, serializerToUse) || (() => undefined); jit.__is = fn; return fn as Guard; } diff --git a/packages/type/tests/integration4.spec.ts b/packages/type/tests/integration4.spec.ts index 1c276b2db..f8eb70413 100644 --- a/packages/type/tests/integration4.spec.ts +++ b/packages/type/tests/integration4.spec.ts @@ -9,8 +9,9 @@ */ import { expect, test } from '@jest/globals'; -import { assertType, Group, groupAnnotation, ReflectionKind } from '../src/reflection/type.js'; +import { assertType, AutoIncrement, Group, groupAnnotation, PrimaryKey, ReflectionKind } from '../src/reflection/type.js'; import { typeOf } from '../src/reflection/reflection.js'; +import { cast } from '../src/serializer-facade.js'; test('group from enum', () => { enum Groups { @@ -67,3 +68,78 @@ test('class base from fn', () => { expect(type.types[1].name).toBe('x'); expect(groupAnnotation.getAnnotations(type.types[1].type)).toEqual(['position']); }); + + +test('complex recursive union type does not cause stack size exceeding 1', () => { + type JSONValue = null | boolean | number | string | JSONObject | JSONArray; + + type JSONArray = JSONValue[]; + + type JSONObject = { + [k: string]: JSONValue; + }; + + expect(cast({ username: 'Peter' })).toEqual({ username: 'Peter' }); + expect(cast(['Peter'])).toEqual(['Peter']); + expect(cast('Peter')).toBe('Peter'); + expect(cast(null)).toBe(null); + expect(cast(true)).toBe(true); + expect(cast(false)).toBe(false); + expect(cast(1)).toBe(1); + expect(cast(0)).toBe(0); + expect(cast(0.5)).toBe(0.5); + expect(cast([])).toEqual([]); + expect(cast({})).toEqual({}); +}); + +test('complex recursive union type does not cause stack size exceeding 2', () => { + type JSONValue = null | boolean | number | JSONObject | JSONArray | string; + + type JSONArray = [JSONValue]; + + type JSONObject = { + [k: string]: JSONValue; + }; + + const user = cast({ username: 'Peter' }); + expect(cast({ username: 'Peter' })).toEqual({ username: 'Peter' }); + expect(cast(['Peter'])).toEqual(['Peter']); + expect(cast('Peter')).toBe('Peter'); + expect(cast(null)).toBe(null); + expect(cast(true)).toBe(true); + expect(cast(false)).toBe(false); + expect(cast(1)).toBe(1); + expect(cast(0)).toBe(0); + expect(cast(0.5)).toBe(0.5); + expect(cast([{}])).toEqual([{}]); + expect(cast([])).toEqual([null]); + expect(cast({})).toEqual({}); +}); + +test('array', () => { + class Address { + name: string = ''; + street: string = ''; + zip: string = ''; + } + + class Person { + id: number & PrimaryKey & AutoIncrement = 0; + tags: string[] = []; + addresses: Address[] = []; + } + + expect(cast({ id: 1, tags: ['a', 'b'], addresses: [{ name: 'a', street: 'b', zip: 'c' }] })).toEqual({ + id: 1, + tags: ['a', 'b'], + addresses: [{ name: 'a', street: 'b', zip: 'c' }], + }); +}); + +test('union loosely', () => { + type a = { date: Date } | { id: number }; + + expect(cast({ date: '2020-08-05T00:00:00.000Z' })).toEqual({ date: new Date('2020-08-05T00:00:00.000Z') }); + expect(cast({ id: 2 })).toEqual({ id: 2 }); + expect(cast({ id: '3' })).toEqual({ id: 3 }); +}); diff --git a/packages/type/tests/validation.spec.ts b/packages/type/tests/validation.spec.ts index 73f38e0e9..256648e1e 100644 --- a/packages/type/tests/validation.spec.ts +++ b/packages/type/tests/validation.spec.ts @@ -167,7 +167,7 @@ test('path', () => { code: 'type', message: 'Not a number', path: 'configs.1.value', - value: undefined + value: undefined, }]); class Container2 { @@ -188,7 +188,7 @@ test('class with union literal', () => { code: 'type', message: 'No valid union member found. Valid: \'local\' | \'majority\' | \'linearizable\' | \'available\'', path: 'readConcernLevel', - value: 'invalid' + value: 'invalid', }]); }); @@ -267,13 +267,13 @@ test('inline object', () => { const errors = validate({ tags: [], - collection: {} // This should make the validator throw an error + collection: {}, // This should make the validator throw an error }); expect(errors).toEqual([{ path: 'collection.items', code: 'type', message: 'Not an array' }]); expect(() => validatedDeserialize({ tags: [], - collection: {} // This should make the validator throw an error + collection: {}, // This should make the validator throw an error })).toThrow('collection.items(type): Not an array'); });