Skip to content

Commit

Permalink
refactor: Improve projection type safety
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuat committed Jan 19, 2024
1 parent 0b14413 commit 4ef0675
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 16 deletions.
12 changes: 12 additions & 0 deletions src/__tests__/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,18 @@ describe('model', () => {
}
});

test('with invalid projection', async () => {
await simpleModel.find(
{},
{
projection: {
// @ts-expect-error `nested.baz` is not present on the schema
'nested.baz': 1,
},
}
);
});

test('with projection, re-assignable to Pick type', async () => {
let results = await simpleModel.find({}, { projection });

Expand Down
24 changes: 12 additions & 12 deletions src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,32 +78,32 @@ export interface Model<TSchema extends BaseSchema, TOptions extends SchemaOption
options?: Omit<FindOptions<TSchema>, 'limit' | 'projection' | 'skip' | 'sort'>
) => Promise<boolean>;

find: <TProjection extends Projection<TSchema> | undefined>(
find: <TProjection extends Projection<TProjection, TSchema> | undefined>(
filter: PaprFilter<TSchema>,
options?: Omit<FindOptions<TSchema>, 'projection'> & { projection?: TProjection }
) => Promise<ProjectionType<TSchema, TProjection>[]>;

findById: <TProjection extends Projection<TSchema> | undefined>(
findById: <TProjection extends Projection<TProjection, TSchema> | undefined>(
id: TSchema['_id'] | string,
options?: Omit<FindOptions<TSchema>, 'projection'> & { projection?: TProjection }
) => Promise<ProjectionType<TSchema, TProjection> | null>;

findCursor: <TProjection extends Projection<TSchema> | undefined>(
findCursor: <TProjection extends Projection<TProjection, TSchema> | undefined>(
filter: PaprFilter<TSchema>,
options?: Omit<FindOptions<TSchema>, 'projection'> & { projection?: TProjection }
) => Promise<FindCursor<ProjectionType<TSchema, TProjection>>>;

findOne: <TProjection extends Projection<TSchema> | undefined>(
findOne: <TProjection extends Projection<TProjection, TSchema> | undefined>(
filter: PaprFilter<TSchema>,
options?: Omit<FindOptions<TSchema>, 'projection'> & { projection?: TProjection }
) => Promise<ProjectionType<TSchema, TProjection> | null>;

findOneAndDelete: <TProjection extends Projection<TSchema> | undefined>(
findOneAndDelete: <TProjection extends Projection<TProjection, TSchema> | undefined>(
filter: PaprFilter<TSchema>,
options?: Omit<FindOneAndDeleteOptions, 'projection'> & { projection?: TProjection }
) => Promise<ProjectionType<TSchema, TProjection> | null>;

findOneAndUpdate: <TProjection extends Projection<TSchema> | undefined>(
findOneAndUpdate: <TProjection extends Projection<TProjection, TSchema> | undefined>(
filter: PaprFilter<TSchema>,
update: PaprUpdateFilter<TSchema>,
options?: Omit<FindOneAndUpdateOptions, 'projection'> & { projection?: TProjection }
Expand Down Expand Up @@ -621,7 +621,7 @@ export function build<TSchema extends BaseSchema, TOptions extends SchemaOptions
// prettier-ignore
model.find = wrap(
model,
async function find<TProjection extends Projection<TSchema> | undefined>(
async function find<TProjection extends Projection<TProjection, TSchema> | undefined>(
filter: PaprFilter<TSchema>,
options?: Omit<FindOptions<TSchema>, 'projection'> & { projection?: TProjection }
): Promise<ProjectionType<TSchema, TProjection>[]> {
Expand Down Expand Up @@ -663,7 +663,7 @@ export function build<TSchema extends BaseSchema, TOptions extends SchemaOptions
// prettier-ignore
model.findById = wrap(
model,
async function findById<TProjection extends Projection<TSchema> | undefined>(
async function findById<TProjection extends Projection<TProjection, TSchema> | undefined>(
id: TSchema['_id'] | string,
options?: Omit<FindOptions<TSchema>, 'projection'> & { projection?: TProjection }
): Promise<ProjectionType<TSchema, TProjection> | null> {
Expand Down Expand Up @@ -702,7 +702,7 @@ export function build<TSchema extends BaseSchema, TOptions extends SchemaOptions
* }
*/
model.findCursor = wrap(model, async function findCursor<
TProjection extends Projection<TSchema> | undefined,
TProjection extends Projection<TProjection, TSchema> | undefined,
>(filter: PaprFilter<TSchema>, options?: Omit<FindOptions<TSchema>, 'projection'> & { projection?: TProjection }): Promise<
FindCursor<ProjectionType<TSchema, TProjection>>
> {
Expand Down Expand Up @@ -741,7 +741,7 @@ export function build<TSchema extends BaseSchema, TOptions extends SchemaOptions
// prettier-ignore
model.findOne = wrap(
model,
async function findOne<TProjection extends Projection<TSchema> | undefined>(
async function findOne<TProjection extends Projection<TProjection, TSchema> | undefined>(
filter: PaprFilter<TSchema>,
options?: Omit<FindOptions<TSchema>, 'projection'> & { projection?: TProjection }
): Promise<ProjectionType<TSchema, TProjection> | null> {
Expand All @@ -767,7 +767,7 @@ export function build<TSchema extends BaseSchema, TOptions extends SchemaOptions
* const user = await User.findOneAndDelete({ firstName: 'John' });
*/
// prettier-ignore
model.findOneAndDelete = wrap(model, async function findOneAndDelete<TProjection extends Projection<TSchema> | undefined>(
model.findOneAndDelete = wrap(model, async function findOneAndDelete<TProjection extends Projection<TProjection, TSchema> | undefined>(
filter: PaprFilter<TSchema>,
options?: Omit<FindOneAndDeleteOptions, 'projection'> & { projection?: TProjection }
): Promise<ProjectionType<TSchema, TProjection> | null> {
Expand Down Expand Up @@ -811,7 +811,7 @@ export function build<TSchema extends BaseSchema, TOptions extends SchemaOptions
* userProjected.lastName; // valid
*/
// prettier-ignore
model.findOneAndUpdate = wrap(model, async function findOneAndUpdate<TProjection extends Projection<TSchema> | undefined>(
model.findOneAndUpdate = wrap(model, async function findOneAndUpdate<TProjection extends Projection<TProjection, TSchema> | undefined>(
filter: PaprFilter<TSchema>,
update: PaprUpdateFilter<TSchema>,
options?: Omit<FindOneAndUpdateOptions, 'projection'> & { projection?: TProjection }
Expand Down
18 changes: 14 additions & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ type FilterProperties<TObject, TValue> = Pick<TObject, KeysOfAType<TObject, TVal
export type ProjectionType<
TSchema extends BaseSchema,
Projection extends
| Partial<Record<Join<NestedPaths<WithId<TSchema>, []>, '.'>, number>>
| ExactPartial<Projection, Record<Join<NestedPaths<WithId<TSchema>, []>, '.'>, number>>
| undefined,
> = undefined extends Projection
? WithId<TSchema>
Expand All @@ -147,9 +147,19 @@ export type ProjectionType<
keyof FilterProperties<Projection, 0>
>;

export type Projection<TSchema> = Partial<
Record<Join<NestedPaths<WithId<TSchema>, []>, '.'>, number>
>;
type ExactPartial<Subset, BaseObject> = {
[K in keyof Subset]: K extends keyof BaseObject
? BaseObject[K] extends Record<number | string | symbol, unknown>
? ExactPartial<Subset[K], BaseObject[K]>
: BaseObject[K]
: never;
};

export type Projection<
TProjection,
TSchema,
TPaths = Record<Join<NestedPaths<WithId<TSchema>, []>, '.'>, number>,
> = ExactPartial<TProjection, TPaths>;

export type PropertyNestedType<
Type,
Expand Down

0 comments on commit 4ef0675

Please sign in to comment.