diff --git a/packages/sql/src/serializer/sql-serializer.ts b/packages/sql/src/serializer/sql-serializer.ts index 5ee1dec74..7b70a40c2 100644 --- a/packages/sql/src/serializer/sql-serializer.ts +++ b/packages/sql/src/serializer/sql-serializer.ts @@ -93,6 +93,12 @@ function deserializeSqlObjectLiteral(type: TypeClass | TypeObjectLiteral, state: export class SqlSerializer extends Serializer { name = 'sql'; + override setExplicitUndefined(type: Type, state: TemplateState): boolean { + //make sure that `foo?: string` is not explicitly set to undefined when database returns `null`. + if (state.target === 'deserialize') return false; + return true; + } + protected registerSerializers() { super.registerSerializers(); @@ -104,7 +110,7 @@ export class SqlSerializer extends Serializer { const uuidType = uuidAnnotation.registerType({ kind: ReflectionKind.string }, true); this.deserializeRegistry.register(ReflectionKind.string, (type, state) => { - //remove string enforcement, since UUID/MonogId are string but received as binary + //remove string enforcement, since UUID/MongoId are string but received as binary state.addSetter(state.accessor); }); diff --git a/packages/sqlite/src/sqlite-adapter.ts b/packages/sqlite/src/sqlite-adapter.ts index e2611a7b8..7464cde09 100644 --- a/packages/sqlite/src/sqlite-adapter.ts +++ b/packages/sqlite/src/sqlite-adapter.ts @@ -11,6 +11,7 @@ import { AbstractClassType, asyncOperation, ClassType, empty } from '@deepkit/core'; import { DatabaseAdapter, + DatabaseError, DatabaseLogger, DatabasePersistenceChangeSet, DatabaseSession, @@ -515,6 +516,9 @@ export class SQLiteQueryResolver extends SQLQueryResolver { const user = ReflectionClass.from(User); @@ -263,3 +263,24 @@ test('connection pool', async () => { expect(c12).toBe(c2); } }); + +test('optional', async () => { + @entity.name('entity') + class MyEntity { + id: number & PrimaryKey & AutoIncrement = 0; + value?: string; + } + + const database = new Database(new SQLiteDatabaseAdapter(), [MyEntity]); + await database.migrate(); + + const entity1 = new MyEntity(); + expect('value' in entity1).toBe(false); + expect('value' in serialize(entity1)).toBe(false); + + await database.persist(entity1); + + const entity2 = await database.query(MyEntity).findOne(); + expect('value' in entity2).toBe(false); + expect('value' in serialize(entity1)).toBe(false); +}); diff --git a/packages/type/src/serializer.ts b/packages/type/src/serializer.ts index 14b837bc7..e575fd067 100644 --- a/packages/type/src/serializer.ts +++ b/packages/type/src/serializer.ts @@ -42,7 +42,6 @@ import { stringifyType, Type, TypeClass, - TypeFunction, TypeIndexSignature, TypeObjectLiteral, TypeParameter, @@ -725,9 +724,8 @@ export function createConverterJSForMember( } } - const optional = isOptional(property instanceof ReflectionProperty ? property.property : property); + const setExplicitUndefined = registry.serializer.setExplicitUndefined(type, state) && isOptional(property instanceof ReflectionProperty ? property.property : property); const nullable = isNullable(type); - // const hasDefault = property instanceof ReflectionProperty ? property.hasDefault() : false; // // since JSON does not support undefined, we emulate it via using null for serialization, and convert that back to undefined when deserialization happens. // // note: When the value is not defined (property.name in object === false), then this code will never run. @@ -746,7 +744,7 @@ export function createConverterJSForMember( //note: this code is only reached when ${accessor} was actually defined checked by the 'in' operator. return ` if (${state.accessor} === undefined) { - if (${optional}) { + if (${setExplicitUndefined}) { ${undefinedSetterCode} } } else if (${state.accessor} === null) { @@ -756,7 +754,7 @@ export function createConverterJSForMember( if (${nullable}) { ${nullSetterCode} } else { - if (${optional}) { + if (${setExplicitUndefined}) { ${undefinedSetterCode} } } @@ -1703,6 +1701,10 @@ export class Serializer { this.registerValidators(); } + public setExplicitUndefined(type: Type, state: TemplateState): boolean { + return true; + } + protected registerValidators() { } diff --git a/packages/type/tests/type-spec.spec.ts b/packages/type/tests/type-spec.spec.ts index 95c40b969..8411bae7c 100644 --- a/packages/type/tests/type-spec.spec.ts +++ b/packages/type/tests/type-spec.spec.ts @@ -95,6 +95,7 @@ test('partial keeps explicitely undefined fields', () => { { const item = serializeToJson>({ title: undefined }); + expect(item).toEqual({ title: null }); } { @@ -755,9 +756,9 @@ test('dynamic properties', () => { } } - const back1 = deserializeFromJson({'~type': 'abc'}); + const back1 = deserializeFromJson({ '~type': 'abc' }); expect(back1.getType()).toBe('abc'); - const back2 = deserializeFromJson({'type': 'abc'}); + const back2 = deserializeFromJson({ 'type': 'abc' }); expect(back2.getType()).toBe('abc'); });