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

How to properly define createdAt and updatedAt members when using timestamps? #13897

Closed
1 task done
anttikes opened this issue Sep 26, 2023 · 6 comments · Fixed by #13990
Closed
1 task done

How to properly define createdAt and updatedAt members when using timestamps? #13897

anttikes opened this issue Sep 26, 2023 · 6 comments · Fixed by #13990
Labels
typescript Types or Types-test related issue / Pull Request
Milestone

Comments

@anttikes
Copy link

anttikes commented Sep 26, 2023

Prerequisites

  • I have written a descriptive issue title

Mongoose version

6.5.2

Node.js version

18.12.1

MongoDB version

6.0.5

Operating system

Windows

Operating system version (i.e. 20.04, 11.3, 10)

11

Issue

I know this has been asked previously but reviewing the various other issues I could not find a suitable solution.

In the https://mongoosejs.com/docs/typescript.html the steps to create interfaces and schemas is pretty straightforward. But things seem to break down when timestamps: true is being used.

A simplified example below:

import {Schema, model, HydratedDocument} from 'mongoose';

interface IDocument {
  name: string
}

const documentSchema = new Schema<IDocument >({
  name: { type: String, required: true }
},
{
  timestamps: true
});

const Document = model<IDocument>('Document', documentSchema);

type DocumentDoc = HydratedDocument<IDocument>;

// Create & save
const newDoc = new Document<IDocument>({
  name: 'Foo'
});

newDoc.save();

// Read it back
const hydratedDocument = (await Document.find())[0];

// The following line does not compile since the interface definition does not have 'createdAt'
// although the instance itself does
console.log(`This document was created at '${hydratedDocument.createdAt}'`);

Attempting to fix this at the interface level does not work:

interface IDocument {
  name: string;
  createdAt: Date;
  updatedAt: Date;
}

// Now this will not compile as 'createdAt' and 'updatedAt' have not been defined in the data-to-be-stored
const newDoc = new Document<IDocument>({
  name: 'Foo'
});

No matter what I've tried (methods & overrides, InferSchemaType etc.) they all seem to fall flat on their face in either one of these problems:

  • new Document<IDocument>(...) will not compile as the 'createdAt' and 'updatedAt' have not been given
  • console.log(hydratedDocument.createdAt) does not compile because the type does not have the property

The following circumventions I know:

  • Omit the IDocument part from the new Document(...); this kind of defeats the purpose of type-safety
  • Extend the HydratedDocument type with SchemaTimestampConfigs; this requires explicitly typing whatever is returned from Document.find() into DocumentDoc | null. It works but it is a bit unwieldy, and it doesn't work at all if the timestamps are at sub-document level
  • Marking createdAt and updatedAt optional; this works but it is semantically wrong. See the answer below for my view on why.

Are there any other options I should or could try? What is the "best" way that results in clean, readable and structurally solid code?

@anttikes anttikes added help This issue can likely be resolved in GitHub issues. No bug fixes, features, or docs necessary help wanted labels Sep 26, 2023
@anttikes anttikes changed the title Where to put createdAt and updatedAt members when using timestamps? How to properly define createdAt and updatedAt members when using timestamps? Sep 26, 2023
@MohOraby
Copy link
Contributor

could you try making createdAt and updatedAt optional?

interface IDocument {
  name: string;
  createdAt?: Date;
  updatedAt?: Date;
}

@anttikes
Copy link
Author

Thanks for mentioning it. This is one of the choices that I considered although I didn't initially mention it in the opening text.

The approach works but it is "semantically wrong" because in the document object returned by the database these properties are not really optional. Of course one could theorize that specifying { timestamps: false } option during saving causes them to not get updated, and if specified on every save since the beginning then they are, in a sense, optional but none of our use cases have this save option.

In short, this approach is a "circumvention". I've edited my opening post to include this.

@vkarpov15 vkarpov15 added this to the 7.5.5 milestone Oct 1, 2023
@vkarpov15 vkarpov15 added typescript Types or Types-test related issue / Pull Request and removed help This issue can likely be resolved in GitHub issues. No bug fixes, features, or docs necessary help wanted labels Oct 1, 2023
@vkarpov15
Copy link
Collaborator

The easiest workaround would be to use Partial<> as follows:

const newDoc = new Document<Partial<IDocument>>({
  name: 'Foo'
});

Admittedly that does involve a bit of friction because you need to add that generic every time you instantiate a Document, but that is the most semantically correct approach.

@hasezoey @AbdelrahmanHafez what do you think of adding Partial<> to the model constructor by default, like new <DocType = Partial<TRawDocType>>(doc?: DocType, ...) ? Tests pass with that change, and that change would likely be more correct given that Mongoose often sets required fields using defaults, middleware, setters, and other mechanisms other than the Model constructor. We used to support partial before 6.4.0, but this commit changed that: 4a5d712

@hasezoey
Copy link
Collaborator

what do you think of adding Partial<> to the model constructor by default, like new <DocType = Partial>(doc?: DocType, ...) ?

i dont know why it was changed, but it should likely be OK. once there is a PR i will try to see if there are any issues with typegoose

We used to support partial before 6.4.0, but this commit changed that: 4a5d712

i cant quite tell what the commit changed to remove this, because at least from what i can tell, it didnt use / remove Partial

@AbdelrahmanHafez
Copy link
Collaborator

I think using Partial<IUser> is a perfectly valid approach, since people might want to populate the rest of the fields after building the constructor. I've been using Partial all over my applications for models, but then again I haven't been using strict option in my tsconfig, which is probably allowing me to do things other people might not be able to do.

@vkarpov15 vkarpov15 modified the milestones: 7.6.2, 7.6.3 Oct 13, 2023
@anttikes
Copy link
Author

anttikes commented Oct 13, 2023

Thanks for the ideas. The Partial<IDocument> approach does work but as stated by @vkarpov15 it is also a kind of workaround. The problem is that it makes all properties optional, not just the createdAt and updatedAt ones.

It seems there's a some kind of a building block missing here but I can't really identify it. It should be possible to say that documents created in code have properties A, B and C while documents returned by find(...) etc. have A, B, C, D and E.

I did try the following code which passes syntax check but I haven't tried running it so I don't know if it actually works or not. The idea behind it was to "separate" the type definition of documents that have not been saved from the type definition of documents returned by find(...) etc.:

import {Schema, model, Model} from 'mongoose';

interface IDocument {
  name: string
}

interface ITimestamps {
    createdAt: Date
    updatedAt: Date
}

const documentSchema = new Schema<IDocument >({
  name: { type: String, required: true }
},
{
  timestamps: true
});

const Document = model<IDocument, Model<IDocument & ITimestamps>>('Document', documentSchema);

// Create & save
const newDoc = new Document<IDocument>({
  name: 'Foo'
});

await newDoc.save();

// Read it back
const hydratedDocument = (await Document.find())[0];

// Now it compiles?
console.log(`This document was created at '${hydratedDocument.createdAt}'`);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
typescript Types or Types-test related issue / Pull Request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants