diff --git a/src/__tests__/model.test.ts b/src/__tests__/model.test.ts index f5146a8e4..7d9f7f397 100644 --- a/src/__tests__/model.test.ts +++ b/src/__tests__/model.test.ts @@ -20,6 +20,7 @@ describe('model', () => { bar: Types.number({ required: true }), foo: Types.string({ required: true }), ham: Types.date(), + list: Types.array(Types.number()), nested: Types.object({ direct: Types.string({ required: true }), other: Types.number(), @@ -2471,6 +2472,20 @@ describe('model', () => { ); }); + test('simple schema, readonly array', async () => { + const filter = { foo: 'foo' } as const; + const update = { $set: { list: [456] } } as const; + await simpleModel.updateOne(filter, update); + + expect(collection.updateOne).toHaveBeenCalledWith( + { foo: 'foo' }, + { + $set: { list: [456] }, + }, + { ignoreUndefined: true } + ); + }); + test.todo('simple schema, uses defaults'); test('timestamps schema', async () => { diff --git a/src/__tests__/mongodbTypes.test.ts b/src/__tests__/mongodbTypes.test.ts index ce1596463..a1e58d012 100644 --- a/src/__tests__/mongodbTypes.test.ts +++ b/src/__tests__/mongodbTypes.test.ts @@ -68,44 +68,119 @@ describe('mongodb types', () => { describe('PaprFilter', () => { describe('existing top-level keys', () => { - test('valid types', () => { - expect(true).toBeTruthy(); + describe('valid types', () => { + test('ObjectId', () => { + const filter = { _id: new ObjectId() } as const; + expectType>(filter); + }); - expectType>({ _id: new ObjectId() }); + test('string', () => { + const filter = { foo: 'foo' } as const; + expectType>(filter); + }); - expectType>({ foo: 'foo' }); - // string fields can be queried by regexp - expectType>({ foo: /foo/ }); + test('string queried by regexp', () => { + const filter = { foo: /foo/ } as const; + // string fields can be queried by regexp + expectType>(filter); + }); - expectType>({ bar: 123 }); + test('number', () => { + const filter = { bar: 123 } as const; + expectType>(filter); + }); - expectType>({ ham: new Date() }); + test('Date', () => { + const filter = { ham: new Date() } as const; + expectType>(filter); + }); - // 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/ }); + test('array-of-strings queried by exact match', () => { + // array fields can be queried by exact match + const filter = { tags: ['foo'] } as const; + expectType>(filter); + }); - expectType>({ - nestedObject: { - deep: { deeper: 'foo' }, - direct: true, - }, + test('array-of-strings queried by string', () => { + // array fields can be queried by element type + const filter = { tags: 'foo' } as const; + expectType>(filter); + }); + + test('array-of-strings queried by regexp', () => { + const filter = { tags: /foo/ } as const; + expectType>(filter); }); - // 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('nested object', () => { + const filter = { + nestedObject: { + deep: { deeper: 'foo' }, + direct: true, + }, + } as const; + expectType>(filter); + }); + + describe('BSON types', () => { + // all BSON types can be used as query values + test('binary', () => { + const filter = { binary: new Binary([], 2) } as const; + expectType>(filter); + }); + + test('BSONSymbol', () => { + const filter = { bsonSymbol: new BSONSymbol('hi') } as const; + expectType>(filter); + }); + + test('Code', () => { + const filter = { code: new Code(() => true) } as const; + expectType>(filter); + }); + + test('Double', () => { + const filter = { double: new Double(123.45) } as const; + expectType>(filter); + }); + + test('DBRef', () => { + const filter = { + dbRef: new DBRef('collection', new ObjectId()), + } as const; + expectType>(filter); + }); + + test('Decimal128', () => { + const filter = { decimal: new Decimal128('123.45') } as const; + expectType>(filter); + }); + + test('Int32', () => { + const filter = { int32: new Int32('123') } as const; + expectType>(filter); + }); + + test('Long', () => { + const filter = { long: new Long('123', 45) } as const; + expectType>(filter); + }); + + test('MaxKey', () => { + const filter = { maxKey: new MaxKey() } as const; + expectType>(filter); + }); + + test('MinKey', () => { + const filter = { minKey: new MinKey() } as const; + expectType>(filter); + }); + + test('regexp', () => { + const filter = { regexp: /foo/ } as const; + expectType>(filter); + }); + }); }); test('invalid types', () => { @@ -148,68 +223,143 @@ describe('mongodb types', () => { }); 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 }); - expectType>({ - 'nestedObject.level2.level3.level4.level5.level6ID': 'foo', + describe('valid types', () => { + describe('nested object', () => { + test('boolean addressed by name', () => { + const filter = { 'nestedObject.direct': true } as const; + // https://www.mongodb.com/docs/manual/tutorial/query-embedded-documents/#query-on-nested-field + expectType>(filter); + }); + + test('number addressed by name', () => { + expectType>({ 'nestedObject.other': 123 }); + }); + + test('string addressed by nested name', () => { + expectType>({ 'nestedObject.deep.deeper': 'foo' }); + }); + + test('number addressed by nested name', () => { + expectType>({ 'nestedObject.deep.other': 123 }); + }); + + test('string addressed by deeply-nested name', () => { + expectType>({ + 'nestedObject.level2.level3.level4.level5.level6ID': 'foo', + }); + }); + }); + + describe('generic object', () => { + test('number addressed by arbitrary nested name', () => { + expectType>({ 'genericObject.foo.id': 123 }); + expectType>({ 'genericObject.bar.id': 123 }); + }); + + test('number addressed by nested object with valid property', () => { + expectType>({ 'genericObject.foo': { id: 123 } }); + }); + }); + + test('string addressed by index + property in array-of-objects', () => { + // 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>({ 'genericObject.foo.id': 123 }); - expectType>({ 'genericObject.bar.id': 123 }); + test('number addressed by index + property in array-of-objects', () => { + expectType>({ 'list.1.other': 123 }); + }); - expectType>({ 'genericObject.foo': { id: 123 } }); + test('number addressed by large index + property in array-of-objects', () => { + // it works with some extreme indexes + expectType>({ 'list.4294967295.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 }); + test('number addressed by super-large index + property in array-of-objects', () => { + expectType>({ 'list.9999999999999999999.other': 123 }); + }); - expectType>({ 'tags.0': 'foo' }); + test('string addressed by property in array-of-objects', () => { + // 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' }); + }); - // 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('number addressed by property in array-of-objects', () => { + expectType>({ 'list.other': 123 }); + }); - test('invalid types', () => { - // @ts-expect-error Type mismatch - expectType>({ 'tags.0': 123 }); + test('string addressed by index in array-of-strings', () => { + expectType>({ 'tags.0': 'foo' }); + }); + }); - // @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' }); - expectType>({ + describe('invalid types', () => { + test('number substituted for string at array numeric index', () => { // @ts-expect-error Type mismatch - 'nestedObject.level2.level3.level4.level5.level6': 123, + expectType>({ 'tags.0': 123 }); }); - expectType>({ - // @ts-expect-error Nesting level too deep - 'nestedObject.level2.level3.level4.level5.level6.level7ID': 'foo', + + describe('nested objects', () => { + test('string substituted for object at nested object property', () => { + // @ts-expect-error Type mismatch + expectType>({ 'nestedObject.direct': 'foo' }); + }); + + test('string substituted for number at shallow nested object property', () => { + // @ts-expect-error Type mismatch + expectType>({ 'nestedObject.other': 'foo' }); + }); + + test('number substituted for string at nested object property', () => { + // @ts-expect-error Type mismatch + expectType>({ 'nestedObject.deep.deeper': 123 }); + }); + + test('string substituted for number at deep nested object property', () => { + // @ts-expect-error Type mismatch + expectType>({ 'nestedObject.deep.other': 'foo' }); + }); + + test('number substituted for object at deeply nested property (level6)', () => { + const filter = { + 'nestedObject.level2.level3.level4.level5.level6': 123, + } as const; + // @ts-expect-error Type mismatch + expectType>(filter); + }); + + test('number substituted for string at deeply nested property (level7) nesting error, not type error', () => { + const filter = { + 'nestedObject.level2.level3.level4.level5.level6.level7ID': 123, + } as const; + expectType>(filter); + }); }); - // @ts-expect-error Type mismatch - expectType>({ 'genericObject.foo.id': 'foo' }); - // @ts-expect-error Type mismatch - expectType>({ 'genericObject.bar.id': true }); + describe('generic objects', () => { + test('string substituted for number on generic object field', () => { + // @ts-expect-error Type mismatch + expectType>({ 'genericObject.bar.id': '123' }); + }); - // Support for this type-check is not available yet - // expectType>({ 'genericObject.foo': { id: 'foo' } }); + test('boolean substituted for number on generic object field', () => { + // @ts-expect-error Type mismatch + expectType>({ 'genericObject.bar.id': true }); + }); + }); - // @ts-expect-error Type mismatch - expectType>({ 'list.0.direct': 123 }); - // @ts-expect-error Type mismatch - expectType>({ 'list.1.other': 'foo' }); + test('number substituted for required string on array-of-objects', () => { + // Support for this type-check is not available yet + // expectType>({ 'genericObject.foo': { id: 'foo' } }); + + // @ts-expect-error Type mismatch + expectType>({ 'list.0.direct': 123 }); + }); + + test('string substituted for optional number on array-of-objects', () => { + // @ts-expect-error Type mismatch + expectType>({ 'list.1.other': 'foo' }); + }); }); }); @@ -429,11 +579,22 @@ describe('mongodb types', () => { describe('PaprUpdateFilter', () => { describe('$currentDate', () => { - test('valid types', () => { - expectType>({ $currentDate: { ham: true } }); - expectType>({ $currentDate: { ham: { $type: 'date' } } }); - expectType>({ - $currentDate: { ham: { $type: 'timestamp' } }, + describe('valid types', () => { + test('object', () => { + const filter = { $currentDate: { ham: true } } as const; + expectType>(filter); + }); + + test('object with $type date', () => { + const filter = { $currentDate: { ham: { $type: 'date' } } } as const; + expectType>(filter); + }); + + test('object with $type timestamp', () => { + const filter = { + $currentDate: { ham: { $type: 'timestamp' } }, + } as const; + expectType>(filter); }); }); diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index f28cbf00f..2e4fc0cc0 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -67,31 +67,98 @@ describe('utils', () => { }); describe('PropertyType', () => { - test('valid types', () => { - expectType>('any string'); - expectType>(123); - expectType>(new Date()); + describe('valid types', () => { + test('top-level string', () => { + expectType>('any string'); + }); - // arrays - expectType>([{ direct: 'foo' }]); - expectType>({ direct: 'foo', other: 123 }); - expectType>('foo'); - expectType>(123); - expectType>(undefined); - expectType>('foo'); - expectType>(123); - expectType>(undefined); + test('top-level number', () => { + expectType>(123); + }); - // object - expectType>({ - deep: { deeper: 'foo' }, - direct: true, + test('top-level date', () => { + expectType>(new Date()); + }); + + describe('arrays', () => { + test('entire list as readonly constant', () => { + const value = [{ direct: 'foo' }] as const; + expectType>(value); + }); + + test('entire list as inline argument without optional number property', () => { + expectType>([{ direct: 'foo' }]); + }); + + test('list item at index as object with optional number property', () => { + expectType>({ direct: 'foo', other: 123 }); + }); + + test('string value for required property of list item at index', () => { + expectType>('foo'); + }); + + test('number value for optional property of list item at index', () => { + expectType>(123); + }); + + test('undefined value for optional property of list item at index', () => { + expectType>(undefined); + }); + + test('string value for string property of list items', () => { + expectType>('foo'); + }); + + test('number value for optional number property of list items', () => { + expectType>(123); + }); + + test('undefined value for optional number property of list items', () => { + expectType>(undefined); + }); + }); + + describe('nested objects', () => { + test('entire object as readonly constant', () => { + const value = { + deep: { deeper: 'foo' }, + direct: true, + } as const; + expectType>(value); + }); + + test('entire object as inline argument', () => { + // object + expectType>({ + deep: { deeper: 'foo' }, + direct: true, + }); + }); + + test('object value for nested property of object', () => { + expectType>({ + deeper: 'foo', + other: 123, + }); + }); + + test('string value for nested string property of object', () => { + expectType>('foo'); + }); + + test('number value for nested optional number property of object', () => { + expectType>(123); + }); + + test('undefined value for nested optional number property of object', () => { + expectType>(undefined); + }); + + test('boolean value for nested property of object', () => { + expectType>(true); + }); }); - expectType>({ deeper: 'foo', other: 123 }); - expectType>('foo'); - expectType>(123); - expectType>(undefined); - expectType>(true); }); test('invalid types', () => { diff --git a/src/mongodbTypes.ts b/src/mongodbTypes.ts index 6429feba4..399508c70 100644 --- a/src/mongodbTypes.ts +++ b/src/mongodbTypes.ts @@ -171,7 +171,7 @@ export type PaprAllProperties = { * } */ export type PaprArrayElementsProperties = { - [Property in `${KeysOfAType, any[]>}.$${ + [Property in `${KeysOfAType, readonly any[]>}.$${ | '' | `[${string}]`}`]?: ArrayElement< PropertyType @@ -193,7 +193,7 @@ export type PaprArrayElementsProperties = { * } */ export type PaprArrayNestedProperties = { - [Property in `${KeysOfAType, Record[]>}.$${ + [Property in `${KeysOfAType, readonly Readonly>[]>}.$${ | '' | `[${string}]`}.${string}`]?: any; }; diff --git a/src/utils.ts b/src/utils.ts index ba7782f35..e8a23aee0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -178,7 +178,7 @@ export type PropertyType = string extends Propert Type extends Record ? Property extends `${string}.${string}` ? PropertyNestedType, Property> - : Type[Property] + : Readonly : Type[Property] : Type extends readonly (infer ArrayType)[] ? // indexed array properties