diff --git a/.eslintrc.json b/.eslintrc.json index 1380dfe44..572177e3e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -153,6 +153,7 @@ } } ], + "no-use-before-define": "warn", "sort-keys": "warn" } } diff --git a/.vscode/settings.json b/.vscode/settings.json index 3662b3700..25fa6215f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { "typescript.tsdk": "node_modules/typescript/lib" -} \ No newline at end of file +} diff --git a/cspell.json b/cspell.json new file mode 100644 index 000000000..06870d823 --- /dev/null +++ b/cspell.json @@ -0,0 +1,8 @@ +{ + "version": "0.2", + "language": "en", + "words": [ + "bson", + "papr" + ] +} diff --git a/docs/api/model.md b/docs/api/model.md index 8604c5c1a..600596d6a 100644 --- a/docs/api/model.md +++ b/docs/api/model.md @@ -94,7 +94,7 @@ Calls the MongoDB [`countDocuments()`](https://mongodb.github.io/node-mongodb-na | Name | Type | Attribute | | --------- | ----------------------- | --------- | -| `filter` | `StrictFilter` | required | +| `filter` | `PaprFilter` | required | | `options` | `CountDocumentsOptions` | optional | **Returns:** @@ -114,10 +114,10 @@ Calls the MongoDB [`deleteMany()`](https://mongodb.github.io/node-mongodb-native **Parameters:** -| Name | Type | Attribute | -| --------- | ----------------------- | --------- | -| `filter` | `StrictFilter` | required | -| `options` | `DeleteOptions` | optional | +| Name | Type | Attribute | +| --------- | --------------------- | --------- | +| `filter` | `PaprFilter` | required | +| `options` | `DeleteOptions` | optional | **Returns:** @@ -135,10 +135,10 @@ Calls the MongoDB [`deleteOne()`](https://mongodb.github.io/node-mongodb-native/ **Parameters:** -| Name | Type | Attribute | -| --------- | ----------------------- | --------- | -| `filter` | `StrictFilter` | required | -| `options` | `DeleteOptions` | optional | +| Name | Type | Attribute | +| --------- | --------------------- | --------- | +| `filter` | `PaprFilter` | required | +| `options` | `DeleteOptions` | optional | **Returns:** @@ -156,11 +156,11 @@ Calls the MongoDB [`distinct()`](https://mongodb.github.io/node-mongodb-native/5 **Parameters:** -| Name | Type | Attribute | -| --------- | ----------------------- | --------- | -| `key` | `keyof TSchema` | required | -| `filter` | `StrictFilter` | optional | -| `options` | `DistinctOptions` | optional | +| Name | Type | Attribute | +| --------- | --------------------- | --------- | +| `key` | `keyof TSchema` | required | +| `filter` | `PaprFilter` | optional | +| `options` | `DistinctOptions` | optional | **Returns:** @@ -180,7 +180,7 @@ Performs an optimized `find` to test for the existence of any document matching | Name | Type | Attribute | | --------- | --------------------------------------------------------------------------- | --------- | -| `filter` | `StrictFilter` | required | +| `filter` | `PaprFilter` | required | | `options` | `Omit, ("projection" \| "limit" \| "sort" \| "skip")>` | optional | **Returns:** @@ -205,10 +205,10 @@ The result type (`TProjected`) takes into account the projection for this query **Parameters:** -| Name | Type | Attribute | -| --------- | ----------------------- | --------- | -| `filter` | `StrictFilter` | required | -| `options` | `FindOptions` | optional | +| Name | Type | Attribute | +| --------- | ---------------------- | --------- | +| `filter` | `PaprFilter` | required | +| `options` | `FindOptions` | optional | **Returns:** @@ -265,10 +265,10 @@ The result type (`TProjected`) takes into account the projection for this query **Parameters:** -| Name | Type | Attribute | -| --------- | ----------------------- | --------- | -| `filter` | `StrictFilter` | required | -| `options` | `FindOptions` | optional | +| Name | Type | Attribute | +| --------- | ---------------------- | --------- | +| `filter` | `PaprFilter` | required | +| `options` | `FindOptions` | optional | **Returns:** @@ -296,7 +296,7 @@ The result type (`TProjected`) takes into account the projection for this query | Name | Type | Attribute | | --------- | ------------------------- | --------- | -| `filter` | `StrictFilter` | required | +| `filter` | `PaprFilter` | required | | `options` | `FindOneAndUpdateOptions` | optional | **Returns:** @@ -317,11 +317,11 @@ The result type (`TProjected`) takes into account the projection for this query **Parameters:** -| Name | Type | Attribute | -| --------- | ----------------------------- | --------- | -| `filter` | `StrictFilter` | required | -| `update` | `StrictUpdateFilter` | required | -| `options` | `FindOneAndUpdateOptions` | optional | +| Name | Type | Attribute | +| --------- | --------------------------- | --------- | +| `filter` | `PaprFilter` | required | +| `update` | `PaprUpdateFilter` | required | +| `options` | `FindOneAndUpdateOptions` | optional | **Returns:** @@ -397,11 +397,11 @@ Calls the MongoDB [`updateMany()`](https://mongodb.github.io/node-mongodb-native **Parameters:** -| Name | Type | Attribute | -| --------- | ----------------------------- | --------- | -| `filter` | `StrictFilter` | required | -| `update` | `StrictUpdateFilter` | required | -| `options` | `UpdateOptions` | optional | +| Name | Type | Attribute | +| --------- | --------------------------- | --------- | +| `filter` | `PaprFilter` | required | +| `update` | `PaprUpdateFilter` | required | +| `options` | `UpdateOptions` | optional | **Returns:** @@ -419,11 +419,11 @@ Calls the MongoDB [`updateOne()`](https://mongodb.github.io/node-mongodb-native/ **Parameters:** -| Name | Type | Attribute | -| --------- | ----------------------------- | --------- | -| `filter` | `StrictFilter` | required | -| `update` | `StrictUpdateFilter` | required | -| `options` | `UpdateOptions` | optional | +| Name | Type | Attribute | +| --------- | --------------------------- | --------- | +| `filter` | `PaprFilter` | required | +| `update` | `PaprUpdateFilter` | required | +| `options` | `UpdateOptions` | optional | **Returns:** @@ -441,10 +441,10 @@ Calls the MongoDB [`findOneAndUpdate()`](https://mongodb.github.io/node-mongodb- **Parameters:** -| Name | Type | Attribute | -| -------- | ----------------------------- | --------- | -| `filter` | `StrictFilter` | required | -| `update` | `StrictUpdateFilter` | required | +| Name | Type | Attribute | +| -------- | --------------------------- | --------- | +| `filter` | `PaprFilter` | required | +| `update` | `PaprUpdateFilter` | required | **Returns:** diff --git a/docs/recipes.md b/docs/recipes.md index 37517b02a..1d0411215 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -199,15 +199,16 @@ Starting with `mongodb` v4.3.0, these types were enhanced to support dot notatio However, `mongodb` v5.0.0 [removed these types](https://github.com/mongodb/node-mongodb-native/blob/main/etc/notes/CHANGES_5.0.0.md#dot-notation-typescript-support-removed-by-default) as the default ones used in their methods and reverted to the old ones without dot notation support. The previous enhanced types were not removed, instead they were renamed to `StrictFilter` and `StrictUpdateFilter`, but they aren't referenced in any of their methods. -Papr is using the strict types to provide type safety for all query and update filters. +Papr v11 has adopted and enhanced these strict types to provide type safety for all query and update filters. -This comes with a caveat: whenever you need to interact with the `mongodb` driver collections, you need to cast filter types to their simple counterparts, since `Filter` is not compatible with `StrictFilter`. +This comes with a caveat: whenever you need to interact with the `mongodb` driver collections, you need to cast filter types to their simple counterparts, since `Filter` is not compatible with `PaprFilter`. ```ts -import { Filter, StrictFitler } from 'mongodb'; +import { Filter } from 'mongodb'; +import { PaprFilter } from 'papr'; import User, { UserDocument } from './user'; -const filter: StrictFilter = { +const filter: PaprFilter = { firstName: 'John', }; diff --git a/example/User.ts b/example/User.ts index 62ca99b75..5ba1910e0 100644 --- a/example/User.ts +++ b/example/User.ts @@ -4,9 +4,20 @@ import papr from './papr'; const userSchema = schema( { active: types.boolean(), + address: types.object({ + country: types.string({ required: true }), + zip: types.number({ required: true }), + }), age: types.number(), firstName: types.string({ required: true }), lastName: types.string({ required: true }), + orders: types.array( + types.object({ + product: types.string({ required: true }), + quantity: types.number({ required: true }), + }) + ), + tags: types.array(types.string()), }, { defaults: { active: true }, diff --git a/package.json b/package.json index 0c7c80d87..9eaa09be5 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "standard-version": "9.5.0", "ts-expect": "1.3.0", "ts-node": "10.9.1", - "typescript": "4.9.3" + "typescript": "4.9.5" }, "peerDependencies": { "mongodb": "^5.0.0" diff --git a/src/__tests__/model.test.ts b/src/__tests__/model.test.ts index e45a20300..61d1ff867 100644 --- a/src/__tests__/model.test.ts +++ b/src/__tests__/model.test.ts @@ -3,9 +3,9 @@ import { Collection, MongoError, ObjectId } from 'mongodb'; import { expectType } from 'ts-expect'; import { Hooks } from '../hooks'; import { abstract, build, Model } from '../model'; +import { PaprBulkWriteOperation } from '../mongodbTypes'; import schema from '../schema'; import Types from '../types'; -import { BulkWriteOperation } from '../utils'; describe('model', () => { let collection: Collection; @@ -219,7 +219,7 @@ describe('model', () => { describe('bulkWrite', () => { test('simple schema', async () => { - const operations: BulkWriteOperation[] = [ + const operations: PaprBulkWriteOperation[] = [ { insertOne: { document: { @@ -273,7 +273,7 @@ describe('model', () => { }); test('schema with defaults', async () => { - const operations: BulkWriteOperation[] = [ + const operations: PaprBulkWriteOperation[] = [ { insertOne: { document: { diff --git a/src/__tests__/mongodbTypes.test.ts b/src/__tests__/mongodbTypes.test.ts new file mode 100644 index 000000000..8efba3db7 --- /dev/null +++ b/src/__tests__/mongodbTypes.test.ts @@ -0,0 +1,948 @@ +import { describe, expect, test } from '@jest/globals'; +import { + Binary, + BSONSymbol, + Code, + DBRef, + Decimal128, + Double, + Int32, + Long, + MaxKey, + MinKey, + ObjectId, +} from 'mongodb'; +import { expectType } from 'ts-expect'; +import { PaprFilter, PaprUpdateFilter } from '../mongodbTypes'; + +describe('mongodb types', () => { + interface TestDocument { + _id: ObjectId; + foo: string; + bar: number; + ham?: Date; + tags: string[]; + numbers: number[]; + list: { + direct: string; + other?: number; + }[]; + nestedObject: { + deep: { + deeper: string; + other?: number; + }; + direct: boolean; + other?: number; + }; + binary: Binary; + bsonSymbol: BSONSymbol; + dbRef: DBRef; + double: Double; + code: Code; + decimal: Decimal128; + int32: Int32; + long: Long; + maxKey: MaxKey; + minKey: MinKey; + regexp: RegExp; + } + + describe('PaprFilter', () => { + describe('existing top-level keys', () => { + test('valid types', () => { + expect(true).toBeTruthy(); + + expectType>({ _id: new ObjectId() }); + + expectType>({ foo: 'foo' }); + // string fields can be queried by regexp + expectType>({ foo: /foo/ }); + + expectType>({ bar: 123 }); + + expectType>({ ham: new Date() }); + + // array fields can be queried by exact match + expectType>({ tags: ['foo'] }); + // array fields can be queried by element type + expectType>({ tags: 'foo' }); + expectType>({ tags: /foo/ }); + + expectType>({ + nestedObject: { + deep: { deeper: 'foo' }, + direct: true, + }, + }); + + // all BSON types can be used as query values + expectType>({ binary: new Binary('', 2) }); + expectType>({ bsonSymbol: new BSONSymbol('hi') }); + expectType>({ code: new Code(() => true) }); + expectType>({ double: new Double(123.45) }); + expectType>({ dbRef: new DBRef('collection', new ObjectId()) }); + expectType>({ decimal: new Decimal128('123.45') }); + expectType>({ int32: new Int32('123') }); + expectType>({ long: new Long('123', 45) }); + expectType>({ maxKey: new MaxKey() }); + expectType>({ minKey: new MinKey() }); + expectType>({ regexp: /foo/ }); + }); + + test('invalid types', () => { + expect(true).toBeTruthy(); + + // @ts-expect-error Type mismatch + expectType>({ _id: '577fa2d90c4cc47e31cf4b6f' }); + + // @ts-expect-error Type mismatch + expectType>({ foo: true }); + // @ts-expect-error Type mismatch + expectType>({ foo: ['foo'] }); + + // @ts-expect-error Type mismatch + expectType>({ bar: '123' }); + // @ts-expect-error Type mismatch + expectType>({ bar: [123] }); + + // @ts-expect-error Type mismatch + expectType>({ ham: 123 }); + + // @ts-expect-error Type mismatch + expectType>({ tags: 123 }); + + expectType>({ + nestedObject: { + deep: { deeper: 'foo' }, + // @ts-expect-error Type mismatch + direct: 'foo', + }, + }); + expectType>({ + nestedObject: { + // @ts-expect-error Type mismatch + deep: { deeper: 123 }, + direct: true, + }, + }); + }); + }); + + describe('existing nested keys using dot notation', () => { + test('valid types', () => { + // https://www.mongodb.com/docs/manual/tutorial/query-embedded-documents/#query-on-nested-field + expectType>({ 'nestedObject.direct': true }); + expectType>({ 'nestedObject.other': 123 }); + expectType>({ 'nestedObject.deep.deeper': 'foo' }); + expectType>({ 'nestedObject.deep.other': 123 }); + + // https://www.mongodb.com/docs/manual/tutorial/query-array-of-documents/#use-the-array-index-to-query-for-a-field-in-the-embedded-document + expectType>({ 'list.0.direct': 'foo' }); + expectType>({ 'list.1.other': 123 }); + // it works with some extreme indexes + expectType>({ 'list.4294967295.other': 123 }); + expectType>({ 'list.9999999999999999999.other': 123 }); + + expectType>({ 'tags.0': 'foo' }); + + // https://www.mongodb.com/docs/manual/tutorial/query-array-of-documents/#specify-a-query-condition-on-a-field-embedded-in-an-array-of-documents + expectType>({ 'list.direct': 'foo' }); + expectType>({ 'list.other': 123 }); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch + expectType>({ 'tags.0': 123 }); + + // @ts-expect-error Type mismatch + expectType>({ 'nestedObject.direct': 'foo' }); + // @ts-expect-error Type mismatch + expectType>({ 'nestedObject.other': 'foo' }); + // @ts-expect-error Type mismatch + expectType>({ 'nestedObject.deep.deeper': 123 }); + // @ts-expect-error Type mismatch + expectType>({ 'nestedObject.deep.other': 'foo' }); + + // @ts-expect-error Type mismatch + expectType>({ 'list.0.direct': 123 }); + // @ts-expect-error Type mismatch + expectType>({ 'list.1.other': 'foo' }); + }); + }); + + test('BSON types should not be broken up in nested keys', () => { + // @ts-expect-error Type mismatch + expectType>({ 'binary.sub_type': 2 }); + // @ts-expect-error Type mismatch + expectType>({ 'bsonSymbol.value': 'hi' }); + // @ts-expect-error Type mismatch + expectType>({ 'code.code': 'string' }); + // @ts-expect-error Type mismatch + expectType>({ 'dbRef.collection': 'collection' }); + // @ts-expect-error Type mismatch + expectType>({ 'decimal.bytes.BYTES_PER_ELEMENT': 1 }); + // @ts-expect-error Type mismatch + expectType>({ 'maxKey._bsontype': 'MaxKey' }); + // @ts-expect-error Type mismatch + expectType>({ 'minKey._bsontype': 'MinKey' }); + // @ts-expect-error Type mismatch + expectType>({ 'regexp.dotAll': true }); + }); + + test('invalid keys', () => { + // @ts-expect-error Invalid key + expectType>({ inexistent: 'foo' }); + + // @ts-expect-error Invalid key + expectType>({ 'nestedObject.inexistent': 'foo' }); + // @ts-expect-error Invalid key + expectType>({ 'nestedObject.deep.inexistent': 'foo' }); + // @ts-expect-error Invalid key + expectType>({ 'nestedObject.inexistent.deeper': 'foo' }); + + // @ts-expect-error Invalid key + expectType>({ 'list.0.inexistent': 'foo' }); + // @ts-expect-error Invalid key + expectType>({ 'inexistent.0.other': 'foo' }); + }); + + describe('root filter operators', () => { + test('valid usage', () => { + expectType>({ $and: [{ foo: 'foo' }] }); + expectType>({ $and: [{ foo: 'foo' }, { bar: 123 }] }); + + expectType>({ $or: [{ foo: 'foo' }, { bar: 123 }] }); + + expectType>({ $nor: [{ foo: 'foo' }] }); + }); + + test('invalid usage', () => { + // @ts-expect-error Type mismatch: should not accept single objects for __$and, $or, $nor operator__ query + expectType>({ $and: { foo: 'foo' } }); + + // @ts-expect-error Type mismatch: should not accept __$and, $or, $nor operator__ as non-root query + expectType>({ foo: { $or: ['foo', 'bar'] } }); + }); + }); + + describe('filter operators', () => { + describe('logical filter operators', () => { + test('valid types on existing top-level keys', () => { + expect(true).toBeTruthy(); + + expectType>({ _id: { $in: [new ObjectId()] } }); + + expectType>({ foo: { $eq: 'foo' } }); + expectType>({ foo: { $eq: /foo/ } }); + expectType>({ foo: { $not: { $eq: 'foo' } } }); + expectType>({ foo: { $not: { $eq: /foo/ } } }); + + expectType>({ bar: { $gt: 123, $lt: 1000 } }); + + expectType>({ ham: { $nin: [new Date()] } }); + + expectType>({ tags: { $in: ['foo', 'bar'] } }); + }); + + test('valid types on existing nested keys using dot notation', () => { + // https://www.mongodb.com/docs/manual/tutorial/query-embedded-documents/#query-on-nested-field + expectType>({ 'nestedObject.direct': { $eq: true } }); + expectType>({ 'nestedObject.other': { $lt: 123 } }); + expectType>({ 'nestedObject.deep.deeper': { $in: ['foo'] } }); + expectType>({ 'nestedObject.deep.other': { $gte: 123 } }); + + // https://www.mongodb.com/docs/manual/tutorial/query-array-of-documents/#use-the-array-index-to-query-for-a-field-in-the-embedded-document + expectType>({ 'list.0.direct': { $ne: 'foo' } }); + expectType>({ 'list.1.other': { $ne: 123 } }); + + // https://www.mongodb.com/docs/manual/tutorial/query-array-of-documents/#specify-a-query-condition-on-a-field-embedded-in-an-array-of-documents + expectType>({ 'list.direct': { $ne: 'foo' } }); + expectType>({ 'list.other': { $ne: 123 } }); + }); + + test('invalid types on existing top-level keys', () => { + // @ts-expect-error Type mismatch + expectType>({ _id: { $in: ['foo'] } }); + // @ts-expect-error Type mismatch + expectType>({ foo: { $eq: true } }); + // @ts-expect-error Type mismatch + expectType>({ foo: { $not: 'foo' } }); + // @ts-expect-error Type mismatch + expectType>({ bar: { $gt: 'foo' } }); + // @ts-expect-error Type mismatch + expectType>({ ham: { $nin: [123] } }); + }); + + test('invalid types on existing nested keys using dot notation', () => { + // @ts-expect-error Type mismatch + expectType>({ 'nestedObject.direct': { $eq: 'foo' } }); + // @ts-expect-error Type mismatch + expectType>({ 'nestedObject.other': { $lt: true } }); + // @ts-expect-error Type mismatch + expectType>({ 'nestedObject.deep.deeper': { $in: [123] } }); + // @ts-expect-error Type mismatch + expectType>({ 'nestedObject.deep.other': { $gte: 'foo' } }); + + // @ts-expect-error Type mismatch + expectType>({ 'list.0.direct': { $ne: 123 } }); + // @ts-expect-error Type mismatch + expectType>({ 'list.1.other': { $ne: 'foo' } }); + }); + }); + + describe('element filter operators', () => { + test('valid types', () => { + expectType>({ foo: { $exists: true } }); + expectType>({ foo: { $exists: false } }); + + expectType>({ 'tags.0': { $exists: true } }); + expectType>({ 'list.0': { $exists: true } }); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch: should not query $exists by wrong values + expectType>({ foo: { $exists: '' } }); + // @ts-expect-error Type mismatch: should not query $exists by wrong values + expectType>({ foo: { $exists: 'true' } }); + + // @ts-expect-error Type mismatch: should not query $exists by wrong values + expectType>({ 'tags.0': { $exists: '' } }); + // @ts-expect-error Type mismatch: should not query $exists by wrong values + expectType>({ 'list.0': { $exists: 1 } }); + }); + }); + + describe('evaluation filter operators', () => { + test('valid types', () => { + expectType>({ foo: { $regex: /foo/ } }); + expectType>({ foo: { $options: 'i', $regex: /foo/ } }); + + expectType>({ bar: { $mod: [12, 2] } }); + + expectType>({ $text: { $search: 'foo' } }); + + expectType>({ $expr: { $gt: ['$decimal', '$bar'] } }); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch: should not accept $regex for none string fields + expectType>({ bar: { $regex: /12/ } }); + + // @ts-expect-error Type mismatch: should not accept $mod for none number fields + expectType>({ foo: { $mod: [12, 2] } }); + // @ts-expect-error Type mismatch: should not accept $mod with less/more than 2 elements + expectType>({ bar: { $mod: [12] } }); + // @ts-expect-error Type mismatch: should not accept $mod with less/more than 2 elements + expectType>({ bar: { $mod: [] } }); + + // @ts-expect-error Type mismatch: should fulltext search only by string + expectType>({ $text: { $search: 123 } }); + // @ts-expect-error Type mismatch: should fulltext search only by string + expectType>({ $text: { $search: /foo/ } }); + }); + }); + + describe('array filter operators', () => { + test('valid types', () => { + expectType>({ tags: { $size: 2 } }); + + expectType>({ tags: { $all: ['foo', 'bar'] } }); + + expectType>({ list: { $elemMatch: { direct: 'foo' } } }); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch: should not query non array fields + expectType>({ foo: { $size: 2 } }); + + // @ts-expect-error Type mismatch: should not query non array fields + expectType>({ foo: { $all: ['foo', 'bar'] } }); + + // @ts-expect-error Type mismatch: should not query non array fields + expectType>({ foo: { $elemMatch: { direct: 'foo' } } }); + }); + }); + }); + }); + + describe('PaprUpdateFilter', () => { + describe('$currentDate', () => { + test('valid types', () => { + expectType>({ $currentDate: { ham: true } }); + expectType>({ $currentDate: { ham: { $type: 'date' } } }); + expectType>({ + $currentDate: { ham: { $type: 'timestamp' } }, + }); + }); + + test('invalid keys', () => { + // @ts-expect-error Invalid key + expectType>({ $currentDate: { foo: true } }); + // @ts-expect-error Invalid key + expectType>({ $currentDate: { bar: { $type: 'date' } } }); + }); + }); + + describe('$inc', () => { + describe('top-level keys', () => { + test('valid types', () => { + expectType>({ $inc: { bar: 123 } }); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch + expectType>({ $inc: { bar: 'foo' } }); + }); + + // These are not enforces in the `OnlyFieldsOfType` type: + describe('existing nested keys using dot notation', () => { + test.todo('valid types'); + test.todo('invalid types'); + }); + }); + + test('invalid keys', () => { + // @ts-expect-error Invalid key + expectType>({ $inc: { foo: 123 } }); + }); + + describe('array filters', () => { + test('valid types', () => { + expectType>({ $inc: { 'numbers.$': 123 } }); + expectType>({ $inc: { 'numbers.$[bla]': 123 } }); + expectType>({ $inc: { 'numbers.$[]': 123 } }); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch + expectType>({ $inc: { 'numbers.$': 'foo' } }); + // @ts-expect-error Type mismatch + expectType>({ $inc: { 'numbers.$[bla]': 'foo' } }); + // @ts-expect-error Type mismatch + expectType>({ $inc: { 'numbers.$[]': 'foo' } }); + }); + }); + }); + + describe('$set', () => { + describe('top-level keys', () => { + test('valid types', () => { + expectType>({ $set: { _id: new ObjectId() } }); + + expectType>({ $set: { foo: 'foo' } }); + + expectType>({ $set: { bar: 123 } }); + + expectType>({ $set: { ham: new Date() } }); + + expectType>({ $set: { tags: ['foo', 'bar'] } }); + + expectType>({ + $set: { + nestedObject: { + deep: { deeper: 'foo' }, + direct: true, + }, + }, + }); + + // all BSON types can be used as update values + expectType>({ $set: { binary: new Binary('', 2) } }); + expectType>({ + $set: { bsonSymbol: new BSONSymbol('hi') }, + }); + expectType>({ $set: { code: new Code(() => true) } }); + expectType>({ $set: { double: new Double(123.45) } }); + expectType>({ + $set: { dbRef: new DBRef('collection', new ObjectId()) }, + }); + expectType>({ + $set: { decimal: new Decimal128('123.45') }, + }); + expectType>({ $set: { int32: new Int32('123') } }); + expectType>({ $set: { long: new Long('123', 45) } }); + expectType>({ $set: { maxKey: new MaxKey() } }); + expectType>({ $set: { minKey: new MinKey() } }); + expectType>({ $set: { regexp: /foo/ } }); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch + expectType>({ $set: { _id: 'foo' } }); + + // @ts-expect-error Type mismatch + expectType>({ $set: { foo: 123 } }); + + // @ts-expect-error Type mismatch + expectType>({ $set: { bar: 'foo' } }); + + // @ts-expect-error Type mismatch + expectType>({ $set: { ham: 123 } }); + + // @ts-expect-error Type mismatch + expectType>({ $set: { tags: 'foo' } }); + + expectType>({ + $set: { + nestedObject: { + deep: { deeper: 'foo' }, + // @ts-expect-error Type mismatch + direct: 'foo', + }, + }, + }); + expectType>({ + $set: { + nestedObject: { + // @ts-expect-error Type mismatch + deep: { deeper: 123 }, + direct: true, + }, + }, + }); + }); + + describe('existing nested keys using dot notation', () => { + test('valid types', () => { + expectType>({ $set: { 'nestedObject.direct': true } }); + expectType>({ $set: { 'nestedObject.other': 123 } }); + + expectType>({ + $set: { 'nestedObject.deep': { deeper: 'foo' } }, + }); + expectType>({ + $set: { 'nestedObject.deep.deeper': 'foo' }, + }); + expectType>({ + $set: { 'nestedObject.deep.other': 123 }, + }); + + expectType>({ $set: { 'tags.0': 'foo' } }); + + expectType>({ $set: { 'list.0.direct': 'foo' } }); + expectType>({ $set: { 'list.1.other': 123 } }); + + expectType>({ $set: { 'list.direct': 'foo' } }); + expectType>({ $set: { 'list.other': 123 } }); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch + expectType>({ $set: { 'nestedObject.direct': 'foo' } }); + // @ts-expect-error Type mismatch + expectType>({ $set: { 'nestedObject.other': 'foo' } }); + expectType>({ + // @ts-expect-error Type mismatch + $set: { 'nestedObject.deep.deeper': 123 }, + }); + expectType>({ + // @ts-expect-error Type mismatch + $set: { 'nestedObject.deep.other': 'foo' }, + }); + + // @ts-expect-error Type mismatch + expectType>({ $set: { 'tags.0': 123 } }); + + // @ts-expect-error Type mismatch + expectType>({ $set: { 'list.0.direct': 123 } }); + // @ts-expect-error Type mismatch + expectType>({ $set: { 'list.1.other': 'foo' } }); + }); + }); + }); + + test('invalid keys', () => { + // @ts-expect-error Invalid key + expectType>({ $set: { inexistent: 'foo' } }); + + // @ts-expect-error Invalid key + expectType>({ $set: { 'nestedObject.inexistent': 'foo' } }); + expectType>({ + // @ts-expect-error Invalid key + $set: { 'nestedObject.deep.inexistent': 'foo' }, + }); + expectType>({ + // @ts-expect-error Invalid key + $set: { 'nestedObject.inexistent.deeper': 'foo' }, + }); + + // @ts-expect-error Invalid key + expectType>({ $set: { 'list.0.inexistent': 'foo' } }); + // @ts-expect-error Invalid key + expectType>({ $set: { 'inexistent.0.other': 'foo' } }); + }); + + describe('array filters', () => { + test('valid types', () => { + expectType>({ $set: { 'tags.$': 'foo' } }); + expectType>({ $set: { 'tags.$[bla]': 'foo' } }); + expectType>({ $set: { 'tags.$[]': 'foo' } }); + + // These are not yet enforced in the `PaprMatchKeysAndValues` type + // expectType>({ $set: { 'list.$.direct': 'foo' } }); + // expectType>({ $set: { 'list.$[bla].direct': 'foo' } }); + // expectType>({ $set: { 'list.$[].direct': 'foo' } }); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch + expectType>({ $set: { 'tags.$': 123 } }); + // @ts-expect-error Type mismatch + expectType>({ $set: { 'tags.$[bla]': 123 } }); + // @ts-expect-error Type mismatch + expectType>({ $set: { 'tags.$[]': 123 } }); + + // These are not yet enforced in the `PaprMatchKeysAndValues` type + // expectType>({ $set: { 'list.$.direct': 123 } }); + // expectType>({ $set: { 'list.$[bla].direct': 123 } }); + // expectType>({ $set: { 'list.$[].direct': 123 } }); + }); + }); + }); + + describe('$pull', () => { + test('valid types', () => { + expectType>({ $pull: { tags: 'foo' } }); + + expectType>({ $pull: { list: { direct: 'foo' } } }); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch + expectType>({ $pull: { tags: 123 } }); + + // @ts-expect-error Type mismatch + expectType>({ $pull: { list: { direct: 123 } } }); + }); + + test('invalid keys', () => { + // @ts-expect-error Invalid key + expectType>({ $pull: { foo: 'foo' } }); + // @ts-expect-error Invalid key + expectType>({ $pull: { bar: 123 } }); + }); + }); + + describe('$push', () => { + test('valid types', () => { + expectType>({ $push: { tags: 'foo' } }); + expectType>({ $push: { tags: { $each: ['foo'] } } }); + expectType>({ + $push: { + tags: { $each: ['foo'], $slice: 2 }, + }, + }); + expectType>({ + $push: { + tags: { $each: ['foo'], $position: 3 }, + }, + }); + expectType>({ + $push: { + tags: { $each: ['foo'], $sort: 1 }, + }, + }); + + expectType>({ $push: { list: { direct: 'foo' } } }); + expectType>({ + $push: { list: { $each: [{ direct: 'foo' }] } }, + }); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch + expectType>({ $push: { tags: 123 } }); + // @ts-expect-error Type mismatch + expectType>({ $push: { tags: { $each: 123 } } }); + // @ts-expect-error Type mismatch + expectType>({ $push: { tags: { $each: [123] } } }); + + // @ts-expect-error Type mismatch + expectType>({ $push: { list: { direct: 123 } } }); + }); + + test('invalid keys', () => { + // @ts-expect-error Invalid key + expectType>({ $push: { foo: 'foo' } }); + // @ts-expect-error Invalid key + expectType>({ $push: { bar: 123 } }); + }); + }); + + describe('$setOnInsert', () => { + describe('top-level keys', () => { + test('valid types', () => { + expectType>({ $setOnInsert: { _id: new ObjectId() } }); + + expectType>({ $setOnInsert: { foo: 'foo' } }); + + expectType>({ $setOnInsert: { bar: 123 } }); + + expectType>({ $setOnInsert: { ham: new Date() } }); + + expectType>({ $setOnInsert: { tags: ['foo', 'bar'] } }); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch + expectType>({ $setOnInsert: { _id: 'foo' } }); + + // @ts-expect-error Type mismatch + expectType>({ $setOnInsert: { foo: 123 } }); + + // @ts-expect-error Type mismatch + expectType>({ $setOnInsert: { bar: 'foo' } }); + + // @ts-expect-error Type mismatch + expectType>({ $setOnInsert: { ham: 123 } }); + + // @ts-expect-error Type mismatch + expectType>({ $setOnInsert: { tags: 'foo' } }); + + expectType>({ + $setOnInsert: { + nestedObject: { + deep: { deeper: 'foo' }, + // @ts-expect-error Type mismatch + direct: 'foo', + }, + }, + }); + }); + + describe('existing nested keys using dot notation', () => { + test('valid types', () => { + expectType>({ + $setOnInsert: { 'nestedObject.direct': true }, + }); + expectType>({ + $setOnInsert: { 'nestedObject.other': 123 }, + }); + + expectType>({ + $setOnInsert: { 'nestedObject.deep': { deeper: 'foo' } }, + }); + expectType>({ + $setOnInsert: { 'nestedObject.deep.deeper': 'foo' }, + }); + expectType>({ + $setOnInsert: { 'nestedObject.deep.other': 123 }, + }); + + expectType>({ $setOnInsert: { 'tags.0': 'foo' } }); + + expectType>({ + $setOnInsert: { 'list.0.direct': 'foo' }, + }); + expectType>({ $setOnInsert: { 'list.1.other': 123 } }); + + expectType>({ $setOnInsert: { 'list.direct': 'foo' } }); + expectType>({ $setOnInsert: { 'list.other': 123 } }); + }); + + test('invalid types', () => { + expectType>({ + // @ts-expect-error Type mismatch + $setOnInsert: { 'nestedObject.direct': 'foo' }, + }); + expectType>({ + // @ts-expect-error Type mismatch + $setOnInsert: { 'nestedObject.other': 'foo' }, + }); + expectType>({ + // @ts-expect-error Type mismatch + $setOnInsert: { 'nestedObject.deep.deeper': 123 }, + }); + expectType>({ + // @ts-expect-error Type mismatch + $setOnInsert: { 'nestedObject.deep.other': 'foo' }, + }); + + // @ts-expect-error Type mismatch + expectType>({ $setOnInsert: { 'tags.0': 123 } }); + + // @ts-expect-error Type mismatch + expectType>({ $setOnInsert: { 'list.0.direct': 123 } }); + // @ts-expect-error Type mismatch + expectType>({ $setOnInsert: { 'list.1.other': 'foo' } }); + }); + }); + }); + + test('invalid keys', () => { + // @ts-expect-error Invalid key + expectType>({ $setOnInsert: { inexistent: 'foo' } }); + + expectType>({ + // @ts-expect-error Invalid key + $setOnInsert: { 'nestedObject.inexistent': 'foo' }, + }); + expectType>({ + // @ts-expect-error Invalid key + $setOnInsert: { 'nestedObject.deep.inexistent': 'foo' }, + }); + expectType>({ + // @ts-expect-error Invalid key + $setOnInsert: { 'nestedObject.inexistent.deeper': 'foo' }, + }); + + expectType>({ + // @ts-expect-error Invalid key + $setOnInsert: { 'list.0.inexistent': 'foo' }, + }); + expectType>({ + // @ts-expect-error Invalid key + $setOnInsert: { 'inexistent.0.other': 'foo' }, + }); + }); + + describe('array filters', () => { + test('valid types', () => { + expectType>({ $setOnInsert: { 'tags.$': 'foo' } }); + expectType>({ $setOnInsert: { 'tags.$[bla]': 'foo' } }); + expectType>({ $setOnInsert: { 'tags.$[]': 'foo' } }); + + // These are not yet enforced in the `PaprMatchKeysAndValues` type + // expectType>({ $setOnInsert: { 'list.$.direct': 'foo' } }); + // expectType>({ + // $setOnInsert: { 'list.$[bla].direct': 'foo' }, + // }); + // expectType>({ + // $setOnInsert: { 'list.$[].direct': 'foo' }, + // }); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch + expectType>({ $setOnInsert: { 'tags.$': 123 } }); + // @ts-expect-error Type mismatch + expectType>({ $setOnInsert: { 'tags.$[bla]': 123 } }); + // @ts-expect-error Type mismatch + expectType>({ $setOnInsert: { 'tags.$[]': 123 } }); + + // These are not yet enforced in the `PaprMatchKeysAndValues` type + // expectType>({ $setOnInsert: { 'list.$.direct': 123 } }); + // expectType>({ $setOnInsert: { 'list.$[bla].direct': 123 } }); + // expectType>({ $setOnInsert: { 'list.$[].direct': 123 } }); + }); + }); + }); + + describe('$unset', () => { + describe('top-level keys', () => { + test('valid types', () => { + expectType>({ $unset: { _id: 1 } }); + expectType>({ $unset: { foo: 1 } }); + expectType>({ $unset: { bar: '' } }); + expectType>({ $unset: { ham: true } }); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch + expectType>({ $unset: { _id: 'foo' } }); + // @ts-expect-error Type mismatch + expectType>({ $unset: { foo: 2 } }); + }); + + describe('existing nested keys using dot notation', () => { + test('valid types', () => { + expectType>({ $unset: { 'nestedObject.direct': 1 } }); + expectType>({ $unset: { 'nestedObject.other': 1 } }); + + expectType>({ + $unset: { 'nestedObject.deep': 1 }, + }); + expectType>({ + $unset: { 'nestedObject.deep.deeper': 1 }, + }); + expectType>({ + $unset: { 'nestedObject.deep.other': 1 }, + }); + + expectType>({ $unset: { 'tags.0': 1 } }); + + expectType>({ $unset: { 'list.0.direct': 1 } }); + expectType>({ $unset: { 'list.1.other': 1 } }); + + expectType>({ $unset: { 'list.direct': 1 } }); + expectType>({ $unset: { 'list.other': 1 } }); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch + expectType>({ $unset: { 'nestedObject.direct': 123 } }); + // @ts-expect-error Type mismatch + expectType>({ $unset: { 'nestedObject.other': 123 } }); + expectType>({ + // @ts-expect-error Type mismatch + $unset: { 'nestedObject.deep.deeper': 123 }, + }); + expectType>({ + // @ts-expect-error Type mismatch + $unset: { 'nestedObject.deep.other': 123 }, + }); + + // @ts-expect-error Type mismatch + expectType>({ $unset: { 'tags.0': 123 } }); + + // @ts-expect-error Type mismatch + expectType>({ $unset: { 'list.0.direct': 123 } }); + // @ts-expect-error Type mismatch + expectType>({ $unset: { 'list.1.other': 123 } }); + }); + }); + }); + + test('invalid keys', () => { + // @ts-expect-error Invalid key + expectType>({ $unset: { inexistent: 'foo' } }); + + expectType>({ + // @ts-expect-error Invalid key + $unset: { 'nestedObject.inexistent': 'foo' }, + }); + expectType>({ + // @ts-expect-error Invalid key + $unset: { 'nestedObject.deep.inexistent': 'foo' }, + }); + expectType>({ + // @ts-expect-error Invalid key + $unset: { 'nestedObject.inexistent.deeper': 'foo' }, + }); + + // @ts-expect-error Invalid key + expectType>({ $unset: { 'list.0.inexistent': 'foo' } }); + // @ts-expect-error Invalid key + expectType>({ $unset: { 'inexistent.0.other': 'foo' } }); + }); + + describe('array filters', () => { + test('valid types', () => { + expectType>({ $unset: { 'tags.$': 1 } }); + expectType>({ $unset: { 'tags.$[bla]': 1 } }); + expectType>({ $unset: { 'tags.$[]': 1 } }); + + // These are not enforced in the `OnlyFieldsOfType` type + // expectType>({ $unset: { 'list.$.direct': 1 } }); + // expectType>({ $unset: { 'list.$[bla].direct': 1 } }); + // expectType>({ $unset: { 'list.$[].direct': 1 } }); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch + expectType>({ $unset: { 'tags.$': 123 } }); + // @ts-expect-error Type mismatch + expectType>({ $unset: { 'tags.$[bla]': 123 } }); + // @ts-expect-error Type mismatch + expectType>({ $unset: { 'tags.$[]': 123 } }); + + // These are not yet supported in the `OnlyFieldsOfType` type + // expectType>({ $set: { 'list.$.direct': 123 } }); + // expectType>({ $set: { 'list.$[bla].direct': 123 } }); + // expectType>({ $set: { 'list.$[].direct': 123 } }); + }); + }); + }); + }); +}); diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index 760e231a9..df94e30ba 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -1,14 +1,15 @@ import { describe, expect, test } from '@jest/globals'; import { ObjectId } from 'mongodb'; import { expectType } from 'ts-expect'; -import { ProjectionType, getIds } from '../utils'; +import { NestedPaths, ProjectionType, getIds, PropertyType } from '../utils'; describe('utils', () => { - interface Schema { + interface TestDocument { _id: ObjectId; foo: string; bar: number; ham?: Date; + tag?: string; nestedList: { direct: string; other?: number; @@ -23,185 +24,305 @@ describe('utils', () => { }; } - test('ProjectionType, required fields', () => { - const foo = { foo: 1 }; + describe('NestedPaths', () => { + test('valid types', () => { + expectType>(['_id']); + expectType>(['foo']); + expectType>(['bar']); + expectType>(['ham']); + expectType>(['tag']); - const testFoo: ProjectionType = { - _id: new ObjectId(), - foo: 'foo', - }; + // arrays + expectType>(['nestedList']); + expectType>(['nestedList', 'direct']); + expectType>(['nestedList', 'other']); + expectType>(['nestedList', 0]); + expectType>(['nestedList', 0, 'direct']); + expectType>(['nestedList', 1, 'other']); - expectType<{ - _id: ObjectId; - foo: string; - }>(testFoo); - expectType(testFoo.foo); - // @ts-expect-error `bar` should be undefined here - testFoo.bar; - // @ts-expect-error `ham` should be undefined here - testFoo.ham; - - const bar = { bar: 1 }; - - const testBar: ProjectionType = { - _id: new ObjectId(), - bar: 123, - }; + // objects + expectType>(['nestedObject']); + expectType>(['nestedObject', 'direct']); + expectType>(['nestedObject', 'other']); + expectType>(['nestedObject', 'deep']); + expectType>(['nestedObject', 'deep', 'deeper']); + expectType>(['nestedObject', 'deep', 'other']); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch + expectType>(['inexistent']); - expectType<{ - _id: ObjectId; - bar: number; - }>(testBar); - // @ts-expect-error `foo` should be undefined here - testBar.foo; - expectType(testBar.bar); - // @ts-expect-error `ham` should be undefined here - testBar.ham; + // @ts-expect-error Type mismatch + expectType>(['nestedList', 'inexistent']); + // @ts-expect-error Type mismatch + expectType>(['nestedList', 0, 'inexistent']); + + // @ts-expect-error Type mismatch + expectType>(['nestedObject', 'inexistent']); + // @ts-expect-error Type mismatch + expectType>(['nestedObject', 'deep', 'inexistent']); + }); }); - test('ProjectionType, multiple mixed fields', () => { - const multiple = { - bar: 1, - ham: 1, - }; + describe('PropertyType', () => { + test('valid types', () => { + expectType>('any string'); + expectType>(123); + expectType>(new Date()); - const testMultiple: ProjectionType = { - _id: new ObjectId(), - bar: 123, - ham: new Date(), - }; + // arrays + expectType>([{ direct: 'foo' }]); + expectType>({ direct: 'foo', other: 123 }); + expectType>('foo'); + expectType>(123); + expectType>(undefined); + expectType>('foo'); + expectType>(123); + expectType>(undefined); - expectType<{ - _id: ObjectId; - bar: number; - ham?: Date; - }>(testMultiple); - // @ts-expect-error `foo` should be undefined here - testMultiple.foo; - expectType(testMultiple.bar); - expectType(testMultiple.ham); + // object + expectType>({ + deep: { deeper: 'foo' }, + direct: true, + }); + expectType>({ deeper: 'foo', other: 123 }); + expectType>('foo'); + expectType>(123); + expectType>(undefined); + expectType>(true); + }); + + test('invalid types', () => { + // @ts-expect-error Type mismatch + expectType>(123); + // @ts-expect-error Type mismatch + expectType>('foo'); + // @ts-expect-error Type mismatch + expectType>(123); + + // arrays + // @ts-expect-error Type mismatch + expectType>([{ direct: 123 }]); + // @ts-expect-error Type mismatch + expectType>({ direct: 123, other: 123 }); + // @ts-expect-error Type mismatch + expectType>({ direct: 'foo', other: 'foo' }); + // @ts-expect-error Type mismatch + expectType>(123); + // @ts-expect-error Type mismatch + expectType>(undefined); + // @ts-expect-error Type mismatch + expectType>('foo'); + // @ts-expect-error Type mismatch + expectType>(123); + // @ts-expect-error Type mismatch + expectType>(undefined); + // @ts-expect-error Type mismatch + expectType>('foo'); + + // object + expectType>({ + // @ts-expect-error Type mismatch + deep: { deeper: 123 }, + direct: true, + }); + // @ts-expect-error Type mismatch + expectType>({ deeper: 123, other: 123 }); + // @ts-expect-error Type mismatch + expectType>({ deeper: 'foo', other: 'foo' }); + // @ts-expect-error Type mismatch + expectType>(123); + // @ts-expect-error Type mismatch + expectType>(undefined); + // @ts-expect-error Type mismatch + expectType>('foo'); + // @ts-expect-error Type mismatch + expectType>('foo'); + }); }); - test('ProjectionType, nested fields', () => { - const nested = { - foo: 1, - 'nestedList.0.direct': 1, - 'nestedObject.deep.deeper': 1, - 'nestedObject.direct': 1, - }; + describe('ProjectionType', () => { + test('required fields', () => { + const foo = { foo: 1 }; - const testNested: ProjectionType = { - _id: new ObjectId(), - foo: 'foo', - nestedList: [ - { - direct: 'in list', - }, - ], - nestedObject: { - deep: { - deeper: 'in object', + const testFoo: ProjectionType = { + _id: new ObjectId(), + foo: 'foo', + }; + + expectType<{ + _id: ObjectId; + foo: string; + }>(testFoo); + expectType(testFoo.foo); + // @ts-expect-error `bar` should be undefined here + testFoo.bar; + // @ts-expect-error `ham` should be undefined here + testFoo.ham; + + const bar = { bar: 1 }; + + const testBar: ProjectionType = { + _id: new ObjectId(), + bar: 123, + }; + + expectType<{ + _id: ObjectId; + bar: number; + }>(testBar); + // @ts-expect-error `foo` should be undefined here + testBar.foo; + expectType(testBar.bar); + // @ts-expect-error `ham` should be undefined here + testBar.ham; + }); + + test('multiple mixed fields', () => { + const multiple = { + bar: 1, + ham: 1, + }; + + const testMultiple: ProjectionType = { + _id: new ObjectId(), + bar: 123, + ham: new Date(), + }; + + expectType<{ + _id: ObjectId; + bar: number; + ham?: Date; + }>(testMultiple); + // @ts-expect-error `foo` should be undefined here + testMultiple.foo; + expectType(testMultiple.bar); + expectType(testMultiple.ham); + }); + + test('nested fields', () => { + const nested = { + foo: 1, + 'nestedList.0.direct': 1, + 'nestedObject.deep.deeper': 1, + 'nestedObject.direct': 1, + }; + + const testNested: ProjectionType = { + _id: new ObjectId(), + foo: 'foo', + nestedList: [ + { + direct: 'in list', + }, + ], + nestedObject: { + deep: { + deeper: 'in object', + }, + direct: true, }, - direct: true, - }, - }; + }; - expectType<{ - _id: ObjectId; - foo: string; - nestedList: { - direct: string; - }[]; - nestedObject?: { - deep?: { - deeper: string; + expectType<{ + _id: ObjectId; + foo: string; + nestedList: { + direct: string; + }[]; + nestedObject?: { + deep?: { + deeper: string; + }; + direct: boolean; }; - direct: boolean; - }; - }>(testNested); - expectType(testNested.foo); - // @ts-expect-error `bar` should be undefined here - testNested.bar; - // @ts-expect-error `ham` should be undefined here - testNested.ham; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expectType(testNested.nestedList); - expectType(testNested.nestedList[0].direct); - // @ts-expect-error `nestedList[0].other` should be undefined here - testNested.nestedList[0].other; - expectType(testNested.nestedObject); - expectType(testNested.nestedObject.deep); - expectType(testNested.nestedObject.deep.deeper); - // @ts-expect-error `nestedObject.deep.other` should be undefined here - testNested.nestedObject.deep.other; - // @ts-expect-error `nestedObject.other` should be undefined here - testNested.nestedObject.other; - }); + }>(testNested); + expectType(testNested.foo); + // @ts-expect-error `bar` should be undefined here + testNested.bar; + // @ts-expect-error `ham` should be undefined here + testNested.ham; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expectType(testNested.nestedList); + expectType(testNested.nestedList[0].direct); + // @ts-expect-error `nestedList[0].other` should be undefined here + testNested.nestedList[0].other; + expectType(testNested.nestedObject); + expectType(testNested.nestedObject.deep); + expectType(testNested.nestedObject.deep.deeper); + // @ts-expect-error `nestedObject.deep.other` should be undefined here + testNested.nestedObject.deep.other; + // @ts-expect-error `nestedObject.other` should be undefined here + testNested.nestedObject.other; + }); - test('ProjectionType, excluding _id', () => { - const excluding = { - _id: 0, - bar: 1, - ham: 1, - } as const; + test('excluding _id', () => { + const excluding = { + _id: 0, + bar: 1, + ham: 1, + } as const; - const testExcluding: ProjectionType = { - bar: 123, - ham: new Date(), - }; + const testExcluding: ProjectionType = { + bar: 123, + ham: new Date(), + }; - expectType<{ - ham?: Date; - }>(testExcluding); - // @ts-expect-error `_id` should be undefined here - testExcluding._id; - expectType(testExcluding.ham); - }); + expectType<{ + ham?: Date; + }>(testExcluding); + // @ts-expect-error `_id` should be undefined here + testExcluding._id; + expectType(testExcluding.ham); + }); + + test('ProjectionType, full schema except foo', () => { + const excludingFoo = { + foo: 0, + } as const; - test('ProjectionType, full schema except foo', () => { - const excludingFoo = { - foo: 0, - } as const; - - const testExceptFoo: ProjectionType = { - _id: new ObjectId(), - bar: 123, - ham: new Date(), - nestedList: [], - nestedObject: { - deep: { - deeper: 'hi', + const testExceptFoo: ProjectionType = { + _id: new ObjectId(), + bar: 123, + ham: new Date(), + nestedList: [], + nestedObject: { + deep: { + deeper: 'hi', + }, + direct: true, }, - direct: true, - }, - }; + }; - expectType>(testExceptFoo); - // @ts-expect-error `foo` should be undefined here - testExceptFoo.foo; - expectType(testExceptFoo.bar); - expectType(testExceptFoo.ham); - }); + expectType>(testExceptFoo); + // @ts-expect-error `foo` should be undefined here + testExceptFoo.foo; + expectType(testExceptFoo.bar); + expectType(testExceptFoo.ham); + }); - test('ProjectionType, full schema', () => { - const testFull: ProjectionType = { - _id: new ObjectId(), - bar: 123, - foo: 'foo', - ham: new Date(), - nestedList: [], - nestedObject: { - deep: { - deeper: 'hi', + test('full schema', () => { + const testFull: ProjectionType = { + _id: new ObjectId(), + bar: 123, + foo: 'foo', + ham: new Date(), + nestedList: [], + nestedObject: { + deep: { + deeper: 'hi', + }, + direct: true, }, - direct: true, - }, - }; + }; - expectType(testFull); - expectType(testFull.foo); - expectType(testFull.bar); - expectType(testFull.ham); + expectType(testFull); + expectType(testFull.foo); + expectType(testFull.bar); + expectType(testFull.ham); + }); }); test.each([ diff --git a/src/index.ts b/src/index.ts index 393e062ae..c7fc68e50 100644 --- a/src/index.ts +++ b/src/index.ts @@ -178,5 +178,6 @@ export default class Papr { export { schema, types, types as Types }; export * from './hooks'; export * from './model'; +export * from './mongodbTypes'; export * from './schema'; export * from './utils'; diff --git a/src/model.ts b/src/model.ts index 3d8c0bdfe..7980b0425 100644 --- a/src/model.ts +++ b/src/model.ts @@ -16,19 +16,17 @@ import type { Flatten, InsertOneOptions, OptionalUnlessRequiredId, - StrictFilter, StrictMatchKeysAndValues, - StrictUpdateFilter, UpdateFilter, UpdateOptions, UpdateResult, WithId, } from 'mongodb'; import { serializeArguments } from './hooks'; +import type { PaprBulkWriteOperation, PaprFilter, PaprUpdateFilter } from './mongodbTypes'; import { SchemaOptions, SchemaTimestampOptions } from './schema'; import { BaseSchema, - BulkWriteOperation, cleanSetOnInsert, DocumentForInsert, getTimestampProperty, @@ -57,32 +55,29 @@ export interface Model Promise; bulkWrite: ( - operations: BulkWriteOperation[], + operations: PaprBulkWriteOperation[], options?: BulkWriteOptions ) => Promise; - countDocuments: ( - filter: StrictFilter, - options?: CountDocumentsOptions - ) => Promise; + countDocuments: (filter: PaprFilter, options?: CountDocumentsOptions) => Promise; - deleteMany: (filter: StrictFilter, options?: DeleteOptions) => Promise; + deleteMany: (filter: PaprFilter, options?: DeleteOptions) => Promise; - deleteOne: (filter: StrictFilter, options?: DeleteOptions) => Promise; + deleteOne: (filter: PaprFilter, options?: DeleteOptions) => Promise; distinct: >( key: TKey, - filter: StrictFilter, + filter: PaprFilter, options?: DistinctOptions ) => Promise[TKey]>[]>; exists: ( - filter: StrictFilter, + filter: PaprFilter, options?: Omit, 'limit' | 'projection' | 'skip' | 'sort'> ) => Promise; find: | undefined>( - filter: StrictFilter, + filter: PaprFilter, options?: Omit, 'projection'> & { projection?: TProjection } ) => Promise[]>; @@ -92,18 +87,18 @@ export interface Model Promise | null>; findOne: | undefined>( - filter: StrictFilter, + filter: PaprFilter, options?: Omit, 'projection'> & { projection?: TProjection } ) => Promise | null>; findOneAndDelete: | undefined>( - filter: StrictFilter, + filter: PaprFilter, options?: Omit & { projection?: TProjection } ) => Promise | null>; findOneAndUpdate: | undefined>( - filter: StrictFilter, - update: StrictUpdateFilter, + filter: PaprFilter, + update: PaprUpdateFilter, options?: Omit & { projection?: TProjection } ) => Promise | null>; @@ -118,20 +113,20 @@ export interface Model Promise; updateOne: ( - filter: StrictFilter, - update: StrictUpdateFilter, + filter: PaprFilter, + update: PaprUpdateFilter, options?: Omit ) => Promise; updateMany: ( - filter: StrictFilter, - update: StrictUpdateFilter, + filter: PaprFilter, + update: PaprUpdateFilter, options?: UpdateOptions ) => Promise; upsert: ( - filter: StrictFilter, - update: StrictUpdateFilter + filter: PaprFilter, + update: PaprUpdateFilter ) => Promise>; } @@ -369,7 +364,7 @@ export function build[], + operations: PaprBulkWriteOperation[], options?: BulkWriteOptions ): Promise { const finalOperations = operations.map((op) => { @@ -395,7 +390,6 @@ export function build} + * @param filter {PaprFilter} * @param [options] {CountDocumentsOptions} * * @returns {Promise} @@ -439,7 +433,7 @@ export function build, + filter: PaprFilter, options?: CountDocumentsOptions ): Promise { return model.collection.countDocuments(filter as Filter, { @@ -453,7 +447,7 @@ export function build} + * @param filter {PaprFilter} * @param [options] {DeleteOptions} * * @returns {Promise} https://mongodb.github.io/node-mongodb-native/5.0/interfaces/DeleteResult.html @@ -464,7 +458,7 @@ export function build, + filter: PaprFilter, options?: DeleteOptions ): Promise { return model.collection.deleteMany( @@ -481,7 +475,7 @@ export function build} + * @param filter {PaprFilter} * @param [options] {DeleteOptions} * * @returns {Promise} https://mongodb.github.io/node-mongodb-native/5.0/interfaces/DeleteResult.html @@ -492,7 +486,7 @@ export function build, + filter: PaprFilter, options?: DeleteOptions ): Promise { return model.collection.deleteOne( @@ -510,7 +504,7 @@ export function build} + * @param [filter] {PaprFilter} * @param [options] {DistinctOptions} * * @returns {Promise>} `TValue` is the type of the `key` field in the schema @@ -524,7 +518,7 @@ export function build>( key: TKey, - filter?: StrictFilter, + filter?: PaprFilter, options?: DistinctOptions ): Promise[TKey]>[]> { return model.collection.distinct( @@ -542,7 +536,7 @@ export function build} + * @param filter {PaprFilter} * @param [options] {Omit, "projection" | "limit" | "sort" | "skip">} * * @returns {Promise} @@ -555,7 +549,7 @@ export function build, + filter: PaprFilter, options?: Omit, 'limit' | 'projection' | 'skip' | 'sort'> ): Promise { // If there are any entries in the filter, we project out the value from @@ -587,7 +581,7 @@ export function build} + * @param filter {PaprFilter} * @param [options] {FindOptions} * * @returns {Promise>} @@ -609,7 +603,7 @@ export function build | undefined>( - filter: StrictFilter, + filter: PaprFilter, options?: Omit, 'projection'> & { projection?: TProjection } ): Promise[]> { return model.collection @@ -674,7 +668,7 @@ export function build} + * @param filter {PaprFilter} * @param [options] {FindOptions} * * @returns {Promise} @@ -695,7 +689,7 @@ export function build | undefined>( - filter: StrictFilter, + filter: PaprFilter, options?: Omit, 'projection'> & { projection?: TProjection } ): Promise | null> { return model.collection.findOne( @@ -711,7 +705,7 @@ export function build} + * @param filter {PaprFilter} * @param [options] {FindOneAndUpdateOptions} * * @returns {Promise} @@ -721,7 +715,7 @@ export function build | undefined>( - filter: StrictFilter, + filter: PaprFilter, options?: Omit & { projection?: TProjection } ): Promise | null> { const result = await model.collection.findOneAndDelete( @@ -745,8 +739,8 @@ export function build} - * @param update {StrictUpdateFilter} + * @param filter {PaprFilter} + * @param update {PaprUpdateFilter} * @param [options] {FindOneAndUpdateOptions} * * @returns {Promise} @@ -769,8 +763,8 @@ export function build | undefined>( - filter: StrictFilter, - update: StrictUpdateFilter, + filter: PaprFilter, + update: PaprUpdateFilter, options?: Omit & { projection?: TProjection } ): Promise | null> { const finalUpdate = model.timestamps ? timestampUpdateFilter(update, model.timestamps) : update; @@ -913,8 +907,8 @@ export function build} - * @param update {StrictUpdateFilter} + * @param filter {PaprFilter} + * @param update {PaprUpdateFilter} * @param [options] {UpdateOptions} * * @returns {Promise} https://mongodb.github.io/node-mongodb-native/5.0/interfaces/UpdateResult.html @@ -928,8 +922,8 @@ export function build, - update: StrictUpdateFilter, + filter: PaprFilter, + update: PaprUpdateFilter, options?: UpdateOptions ): Promise { const finalUpdate = model.timestamps @@ -951,8 +945,8 @@ export function build} - * @param update {StrictUpdateFilter} + * @param filter {PaprFilter} + * @param update {PaprUpdateFilter} * @param [options] {UpdateOptions} * * @returns {Promise} https://mongodb.github.io/node-mongodb-native/5.0/interfaces/UpdateResult.html @@ -966,8 +960,8 @@ export function build, - update: StrictUpdateFilter, + filter: PaprFilter, + update: PaprUpdateFilter, options?: Omit ): Promise { const finalUpdate = model.timestamps @@ -991,8 +985,8 @@ export function build} - * @param update {StrictUpdateFilter} + * @param filter {PaprFilter} + * @param update {PaprUpdateFilter} * * @returns {Promise} * @@ -1003,8 +997,8 @@ export function build, - update: StrictUpdateFilter + filter: PaprFilter, + update: PaprUpdateFilter ): Promise> { const item = await model.findOneAndUpdate(filter, update, { upsert: true, diff --git a/src/mongodbTypes.ts b/src/mongodbTypes.ts new file mode 100644 index 000000000..ccbe21586 --- /dev/null +++ b/src/mongodbTypes.ts @@ -0,0 +1,191 @@ +/* eslint-disable no-use-before-define */ + +import type { + AlternativeType, + ArrayElement, + BitwiseFilter, + BSONRegExp, + BSONType, + BSONTypeAlias, + DeleteManyModel, + DeleteOneModel, + Document, + IntegerType, + Join, + NumericType, + OnlyFieldsOfType, + PullAllOperator, + PullOperator, + PushOperator, + ReplaceOneModel, + SetFields, + Timestamp, + UpdateManyModel, + UpdateOneModel, + WithId, +} from 'mongodb'; +import { SchemaOptions } from './schema'; +import { DocumentForInsert, NestedPaths, NestedPathsOfType, PropertyType } from './utils'; + +// Some of the types are adapted from originals at: https://github.com/mongodb/node-mongodb-native/blob/v5.0.1/src/mongo_types.ts +// licensed under Apache License 2.0: https://github.com/mongodb/node-mongodb-native/blob/v5.0.1/LICENSE.md + +// The strict MongoDB types (`StrictFilter` and `StrictUpdateFilter` and their dependencies) +// are too permissive due to merging their type definitions with `Document`, +// which is just an alias for `Record`. +// +// This type merge is preventing important type checks that can be done on the filter queries: +// e.g. checking for undefined attributes in the schema being used inside a query +// +// We've adopted these types in this repository and made some improvements to them. +// See: https://github.com/plexinc/papr/issues/410 + +// These buik operation types need our own `PaprFilter` and `PaprUpdateFilter` in their definition +export type PaprBulkWriteOperation> = + | { + // @ts-expect-error Type expects a Document extended type, but Document is too generic + deleteMany: Omit, 'filter'> & { filter: PaprFilter }; + } + | { + // @ts-expect-error Type expects a Document extended type, but Document is too generic + deleteOne: Omit, 'filter'> & { filter: PaprFilter }; + } + | { + // @ts-expect-error Type expects a Document extended type, but Document is too generic + replaceOne: Omit, 'filter'> & { filter: PaprFilter }; + } + | { + // @ts-expect-error Type expects a Document extended type, but Document is too generic + updateMany: Omit, 'filter' | 'update'> & { + filter: PaprFilter; + update: PaprUpdateFilter; + }; + } + | { + // @ts-expect-error Type expects a Document extended type, but Document is too generic + updateOne: Omit, 'filter' | 'update'> & { + filter: PaprFilter; + update: PaprUpdateFilter; + }; + } + | { + insertOne: { + document: DocumentForInsert; + }; + }; + +export type PaprFilter = + | Partial> + | (PaprFilterConditions> & PaprRootFilterOperators>); + +export type PaprFilterConditions = { + [Property in Join, '.'>]?: PaprCondition< + PropertyType + >; +}; + +export interface PaprRootFilterOperators { + $and?: PaprFilter[]; + $nor?: PaprFilter[]; + $or?: PaprFilter[]; + $expr?: Record; + $text?: { + $search: string; + $language?: string; + $caseSensitive?: boolean; + $diacriticSensitive?: boolean; + }; + $where?: string | ((this: TSchema) => boolean); + $comment?: Document | string; +} + +export type PaprCondition = + | AlternativeType + | PaprFilterOperators>; + +export interface PaprFilterOperators { + $eq?: TValue; + $gt?: TValue; + $gte?: TValue; + $in?: readonly TValue[]; + $lt?: TValue; + $lte?: TValue; + $ne?: TValue; + $nin?: readonly TValue[]; + $not?: TValue extends string ? PaprFilterOperators | RegExp : PaprFilterOperators; + /** + * When `true`, `$exists` matches the documents that contain the field, + * including documents where the field value is null. + */ + $exists?: boolean; + $type?: BSONType | BSONTypeAlias; + $expr?: Record; + $jsonSchema?: Record; + $mod?: TValue extends number ? [number, number] : never; + $regex?: TValue extends string ? BSONRegExp | RegExp | string : never; + $options?: TValue extends string ? string : never; + $geoIntersects?: { + $geometry: Document; + }; + $geoWithin?: Document; + $near?: Document; + $nearSphere?: Document; + $maxDistance?: number; + $all?: TValue extends readonly any[] ? readonly any[] : never; + $elemMatch?: TValue extends readonly any[] ? Document : never; + $size?: TValue extends readonly any[] ? number : never; + $bitsAllClear?: BitwiseFilter; + $bitsAllSet?: BitwiseFilter; + $bitsAnyClear?: BitwiseFilter; + $bitsAnySet?: BitwiseFilter; + $rand?: Record; +} + +export type PaprMatchKeysAndValues = { + [Property in `${NestedPathsOfType}.$${'' | `[${string}]`}`]?: ArrayElement< + PropertyType + >; +} & { + [Property in `${NestedPathsOfType[]>}.$${ + | '' + | `[${string}]`}.${string}`]?: any; +} & { + [Property in Join, '.'>]?: PropertyType; +}; + +export interface PaprUpdateFilter { + $currentDate?: OnlyFieldsOfType< + TSchema, + Date | Timestamp, + | true + | { + $type: 'date' | 'timestamp'; + } + >; + $inc?: OnlyFieldsOfType; + $min?: PaprMatchKeysAndValues; + $max?: PaprMatchKeysAndValues; + $mul?: OnlyFieldsOfType; + $rename?: Record; + $set?: PaprMatchKeysAndValues; + $setOnInsert?: PaprMatchKeysAndValues; + $unset?: OnlyFieldsOfType; + $addToSet?: SetFields; + $pop?: OnlyFieldsOfType; + $pull?: PullOperator; + $push?: PushOperator; + $pullAll?: PullAllOperator; + $bit?: OnlyFieldsOfType< + TSchema, + NumericType | undefined, + | { + and: IntegerType; + } + | { + or: IntegerType; + } + | { + xor: IntegerType; + } + >; +} diff --git a/src/utils.ts b/src/utils.ts index 4e9271731..3fc8bc351 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,19 +1,14 @@ +/* eslint-disable no-use-before-define */ + import { ObjectId } from 'mongodb'; -import type { - DeleteManyModel, - DeleteOneModel, - Join, - NestedPaths, - OptionalId, - ReplaceOneModel, - StrictUpdateFilter, - UpdateManyModel, - UpdateOneModel, - WithId, -} from 'mongodb'; -import { DeepPick } from './DeepPick'; -import { Hooks } from './hooks'; -import { SchemaOptions, SchemaTimestampOptions } from './schema'; +import type { Join, KeysOfAType, OptionalId, WithId } from 'mongodb'; +import type { DeepPick } from './DeepPick'; +import type { Hooks } from './hooks'; +import type { PaprBulkWriteOperation, PaprUpdateFilter } from './mongodbTypes'; +import type { SchemaOptions, SchemaTimestampOptions } from './schema'; + +// Some of the types are adapted from originals at: https://github.com/mongodb/node-mongodb-native/blob/v5.0.1/src/mongo_types.ts +// licensed under Apache License 2.0: https://github.com/mongodb/node-mongodb-native/blob/v5.0.1/LICENSE.md export enum VALIDATION_ACTIONS { ERROR = 'error', @@ -72,38 +67,70 @@ export type DocumentForInsert< Partial> : DocumentForInsertWithoutDefaults; -export type BulkWriteOperation> = - | { - // @ts-expect-error Type expects a Document extended type, but Document is too generic - deleteMany: DeleteManyModel; - } - | { - // @ts-expect-error Type expects a Document extended type, but Document is too generic - deleteOne: DeleteOneModel; - } - | { - // @ts-expect-error Type expects a Document extended type, but Document is too generic - replaceOne: ReplaceOneModel; - } - | { - // @ts-expect-error Type expects a Document extended type, but Document is too generic - updateMany: UpdateManyModel; - } - | { - // @ts-expect-error Type expects a Document extended type, but Document is too generic - updateOne: UpdateOneModel; - } - | { - insertOne: { - document: DocumentForInsert; - }; - }; +export type Identity = Type; + +export type Flatten = Identity<{ + [Key in keyof Type]: Type[Key]; +}>; -type FilterKeys = { - [TKey in keyof TObject]: TObject[TKey] extends ValueType ? TKey : never; -}[keyof TObject]; +/** + * Returns tuple of strings (keys to be joined on '.') that represent every path into a schema + * + * https://docs.mongodb.com/manual/tutorial/query-embedded-documents/ + * + * @remarks + * Through testing we determined that a depth of 7 is safe for the typescript compiler + * and provides reasonable compilation times. This number is otherwise not special and + * should be changed if issues are found with this level of checking. Beyond this + * depth any helpers that make use of NestedPaths should devolve to not asserting any + * type safety on the input. + */ +export type NestedPaths = Depth['length'] extends 7 + ? [] + : Type extends + | Buffer + | Date + | RegExp + | Uint8Array + | boolean + | number + | string + | ((...args: any[]) => any) + | { + _bsontype: string; + } + ? [] + : Type extends readonly (infer ArrayType)[] + ? // This returns the non-indexed dot-notation path: e.g. `foo.bar` + | [...NestedPaths] + // This returns the array parent itself: e.g. `foo` + | [] + // This returns the indexed dot-notation path: e.g. `foo.0.bar` + | [number, ...NestedPaths] + // This returns the indexed element path: e.g. `foo.0` + | [number] + : Type extends Map + ? [string] + : Type extends object + ? { + [Key in Extract]: Type[Key] extends readonly any[] + ? [Key, ...NestedPaths] // child is not structured the same as the parent + : [Key, ...NestedPaths] | [Key]; + }[Extract] + : []; + +/** + * Returns keys (strings) for every path into a schema with a value of type + * https://docs.mongodb.com/manual/tutorial/query-embedded-documents/ + */ +export type NestedPathsOfType = KeysOfAType< + { + [Property in Join, '.'>]: PropertyType; + }, + Type +>; -type FilterProperties = Pick>; +type FilterProperties = Pick>; export type ProjectionType< TSchema extends BaseSchema, @@ -125,11 +152,37 @@ export type Projection = Partial< Record, []>, '.'>, number | 0 | 1> >; -export type Identity = Type; - -export type Flatten = Identity<{ - [Key in keyof Type]: Type[Key]; -}>; +export type PropertyNestedType< + Type, + Property extends string +> = Property extends `${infer Key}.${infer Rest}` + ? Key extends `${number}` + ? // indexed array nested properties + NonNullable extends readonly (infer ArrayType)[] + ? PropertyType + : unknown + : // object nested properties & non-indexed array nested properties + Key extends keyof Type + ? Type[Key] extends Map + ? MapType + : PropertyType, Rest> + : unknown + : unknown; + +export type PropertyType = string extends Property + ? unknown + : // object properties + Property extends keyof Type + ? Type[Property] + : NonNullable extends readonly (infer ArrayType)[] + ? // indexed array properties + Property extends `${number}` + ? ArrayType + : // non-indexed array properties + Property extends keyof ArrayType + ? PropertyType + : PropertyNestedType, Property> + : PropertyNestedType, Property>; export type RequireAtLeastOne = { [Key in Keys]-?: Partial>> & Required>; @@ -236,13 +289,14 @@ export function getTimestampProperty< // Creates new update object so the original doesn't get mutated export function timestampUpdateFilter>( - update: StrictUpdateFilter, + update: PaprUpdateFilter, timestamps: TOptions['timestamps'] -): StrictUpdateFilter { +): PaprUpdateFilter { const updatedAtProperty = getTimestampProperty('updatedAt', timestamps); const $currentDate = { ...update.$currentDate, + // @ts-expect-error Ignore dynamic string property access ...(!update.$set?.[updatedAtProperty] && !update.$unset?.[updatedAtProperty] && { [updatedAtProperty]: true, @@ -258,9 +312,9 @@ export function timestampUpdateFilter>( - operation: BulkWriteOperation, + operation: PaprBulkWriteOperation, timestamps: TOptions['timestamps'] -): BulkWriteOperation { +): PaprBulkWriteOperation { const createdAtProperty = getTimestampProperty('createdAt', timestamps); const updatedAtProperty = getTimestampProperty('updatedAt', timestamps); @@ -286,6 +340,7 @@ export function timestampBulkWriteOperation['$setOnInsert']>` +// triggers a stack overflow error in `tsc`, so we choose a simple `Record` type here. export function cleanSetOnInsert( - $setOnInsert: NonNullable['$setOnInsert']>, - update: StrictUpdateFilter -): NonNullable['$setOnInsert']> { + $setOnInsert: Record, + update: PaprUpdateFilter +): NonNullable['$setOnInsert']> { for (const key of Object.keys($setOnInsert)) { if ( key in (update.$set || {}) || @@ -377,8 +437,9 @@ export function cleanSetOnInsert( key in (update.$unset || {}) ) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete $setOnInsert[key as keyof typeof $setOnInsert]; + delete $setOnInsert[key]; } } - return $setOnInsert; + + return $setOnInsert as NonNullable['$setOnInsert']>; } diff --git a/yarn.lock b/yarn.lock index e9678f919..c4d38fb83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9329,7 +9329,7 @@ __metadata: standard-version: 9.5.0 ts-expect: 1.3.0 ts-node: 10.9.1 - typescript: 4.9.3 + typescript: 4.9.5 peerDependencies: mongodb: ^5.0.0 languageName: unknown @@ -11252,7 +11252,17 @@ __metadata: languageName: node linkType: hard -"typescript@npm:4.9.3, typescript@npm:^4.6.4": +"typescript@npm:4.9.5": + version: 4.9.5 + resolution: "typescript@npm:4.9.5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db + languageName: node + linkType: hard + +"typescript@npm:^4.6.4": version: 4.9.3 resolution: "typescript@npm:4.9.3" bin: @@ -11262,7 +11272,17 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@4.9.3#~builtin, typescript@patch:typescript@^4.6.4#~builtin": +"typescript@patch:typescript@4.9.5#~builtin": + version: 4.9.5 + resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=ad5954" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 8f6260acc86b56bfdda6004bc53f32ea548f543e8baef7071c8e34d29d292f3e375c8416556c8de10b24deef6933cd1c16a8233dc84a3dd43a13a13265d0faab + languageName: node + linkType: hard + +"typescript@patch:typescript@^4.6.4#~builtin": version: 4.9.3 resolution: "typescript@patch:typescript@npm%3A4.9.3#~builtin::version=4.9.3&hash=d73830" bin: