diff --git a/packages/repository/package-lock.json b/packages/repository/package-lock.json index c1bf68ab1674..3478d82fa1c0 100644 --- a/packages/repository/package-lock.json +++ b/packages/repository/package-lock.json @@ -4,6 +4,15 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/bson": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.0.tgz", + "integrity": "sha512-pq/rqJwJWkbS10crsG5bgnrisL8pML79KlMKQMoQwLUjlPAkrUHMvHJ3oGwE7WHR61Lv/nadMwXVAD2b+fpD8Q==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/debug": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", @@ -79,6 +88,12 @@ "concat-map": "0.0.1" } }, + "bson": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.6.tgz", + "integrity": "sha512-D8zmlb46xfuK2gGvKmUjIklQEouN2nQ0LEHHeZ/NoHM2LDiMk2EYzZ5Ntw/Urk+bgMDosOZxaRzXxvhI5TcAVQ==", + "dev": true + }, "charenc": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", diff --git a/packages/repository/package.json b/packages/repository/package.json index 3554c63d97ae..2da04853df8d 100644 --- a/packages/repository/package.json +++ b/packages/repository/package.json @@ -22,7 +22,9 @@ "@loopback/eslint-config": "^4.1.0", "@loopback/testlab": "^1.8.1", "@types/lodash": "^4.14.138", - "@types/node": "^10.14.18" + "@types/node": "^10.14.18", + "@types/bson": "^4.0.0", + "bson": "1.0.6" }, "dependencies": { "@loopback/context": "^1.23.0", diff --git a/packages/repository/src/__tests__/unit/repositories/relations-helpers/build-lookup-map.unit.ts b/packages/repository/src/__tests__/unit/repositories/relations-helpers/build-lookup-map.unit.ts new file mode 100644 index 000000000000..47e952beaf06 --- /dev/null +++ b/packages/repository/src/__tests__/unit/repositories/relations-helpers/build-lookup-map.unit.ts @@ -0,0 +1,78 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import {buildLookupMap, reduceAsArray, reduceAsSingleItem} from '../../../..'; +import { + Category, + Product, + createProduct, + createCategory, +} from './relations-helpers-fixtures'; + +describe('buildLookupMap', () => { + describe('get the result of using reduceAsArray strategy for hasMany relation', () => { + it('returns multiple instances in an array', () => { + const pen = createProduct({name: 'pen', categoryId: 1}); + const pencil = createProduct({name: 'pencil', categoryId: 1}); + + const result = buildLookupMap( + [pen, pencil], + 'categoryId', + reduceAsArray, + ); + const expected = new Map>(); + expected.set(1, [pen, pencil]); + expect(result).to.eql(expected); + }); + + it('return instances in multiple arrays', () => { + const pen = createProduct({name: 'pen', categoryId: 1}); + const pencil = createProduct({name: 'pencil', categoryId: 1}); + const eraser = createProduct({name: 'eraser', categoryId: 2}); + // 'id' is the foreign key in Category in respect to Product when we talk about belongsTo + const result = buildLookupMap( + [pen, eraser, pencil], + 'categoryId', + reduceAsArray, + ); + const expected = new Map>(); + expected.set(1, [pen, pencil]); + expected.set(2, [eraser]); + expect(result).to.eql(expected); + }); + }); + + describe('get the result of using reduceAsSingleItem strategy for belongsTo relation', () => { + it('returns one instance when one target instance is passed in', () => { + const cat = createCategory({name: 'stationery', id: 1}); + + const result = buildLookupMap( + [cat], + 'id', + reduceAsSingleItem, + ); + const expected = new Map(); + expected.set(1, cat); + expect(result).to.eql(expected); + }); + + it('returns multiple instances when multiple target instances are passed in', () => { + const cat1 = createCategory({name: 'stationery', id: 1}); + const cat2 = createCategory({name: 'book', id: 2}); + + // 'id' is the foreign key in Category in respect to Product when we talk about belongsTo + const result = buildLookupMap( + [cat1, cat2], + 'id', + reduceAsSingleItem, + ); + const expected = new Map(); + expected.set(1, cat1); + expected.set(2, cat2); + expect(result).to.eql(expected); + }); + }); +}); diff --git a/packages/repository/src/__tests__/unit/repositories/relations-helpers/find-by-foreign-keys.helpers.unit.ts b/packages/repository/src/__tests__/unit/repositories/relations-helpers/find-by-foreign-keys.helpers.unit.ts deleted file mode 100644 index 71fad2147584..000000000000 --- a/packages/repository/src/__tests__/unit/repositories/relations-helpers/find-by-foreign-keys.helpers.unit.ts +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright IBM Corp. 2019. All Rights Reserved. -// Node module: @loopback/repository -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -import {expect} from '@loopback/testlab'; -import {findByForeignKeys} from '../../../..'; -import {ProductRepository, testdb} from './relations-helpers-fixtures'; - -describe('findByForeignKeys', () => { - let productRepo: ProductRepository; - - before(() => { - productRepo = new ProductRepository(testdb); - }); - - beforeEach(async () => { - await productRepo.deleteAll(); - }); - - it('returns an empty array when no foreign keys are passed in', async () => { - const fkIds: number[] = []; - await productRepo.create({id: 1, name: 'product', categoryId: 1}); - const products = await findByForeignKeys(productRepo, 'categoryId', fkIds); - expect(products).to.be.empty(); - }); - - it('returns an empty array when no instances have the foreign key value', async () => { - await productRepo.create({id: 1, name: 'product', categoryId: 1}); - const products = await findByForeignKeys(productRepo, 'categoryId', 2); - expect(products).to.be.empty(); - }); - - it('returns an empty array when no instances have the foreign key values', async () => { - await productRepo.create({id: 1, name: 'product', categoryId: 1}); - const products = await findByForeignKeys(productRepo, 'categoryId', [2, 3]); - expect(products).to.be.empty(); - }); - - it('returns all instances that have the foreign key value', async () => { - const pens = await productRepo.create({name: 'pens', categoryId: 1}); - const pencils = await productRepo.create({name: 'pencils', categoryId: 1}); - const products = await findByForeignKeys(productRepo, 'categoryId', 1); - expect(products).to.deepEqual([pens, pencils]); - }); - - it('does not include instances with different foreign key values', async () => { - const pens = await productRepo.create({name: 'pens', categoryId: 1}); - const pencils = await productRepo.create({name: 'pencils', categoryId: 2}); - const products = await findByForeignKeys(productRepo, 'categoryId', 1); - expect(products).to.deepEqual([pens]); - expect(products).to.not.containDeep(pencils); - }); - - it('includes instances when there is one value in the array of foreign key values', async () => { - const pens = await productRepo.create({name: 'pens', categoryId: 1}); - const pencils = await productRepo.create({name: 'pencils', categoryId: 2}); - const products = await findByForeignKeys(productRepo, 'categoryId', [2]); - expect(products).to.deepEqual([pencils]); - expect(products).to.not.containDeep(pens); - }); - - it('returns all instances that have any of multiple foreign key values', async () => { - const pens = await productRepo.create({name: 'pens', categoryId: 1}); - const pencils = await productRepo.create({name: 'pencils', categoryId: 2}); - const paper = await productRepo.create({name: 'paper', categoryId: 3}); - const products = await findByForeignKeys(productRepo, 'categoryId', [1, 3]); - expect(products).to.deepEqual([pens, paper]); - expect(products).to.not.containDeep(pencils); - }); - - it('throws error if scope is passed in and is non-empty', async () => { - await expect( - findByForeignKeys(productRepo, 'categoryId', [1], {limit: 1}), - ).to.be.rejectedWith('scope is not supported'); - }); - - it('does not throw an error if scope is passed in and is undefined or empty', async () => { - let products = await findByForeignKeys( - productRepo, - 'categoryId', - [1], - undefined, - {}, - ); - expect(products).to.be.empty(); - products = await findByForeignKeys(productRepo, 'categoryId', 1, {}, {}); - expect(products).to.be.empty(); - }); -}); diff --git a/packages/repository/src/__tests__/unit/repositories/relations-helpers/find-by-foreign-keys.unit.ts b/packages/repository/src/__tests__/unit/repositories/relations-helpers/find-by-foreign-keys.unit.ts new file mode 100644 index 000000000000..18c7e10fe9ce --- /dev/null +++ b/packages/repository/src/__tests__/unit/repositories/relations-helpers/find-by-foreign-keys.unit.ts @@ -0,0 +1,147 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/repository +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + expect, + createStubInstance, + sinon, + StubbedInstanceWithSinonAccessor, +} from '@loopback/testlab'; +import {findByForeignKeys} from '../../../..'; +import { + ProductRepository, + Product, + createProduct, +} from './relations-helpers-fixtures'; + +describe('findByForeignKeys', () => { + let productRepo: StubbedInstanceWithSinonAccessor; + + // use beforeEach to restore sinon stub + beforeEach(() => { + productRepo = createStubInstance(ProductRepository); + }); + + it('returns an empty array when no foreign keys are passed in', async () => { + const RESULTS: Product[] = []; + productRepo.stubs.find.resolves(RESULTS); + + const fkIds: number[] = []; + await productRepo.create({id: 1, name: 'product', categoryId: 1}); + const products = await findByForeignKeys(productRepo, 'categoryId', fkIds); + expect(products).to.be.empty(); + + sinon.assert.notCalled(productRepo.stubs.find); + }); + + it('returns an empty array when no instances have the foreign key value', async () => { + const find = productRepo.stubs.find; + find.resolves([]); + await productRepo.create({id: 1, name: 'product', categoryId: 1}); + const products = await findByForeignKeys(productRepo, 'categoryId', 2); + expect(products).to.be.empty(); + sinon.assert.calledWithMatch(find, {}); + }); + + it('returns an empty array when no instances have the foreign key values', async () => { + const find = productRepo.stubs.find; + find.resolves([]); + await productRepo.create({id: 1, name: 'product', categoryId: 1}); + const products = await findByForeignKeys(productRepo, 'categoryId', [2, 3]); + expect(products).to.be.empty(); + sinon.assert.calledWithMatch(find, {}); + }); + + it('returns all instances that have the foreign key value', async () => { + const find = productRepo.stubs.find; + const pen = createProduct({name: 'pen', categoryId: 1}); + const pencil = createProduct({name: 'pencil', categoryId: 1}); + find.resolves([pen, pencil]); + + const products = await findByForeignKeys(productRepo, 'categoryId', 1); + expect(products).to.deepEqual([pen, pencil]); + + sinon.assert.calledWithMatch(find, { + where: { + categoryId: 1, + }, + }); + }); + + it('does not include instances with different foreign key values', async () => { + const find = productRepo.stubs.find; + const pen = await productRepo.create({name: 'pen', categoryId: 1}); + const pencil = await productRepo.create({name: 'pencil', categoryId: 2}); + find.resolves([pen]); + const products = await findByForeignKeys(productRepo, 'categoryId', 1); + expect(products).to.deepEqual([pen]); + expect(products).to.not.containDeep(pencil); + sinon.assert.calledWithMatch(find, { + where: { + categoryId: 1, + }, + }); + }); + + it('includes instances when there is one value in the array of foreign key values', async () => { + const find = productRepo.stubs.find; + const pen = await productRepo.create({name: 'pen', categoryId: 1}); + const pencil = await productRepo.create({name: 'pencil', categoryId: 2}); + find.resolves([pencil]); + const products = await findByForeignKeys(productRepo, 'categoryId', [2]); + expect(products).to.deepEqual([pencil]); + expect(products).to.not.containDeep(pen); + sinon.assert.calledWithMatch(find, { + where: { + categoryId: 2, + }, + }); + }); + + it('returns all instances that have any of multiple foreign key values', async () => { + const pen = createProduct({name: 'pen', categoryId: 1}); + const pencil = createProduct({name: 'pencil', categoryId: 2}); + const paper = createProduct({name: 'paper', categoryId: 3}); + const find = productRepo.stubs.find; + find.resolves([pen, paper]); + const products = await findByForeignKeys(productRepo, 'categoryId', [1, 3]); + expect(products).to.deepEqual([pen, paper]); + expect(products).to.not.containDeep(pencil); + sinon.assert.calledWithMatch(find, { + where: { + categoryId: { + inq: [1, 3], + }, + }, + }); + }); + + // update the test when scope is supported + it('throws error if scope is passed in and is non-empty', async () => { + productRepo.stubs.find.resolves([]); + await expect( + findByForeignKeys(productRepo, 'categoryId', [1], {limit: 1}), + ).to.be.rejectedWith('scope is not supported'); + sinon.assert.notCalled(productRepo.stubs.find); + }); + + // update the test when scope is supported + it('does not throw an error if scope is passed in and is undefined or empty', async () => { + const find = productRepo.stubs.find; + find.resolves([]); + let products = await findByForeignKeys( + productRepo, + 'categoryId', + [1], + undefined, + {}, + ); + expect(products).to.be.empty(); + sinon.assert.calledWithMatch(find, {}); + products = await findByForeignKeys(productRepo, 'categoryId', 1, {}, {}); + expect(products).to.be.empty(); + sinon.assert.calledWithMatch(find, {}); + }); +}); diff --git a/packages/repository/src/__tests__/unit/repositories/relations-helpers/include-related-models.helpers.unit.ts b/packages/repository/src/__tests__/unit/repositories/relations-helpers/include-related-models.unit.ts similarity index 100% rename from packages/repository/src/__tests__/unit/repositories/relations-helpers/include-related-models.helpers.unit.ts rename to packages/repository/src/__tests__/unit/repositories/relations-helpers/include-related-models.unit.ts diff --git a/packages/repository/src/__tests__/unit/repositories/relations-helpers/mongo-related-helpers.unit.ts b/packages/repository/src/__tests__/unit/repositories/relations-helpers/mongo-related-helpers.unit.ts new file mode 100644 index 000000000000..b7cdc1ead9f4 --- /dev/null +++ b/packages/repository/src/__tests__/unit/repositories/relations-helpers/mongo-related-helpers.unit.ts @@ -0,0 +1,145 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/repository-tests +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import {ObjectID} from 'bson'; +import {belongsTo, Entity, hasMany, model, property} from '../../../..'; +import {flattenMapByKeys} from '../../../../relations/relation.helpers'; +import { + isBsonType, + deduplicate, + normalizeKey, + buildLookupMap, + reduceAsArray, +} from '../../../../relations'; + +describe('unit tests, simulates mongodb env for helpers of inclusion resolver ', () => { + describe('helpers for formating instances', () => { + it('checks isBsonType', () => { + const objId = new ObjectID(); + const numId = 1; + expect(isBsonType(objId)).to.be.true(); + expect(isBsonType(numId)).to.be.false(); + }); + + context('deduplicate + isBsonType', () => { + it('passes in a simple unique array', () => { + const id1 = new ObjectID(); + const id2 = new ObjectID(); + + const result = deduplicate([id1, id2]); + expect(result).to.deepEqual([id1, id2]); + }); + + it('passes in a multiple items array', () => { + const id1 = new ObjectID(); + const id2 = new ObjectID(); + const id3 = new ObjectID(); + + const result = deduplicate([id3, id1, id1, id3, id2]); + expect(result).to.deepEqual([id3, id1, id2]); + }); + }); + }); + + describe('helpers for generating inclusion resolvers', () => { + // the tests below simulate mongodb environment. + context('normalizeKey + buildLookupMap', () => { + it('checks if id has been normalized', async () => { + const id = new ObjectID(); + expect(normalizeKey(id)).to.eql(id.toString()); + }); + + it('creates a lookup map with a single key', () => { + const categoryId = new ObjectID(); + const pen = createProduct({name: 'pen', categoryId: categoryId}); + const pencil = createProduct({name: 'pencil', categoryId: categoryId}); + + const result = buildLookupMap( + [pen, pencil], + 'categoryId', + reduceAsArray, + ); + // expects this map to have String/Product pair + const expected = new Map>(); + const strId = categoryId.toString(); + expected.set(strId, [pen, pencil]); + expect(result).to.eql(expected); + }); + + it('creates a lookup map with more than one keys', () => { + const categoryId = new ObjectID(); + const anotherCategoryId = new ObjectID(); + const pen = createProduct({name: 'pen', categoryId: categoryId}); + const pencil = createProduct({name: 'pencil', categoryId: categoryId}); + const eraser = createProduct({ + name: 'eraser', + categoryId: anotherCategoryId, + }); + + const result = buildLookupMap( + [pen, eraser, pencil], + 'categoryId', + reduceAsArray, + ); + // expects this map to have String/Product pair + const expected = new Map>(); + const strId1 = categoryId.toString(); + const strId2 = anotherCategoryId.toString(); + expected.set(strId1, [pen, pencil]); + expected.set(strId2, [eraser]); + expect(result).to.eql(expected); + }); + }); + context('normalizeKey + flattenMapByKeys', () => { + it('checks if id has been normalized', async () => { + const categoryId = new ObjectID(); + const anotherCategoryId = new ObjectID(); + const pen = createProduct({name: 'pen', categoryId: categoryId}); + const pencil = createProduct({name: 'pencil', categoryId: categoryId}); + const eraser = createProduct({ + name: 'eraser', + categoryId: anotherCategoryId, + }); + // stub map + const map = new Map>(); + const strId1 = categoryId.toString(); + const strId2 = anotherCategoryId.toString(); + map.set(strId1, [pen, pencil]); + map.set(strId2, [eraser]); + + const result = flattenMapByKeys([anotherCategoryId, categoryId], map); + expect(result).to.eql([[eraser], [pen, pencil]]); + }); + }); + }); + + //** helpers + @model() + class Product extends Entity { + // uses unknown for id type in this test to get rid of type error: + // type 'ObjectId' is not assignable to parameter of type 'string'. + @property({id: true}) + id: unknown; + @property() + name: string; + @belongsTo(() => Category) + categoryId: unknown; + } + + @model() + class Category extends Entity { + @property({id: true}) + id?: unknown; + @property() + name: string; + @hasMany(() => Product, {keyTo: 'categoryId'}) + products?: Product[]; + } + + function createProduct(properties: Partial) { + return new Product(properties as Product); + } +}); diff --git a/packages/repository/src/__tests__/unit/repositories/relations-helpers/relations-helpers-fixtures.ts b/packages/repository/src/__tests__/unit/repositories/relations-helpers/relations-helpers-fixtures.ts index f6f0ec735256..d6aa0f49cef2 100644 --- a/packages/repository/src/__tests__/unit/repositories/relations-helpers/relations-helpers-fixtures.ts +++ b/packages/repository/src/__tests__/unit/repositories/relations-helpers/relations-helpers-fixtures.ts @@ -24,6 +24,10 @@ export class Product extends Entity { name: string; @belongsTo(() => Category) categoryId: number; + + constructor(data: Partial) { + super(data); + } } export class ProductRepository extends DefaultCrudRepository< @@ -55,6 +59,9 @@ export class Category extends Entity { name: string; @hasMany(() => Product, {keyTo: 'categoryId'}) products?: Product[]; + constructor(data: Partial) { + super(data); + } } interface CategoryRelations { products?: Product[]; @@ -85,3 +92,11 @@ export const testdb: juggler.DataSource = new juggler.DataSource({ name: 'db', connector: 'memory', }); + +export function createCategory(properties: Partial) { + return new Category(properties as Category); +} + +export function createProduct(properties: Partial) { + return new Product(properties as Product); +} diff --git a/packages/repository/src/relations/relation.helpers.ts b/packages/repository/src/relations/relation.helpers.ts index a79674e7b63a..55b0cd9daa16 100644 --- a/packages/repository/src/relations/relation.helpers.ts +++ b/packages/repository/src/relations/relation.helpers.ts @@ -3,6 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +import * as assert from 'assert'; import * as debugFactory from 'debug'; import * as _ from 'lodash'; import { @@ -46,7 +47,14 @@ export async function findByForeignKeys< if (Array.isArray(fkValues)) { if (fkValues.length === 0) return []; - value = fkValues.length === 1 ? fkValues[0] : {inq: fkValues}; + value = + fkValues.length === 1 + ? fkValues[0] + : { + // Create a copy to prevent query coercion algorithm + // inside connectors from modifying the original values + inq: [...fkValues], + }; } else { value = fkValues; } @@ -57,7 +65,7 @@ export async function findByForeignKeys< return targetRepository.find(targetFilter, options); } -type StringKeyOf = Extract; +export type StringKeyOf = Extract; /** * Returns model instances that include related models that have a registered @@ -132,3 +140,135 @@ function isInclusionAllowed( debug('isInclusionAllowed for %j (relation %s)? %s', include, allowed); return allowed; } + +/** + * Returns an array of instances from the target map. The order of arrays is based on + * the order of sourceIds + * + * @param sourceIds - One value or array of values (of the target key) + * @param targetMap - a map that matches sourceIds with instances + */ +export function flattenMapByKeys( + sourceIds: unknown[], + targetMap: Map, +): (T | undefined)[] { + const result: (T | undefined)[] = new Array(sourceIds.length); + // mongodb: use string as key of targetMap, and convert sourceId to strings + // to make sure it gets the related instances. + sourceIds.forEach((id, index) => { + const key = normalizeKey(id); + const target = targetMap.get(key); + result[index] = target; + }); + + return result; +} + +/** + * Returns a map which maps key values(ids) to instances. The instances can be + * grouped by different strategies. + * + * @param list - an array of instances + * @param keyName - key name of the source + * @param reducer - a strategy to reduce inputs to single item or array + */ +export function buildLookupMap( + list: InType[], + keyName: StringKeyOf, + reducer: (accumulator: OutType | undefined, current: InType) => OutType, +): Map { + const lookup = new Map(); + for (const entity of list) { + // get a correct key value + const key = getKeyValue(entity, keyName) as Key; + // these 3 steps are to set up the map, the map differs according to the reducer. + const original = lookup.get(key); + const reduced = reducer(original, entity); + lookup.set(key, reduced); + } + return lookup; +} + +/** + * Returns value of a keyName. Aims to resolve ObjectId problem of Mongo. + * + * @param model - target model + * @param keyName - target key that gets the value from + */ +export function getKeyValue(model: AnyObject, keyName: string) { + return normalizeKey(model[keyName]); +} + +/** + * Workaround for MongoDB, where the connector returns ObjectID + * values even for properties configured with "type: string". + * + * @param rawKey + */ +export function normalizeKey(rawKey: unknown) { + if (isBsonType(rawKey)) { + return rawKey.toString(); + } + return rawKey; +} + +/** + * Returns an array of instances. For HasMany relation usage. + * + * @param acc + * @param it + */ +export function reduceAsArray(acc: T[] | undefined, it: T) { + if (acc) acc.push(it); + else acc = [it]; + return acc; +} +/** + * Returns a single of an instance. For HasOne and BelongsTo relation usage. + * + * @param _acc + * @param it + */ +export function reduceAsSingleItem(_acc: T | undefined, it: T) { + return it; +} + +/** + * Dedupe an array + * @param {Array} input - an array of sourceIds + * @returns {Array} an array with unique items + */ +export function deduplicate(input: T[]): T[] { + const uniqArray: T[] = []; + if (!input) { + return uniqArray; + } + assert(Array.isArray(input), 'array argument is required'); + + const comparableArray = input.map(item => normalizeKey(item)); + for (let i = 0, n = comparableArray.length; i < n; i++) { + if (comparableArray.indexOf(comparableArray[i]) === i) { + uniqArray.push(input[i]); + } + } + return uniqArray; +} + +/** + * Checks if the value is BsonType (mongodb) + * It uses a general way to check the type ,so that it can detect + * different versions of bson that might be used in the code base. + * Might need to update in the future. + * + * @param value + */ +export function isBsonType(value: unknown): value is object { + if (typeof value !== 'object' || !value) return false; + + // bson@1.x stores _bsontype on ObjectID instance, bson@4.x on prototype + return check(value) || check(value.constructor.prototype); + + function check(target: unknown) { + return Object.prototype.hasOwnProperty.call(target, '_bsontype'); + } +}