Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic populate assertion with type inference #896

Open
angelhodar opened this issue Nov 27, 2023 · 3 comments
Open

Automatic populate assertion with type inference #896

angelhodar opened this issue Nov 27, 2023 · 3 comments
Labels
feature Adds a new Feature or Request

Comments

@angelhodar
Copy link

angelhodar commented Nov 27, 2023

Describe what you need | want

So I have been writing a lot of isDocument inside my functions to early return if some properties are not previously populated when passing a document instance as parameter. I would like to not only check for those properties populated but already populate them if they arent, but I dont want to always populate those fields because its not always required, so no need for mongoose-autopopulate

Do you have already an idea for the implementation?

With the help of ChatGPT I think I have found a solution to this with this assertPopulated utility function. I am not a typescript expert so if you think this can be improved it would be a great help:

import { DocumentType, Ref } from '@typegoose/typegoose';
import { BaseEntity } from './models';

// Helper type for property names of a document
type PopulatableProperties<T> = {
  [K in keyof T]: T[K] extends Ref<any> ? K : never;
}[keyof T];

// Type to represent a document with certain properties populated
type WithPopulatedProps<T, Props extends keyof T> = Omit<T, Props> & {
  [P in Props]: T[P] extends Ref<infer R> ? DocumentType<R> : T[P];
};

export async function assertPopulated<T extends BaseEntity, P extends PopulatableProperties<T>>(
  document: DocumentType<T>,
  properties: P[],
): Promise<DocumentType<WithPopulatedProps<T, P>>> {
  try {
    for (const prop of properties) {
      if (!(document[prop] instanceof BaseEntity)) {
        await document.populate(prop as string);
      }
    }
    return document as DocumentType<WithPopulatedProps<T, P>>;
  } catch (error) {
    throw new Error(`Error populating document: ${(error as any).message}`);
  }
}

Example usage:

async finishCampaign(this: DocumentType<Enrollment>) {
    const enrollment = await assertPopulated(this, ["user", "campaign"])

    await enrollment.user.addToken(); // Here user is of the type DocumentType<User>

In my case I have a BaseEntity model, but it should also work with the base Document type from mongoose. This is the BaseEntity in case anyone is interested:

@pre<BaseEntity>(['find', 'findOne', 'countDocuments'], function () {
  // Add a default condition to filter out soft-deleted documents
  // With the line below, the middleware respects queries where the deleted ones are required
  // this.setQuery({ deleted: { $ne: true }, ...this.getQuery() });
  if (this.getOptions().includeDeleted) return
  // With this one, it just extends the query and overrides any possible way to fetch the deleted ones
  this.where({ deleted: { $ne: true } });
})
export class BaseEntity implements Base {
  @Field(() => ID)
  readonly _id: Types.ObjectId;

  @Field(() => ID)
  readonly id: string;

  @Field()
  @prop()
  public createdAt: Date;

  @Field()
  @prop()
  public updatedAt: Date;

  @Field({ nullable: true })
  @prop()
  public deleted?: boolean;

  @Field({ nullable: true })
  @prop()
  public deletedAt?: Date;

  async markDeleted(this: DocumentType<BaseEntity>) {
    this.deleted = true;
    this.deletedAt = new Date();
    await this.save();
  }
}

What do you think? @hasezoey If you think this is correct and useful, feel free to add it to the documentation, or I can just open a PR to do it!

@angelhodar angelhodar added the feature Adds a new Feature or Request label Nov 27, 2023
@hasezoey
Copy link
Member

With the help of ChatGPT

to confirm, did you test the code?


the types look reasonable, but there are some small problems:

  • the !(document[prop] instanceof BaseEntity would need to be changed to be generic and not tied to a specific model
  • there would need to be actual handling if the population fails without throwing a error (like getting set to null, see What If There's No Foreign Document?) which could be solved by always just adding | null to each PopulatableProperties
  • the document type generic would need to be captured and actually be correctly set (there is not just one generic on DocumentType)

@angelhodar
Copy link
Author

angelhodar commented Nov 28, 2023

Yes I tested it but just manually in some functions I have inside the branch where I created this assertPopulated function. I used ChatGPT for the complex types because I couldnt have created them myself fastly. Regarding your points:

  • Changing BaseEntiy with mongoose Document should work without changing anything else.
  • I personally wanted to throw an error because most of the times when I check for population with isDocument and the property is not correctly populated I abort the operation, but as you say it would be easy to add | null to the PopulatableProperties
  • I dont understand exactly what you wanted to say, could you give me more details?

Anyway this was just an experiment to be refined here with you as I am not as expert as you in typescript and typegoose :)

@hasezoey
Copy link
Member

I dont understand exactly what you wanted to say, could you give me more details?

is this in context of the following?

the document type generic would need to be captured and actually be correctly set (there is not just one generic on DocumentType)

when yes, this refers to DocumentType (typegoose's mongoose.Document enhancement) does not just have 1 generic (T), but at current has 2 generics: (T & QueryHelpers)

export type DocumentType<T, QueryHelpers = BeAnObject> = mongoose.Document<unknown, QueryHelpers, T> &

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Adds a new Feature or Request
Projects
None yet
Development

No branches or pull requests

2 participants