Skip to content

Commit 9af338e

Browse files
committed
fix(bson): ensure wrapped bson value is bson type null when null or undefined
fixes #678
1 parent ffd1bc7 commit 9af338e

File tree

4 files changed

+93
-138
lines changed

4 files changed

+93
-138
lines changed

packages/bson/src/bson-serializer.ts

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -166,16 +166,19 @@ function getSignedBinaryBigIntSize(value: bigint): number {
166166

167167
export function getValueSize(value: any): number {
168168
if (value instanceof ValueWithBSONSerializer) {
169+
if (value.value === undefined || value.value === null) {
170+
return 0;
171+
}
172+
169173
if (isUUIDType(value.type)) {
170174
return 4 + 1 + 16;
171175
} else if (isMongoIdType(value.type)) {
172176
return 12;
173177
} else if (isBinaryBigIntType(value.type)) {
174178
const binaryBigInt = binaryBigIntAnnotation.getFirst(value.type)!;
175179
return binaryBigInt === BinaryBigIntType.unsigned ? getBinaryBigIntSize(value.value) : getSignedBinaryBigIntSize(value.value);
176-
} else {
177-
return getValueSize(value.value);
178180
}
181+
return getValueSize(value.value);
179182
} else if ('boolean' === typeof value) {
180183
return 1;
181184
} else if ('string' === typeof value) {
@@ -553,6 +556,24 @@ export class Writer {
553556
}
554557

555558
writeObjectId(value: string) {
559+
if (!value) {
560+
// set all to zero
561+
this.buffer[this.offset + 0] = 0;
562+
this.buffer[this.offset + 1] = 0;
563+
this.buffer[this.offset + 2] = 0;
564+
this.buffer[this.offset + 3] = 0;
565+
this.buffer[this.offset + 4] = 0;
566+
this.buffer[this.offset + 5] = 0;
567+
this.buffer[this.offset + 6] = 0;
568+
this.buffer[this.offset + 7] = 0;
569+
this.buffer[this.offset + 8] = 0;
570+
this.buffer[this.offset + 9] = 0;
571+
this.buffer[this.offset + 10] = 0;
572+
this.buffer[this.offset + 11] = 0;
573+
this.offset += 12;
574+
return;
575+
}
576+
556577
this.buffer[this.offset + 0] = hexToByte(value, 0);
557578
this.buffer[this.offset + 1] = hexToByte(value, 1);
558579
this.buffer[this.offset + 2] = hexToByte(value, 2);
@@ -570,25 +591,28 @@ export class Writer {
570591

571592
write(value: any): void {
572593
if (value instanceof ValueWithBSONSerializer) {
573-
if (value.value !== undefined && value.value !== null) {
574-
if (isUUIDType(value.type)) {
575-
this.writeType(BSONType.BINARY);
576-
this.writeUUID(value.value);
577-
return;
578-
} else if (isMongoIdType(value.type)) {
579-
this.writeType(BSONType.OID);
580-
this.writeObjectId(value.value);
581-
return;
582-
} else if (isBinaryBigIntType(value.type)) {
583-
this.writeType(BSONType.BINARY);
584-
const binary = binaryBigIntAnnotation.getFirst(value.type)!;
585-
if (binary === BinaryBigIntType.signed) {
586-
this.writeSignedBigIntBinary(value.value);
587-
} else {
588-
this.writeBigIntBinary(value.value);
589-
}
590-
return;
594+
if (value.value === undefined || value.value === null) {
595+
this.writeType(BSONType.NULL);
596+
return;
597+
}
598+
599+
if (isUUIDType(value.type)) {
600+
this.writeType(BSONType.BINARY);
601+
this.writeUUID(value.value);
602+
return;
603+
} else if (isMongoIdType(value.type)) {
604+
this.writeType(BSONType.OID);
605+
this.writeObjectId(value.value);
606+
return;
607+
} else if (isBinaryBigIntType(value.type)) {
608+
this.writeType(BSONType.BINARY);
609+
const binary = binaryBigIntAnnotation.getFirst(value.type)!;
610+
if (binary === BinaryBigIntType.signed) {
611+
this.writeSignedBigIntBinary(value.value);
612+
} else {
613+
this.writeBigIntBinary(value.value);
591614
}
615+
return;
592616
}
593617
this.write(value.value);
594618
} else if ('boolean' === typeof value) {

packages/bson/tests/bson-serialize.spec.ts

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,6 @@
11
import { expect, test } from '@jest/globals';
2-
import {
3-
AutoBuffer,
4-
getBSONSerializer,
5-
getBSONSizer,
6-
getValueSize,
7-
hexToByte,
8-
serializeBSONWithoutOptimiser,
9-
uuidStringToByte,
10-
wrapObjectId,
11-
wrapUUID,
12-
wrapValue,
13-
} from '../src/bson-serializer.js';
14-
import {
15-
BinaryBigInt,
16-
createReference,
17-
Excluded,
18-
hasCircularReference,
19-
MongoId,
20-
nodeBufferToArrayBuffer,
21-
PrimaryKey,
22-
Reference,
23-
SignedBinaryBigInt,
24-
typeOf,
25-
uuid,
26-
UUID,
27-
} from '@deepkit/type';
2+
import { AutoBuffer, getBSONSerializer, getBSONSizer, getValueSize, hexToByte, serializeBSONWithoutOptimiser, uuidStringToByte, wrapObjectId, wrapUUID, wrapValue } from '../src/bson-serializer.js';
3+
import { BinaryBigInt, createReference, Excluded, hasCircularReference, MongoId, nodeBufferToArrayBuffer, PrimaryKey, Reference, SignedBinaryBigInt, typeOf, uuid, UUID } from '@deepkit/type';
284
import bson from 'bson';
295
import { randomBytes } from 'crypto';
306
import { BSON_BINARY_SUBTYPE_DEFAULT, BSONType } from '../src/utils.js';
@@ -1670,3 +1646,11 @@ test('string fallback from number', () => {
16701646
expect(back2.v).toEqual(['1', '2']);
16711647
}
16721648
});
1649+
1650+
test('optional MongoId in object', () => {
1651+
type T = { v: any };
1652+
const serialize = getBSONSerializer<T>();
1653+
const bson = serialize({ v: wrapObjectId(undefined as any) });
1654+
const back = getBSONDeserializer<T>()(bson);
1655+
expect(back.v).toBeUndefined();
1656+
});

packages/mongo/src/mongo-serializer.ts

Lines changed: 1 addition & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,7 @@
99
*/
1010

1111
import { BSONBinarySerializer, ValueWithBSONSerializer } from '@deepkit/bson';
12-
import {
13-
ContainerAccessor,
14-
EmptySerializer,
15-
executeTemplates,
16-
isBinaryBigIntType,
17-
isMongoIdType,
18-
isReferenceType,
19-
isUUIDType,
20-
ReflectionClass,
21-
ReflectionKind,
22-
TemplateState,
23-
Type
24-
} from '@deepkit/type';
12+
import { ContainerAccessor, EmptySerializer, executeTemplates, isBinaryBigIntType, isMongoIdType, isReferenceType, isUUIDType, ReflectionClass, ReflectionKind, TemplateState, Type } from '@deepkit/type';
2513

2614
/**
2715
* Serializer class from BSONBinarySerializer with a few adjustments to make sure
@@ -90,65 +78,3 @@ class MongoAnySerializer extends EmptySerializer {
9078
}
9179

9280
export const mongoSerializer = new MongoAnySerializer();
93-
94-
//
95-
// export const mongoSerializer = new Serializer('mongo');
96-
//
97-
// mongoSerializer.fromClass.register('string', compilerToString);
98-
// mongoSerializer.fromClass.register('number', compilerToNumber);
99-
//
100-
// mongoSerializer.fromClass.register('array', (property: PropertySchema, state: CompilerState) => {
101-
// if (property.backReference || (property.parent && property.parent.backReference)) {
102-
// //we don't serialize back references to mongodb
103-
// state.template = '';
104-
// } else {
105-
// convertArray(property, state);
106-
// }
107-
// });
108-
// mongoSerializer.toClass.register('array', (property: PropertySchema, state: CompilerState) => {
109-
// if (property.backReference || (property.parent && property.parent.backReference)) {
110-
// //we don't serialize back references to mongodb
111-
// state.addSetter('undefined');
112-
// } else {
113-
// convertArray(property, state);
114-
// }
115-
// });
116-
//
117-
// mongoSerializer.fromClass.register('class', (property: PropertySchema, state: CompilerState) => {
118-
// //When property is a reference we store the actual primary (as foreign key) of the referenced instance instead of the actual instance.
119-
// //This way we implemented basically relations in the database
120-
// const classSchema = getClassSchema(property.resolveClassType!);
121-
//
122-
// if (property.isReference) {
123-
// const classType = state.setVariable('classType', property.resolveClassType);
124-
// const primary = classSchema.getPrimaryField();
125-
//
126-
// state.addCodeForSetter(`
127-
// if (${state.accessor} instanceof ${classType}) {
128-
// ${getDataConverterJS(state.setter, `${state.accessor}.${primary.name}`, primary, state.serializerCompilers, state.compilerContext, state.jitStack)}
129-
// } else {
130-
// //we treat the input as if the user gave the primary key directly
131-
// ${getDataConverterJS(state.setter, `${state.accessor}`, primary, state.serializerCompilers, state.compilerContext, state.jitStack)}
132-
// }
133-
// `
134-
// );
135-
// } else if (property.backReference || (property.parent && property.parent.backReference)) {
136-
// //we don't serialize back references to mongodb
137-
// state.template = '';
138-
// } else {
139-
// const classToX = state.setVariable('classToX', state.jitStack.getOrCreate(classSchema, () => getClassToXFunction(classSchema, state.serializerCompilers.serializer, state.jitStack)));
140-
// state.addSetter(`${classToX}.fn(${state.accessor}, _options, _stack)`);
141-
// }
142-
// });
143-
//
144-
// //this is necessary since we use in FindCommand `filter: t.any`, so uuid and objectId need to be a wrapper type so that @deepkit/bson serializes correctly
145-
// mongoSerializer.fromClass.register('uuid', (property: PropertySchema, state: CompilerState) => {
146-
// state.setContext({ UUID });
147-
// state.addSetter(`new UUID(${state.accessor})`);
148-
// });
149-
//
150-
// mongoSerializer.fromClass.register('objectId', (property: PropertySchema, state: CompilerState) => {
151-
// state.setContext({ ObjectId });
152-
//
153-
// state.addSetter(`new ObjectId(${state.accessor})`);
154-
// });

packages/mongo/tests/mongo.spec.ts

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,11 @@
11
import { expect, jest, test } from '@jest/globals';
2-
import {
3-
arrayBufferFrom,
4-
AutoIncrement,
5-
BackReference,
6-
cast,
7-
entity,
8-
MongoId,
9-
nodeBufferToArrayBuffer,
10-
PrimaryKey,
11-
Reference,
12-
ReflectionClass,
13-
ReflectionKind,
14-
serialize,
15-
Unique,
16-
UUID,
17-
uuid,
18-
} from '@deepkit/type';
2+
import { arrayBufferFrom, AutoIncrement, BackReference, cast, entity, MongoId, nodeBufferToArrayBuffer, PrimaryKey, Reference, ReflectionClass, ReflectionKind, serialize, Unique, UUID, uuid } from '@deepkit/type';
193
import { Database, getInstanceStateFromItem, UniqueConstraintFailure } from '@deepkit/orm';
204
import { SimpleModel, SuperSimple } from './entities.js';
215
import { createDatabase } from './utils.js';
226
import { MongoDatabaseAdapter } from '../src/adapter.js';
237
import { MemoryLogger } from '@deepkit/logger';
8+
import { ObjectId } from '@deepkit/bson';
249

2510
Error.stackTraceLimit = 100;
2611
jest.setTimeout(10000);
@@ -838,3 +823,39 @@ test('allowDiskUse', async () => {
838823
}
839824
database.disconnect();
840825
});
826+
827+
test('non-optional MongoId', async () => {
828+
class Model {
829+
_id: MongoId & PrimaryKey = '';
830+
831+
constructor(public teamId: MongoId) {
832+
833+
}
834+
}
835+
836+
const database = await createDatabase('non-optional-mongo-id');
837+
838+
const model1 = new Model(ObjectId.generate());
839+
await database.persist(model1);
840+
841+
const items1 = await database.query(Model)
842+
.filter({ teamId: undefined })
843+
.find();
844+
expect(items1.length).toBe(0);
845+
846+
const items2 = await database.query(Model)
847+
.filter({ teamId: null as any })
848+
.find();
849+
expect(items2.length).toBe(0);
850+
851+
const items3 = await database.query(Model)
852+
.filter({ teamId: '' })
853+
.find();
854+
expect(items3.length).toBe(0);
855+
856+
const items4 = await database.query(Model)
857+
.filter({ teamId: model1.teamId })
858+
.find();
859+
expect(items4.length).toBe(1);
860+
expect(items4[0]).toBeInstanceOf(Model);
861+
});

0 commit comments

Comments
 (0)