francescov1 / mongoose-tsgen

A plug-n-play Typescript generator for Mongoose.
105 stars 25 forks source link

Object schema with default: Type is not assignable #93

Closed NelsonFrancisco closed 2 years ago

NelsonFrancisco commented 2 years ago

Here's a failing snippet:

import mongoose from 'mongoose';
import {
  TestDocument,
  TestModel,
  TestSchema,
} from '../interfaces/mongoose.gen';

const { Schema } = mongoose;
const booleanGroupSchema = new Schema(
  {
    editSignature: Boolean,
    editStatement: Boolean,
    reviewTemplates: Boolean,
  },
  { _id: false },
);

const testSchema: TestSchema = new Schema(
  {
    _id: { type: String },
    booleanGroup: {
      type: booleanGroupSchema,
      default: {},
    },
  },
  { timestamps: true },
);

const Test: TestModel =
  mongoose.models.Test ||
  mongoose.model<TestDocument, TestModel>('Test', testSchema);
export default Test;

The complete ts error is:

(property) booleanGroup?: mongoose.SchemaDefinitionProperty<TestBooleanGroupDocument>
Type '{ type: mongoose.Schema<any, mongoose.Model<any, any, any, any>, {}, {}>; default: {}; }' is not assignable to type 'SchemaDefinitionProperty<TestBooleanGroupDocument>'.
  Type '{ type: mongoose.Schema<any, mongoose.Model<any, any, any, any>, {}, {}>; default: {}; }' is not assignable to type 'string'.ts(2322)
[mongoose.gen.ts(117, 5): ]()The expected type comes from property 'booleanGroup' which is declared here on type '{ _id?: SchemaDefinitionProperty<string>; __v?: SchemaDefinitionProperty<any>; id?: SchemaDefinitionProperty<any>; updatedAt?: SchemaDefinitionProperty<...>; createdAt?: SchemaDefinitionProperty<...>; booleanGroup?: SchemaDefinitionProperty<...>; }'

I cannot figure out. Looks like it lacks some properties

Here's the result of the generation:

/* tslint:disable */
/* eslint-disable */

// ######################################## THIS FILE WAS GENERATED BY MONGOOSE-TSGEN ######################################## //

// NOTE: ANY CHANGES MADE WILL BE OVERWRITTEN ON SUBSEQUENT EXECUTIONS OF MONGOOSE-TSGEN.

import mongoose from 'mongoose';

/**
 * Lean version of TestBooleanGroupDocument
 *
 * This has all Mongoose getters & functions removed. This type will be returned from `TestDocument.toObject()`.
 * ```
 * const testObject = test.toObject();
 * ```
 */
export type TestBooleanGroup = {
  editSignature?: boolean;
  editStatement?: boolean;
  reviewTemplates?: boolean;
};

/**
 * Lean version of TestDocument
 *
 * This has all Mongoose getters & functions removed. This type will be returned from `TestDocument.toObject()`. To avoid conflicts with model names, use the type alias `TestObject`.
 * ```
 * const testObject = test.toObject();
 * ```
 */
export type Test = {
  _id: string;
  updatedAt?: Date;
  createdAt?: Date;
  booleanGroup?: TestBooleanGroup;
};

/**
 * Lean version of TestDocument (type alias of `Test`)
 *
 * Use this type alias to avoid conflicts with model names:
 * ```
 * import { Test } from "../models"
 * import { TestObject } from "../interfaces/mongoose.gen.ts"
 *
 * const testObject: TestObject = test.toObject();
 * ```
 */
export type TestObject = Test;

/**
 * Mongoose Query type
 *
 * This type is returned from query functions. For most use cases, you should not need to use this type explicitly.
 */
export type TestQuery = mongoose.Query<any, TestDocument, TestQueries> & TestQueries;

/**
 * Mongoose Query helper types
 *
 * This type represents `TestSchema.query`. For most use cases, you should not need to use this type explicitly.
 */
export type TestQueries = {};

export type TestMethods = {};

export type TestStatics = {};

/**
 * Mongoose Model type
 *
 * Pass this type to the Mongoose Model constructor:
 * ```
 * const Test = mongoose.model<TestDocument, TestModel>("Test", TestSchema);
 * ```
 */
export type TestModel = mongoose.Model<TestDocument, TestQueries> & TestStatics;

/**
 * Mongoose Schema type
 *
 * Assign this type to new Test schema instances:
 * ```
 * const TestSchema: TestSchema = new mongoose.Schema({ ... })
 * ```
 */
export type TestSchema = mongoose.Schema<TestDocument, TestModel, TestMethods, TestQueries>;

/**
 * Mongoose Document type
 *
 * Pass this type to the Mongoose Model constructor:
 * ```
 * const Test = mongoose.model<TestDocument, TestModel>("Test", TestSchema);
 * ```
 */
export type TestBooleanGroupDocument = mongoose.Document<string> & {
  editSignature?: boolean;
  editStatement?: boolean;
  reviewTemplates?: boolean;
};

/**
 * Mongoose Document type
 *
 * Pass this type to the Mongoose Model constructor:
 * ```
 * const Test = mongoose.model<TestDocument, TestModel>("Test", TestSchema);
 * ```
 */
export type TestDocument = mongoose.Document<string, TestQueries> &
  TestMethods & {
    _id: string;
    updatedAt?: Date;
    createdAt?: Date;
    booleanGroup?: TestBooleanGroupDocument;
  };

/**
 * Check if a property on a document is populated:
 * ```
 * import { IsPopulated } from "../interfaces/mongoose.gen.ts"
 *
 * if (IsPopulated<UserDocument["bestFriend"]>) { ... }
 * ```
 */
export function IsPopulated<T>(doc: T | mongoose.Types.ObjectId): doc is T {
  return doc instanceof mongoose.Document;
}

/**
 * Helper type used by `PopulatedDocument`. Returns the parent property of a string
 * representing a nested property (i.e. `friend.user` -> `friend`)
 */
type ParentProperty<T> = T extends `${infer P}.${string}` ? P : never;

/**
 * Helper type used by `PopulatedDocument`. Returns the child property of a string
 * representing a nested property (i.e. `friend.user` -> `user`).
 */
type ChildProperty<T> = T extends `${string}.${infer C}` ? C : never;

/**
 * Helper type used by `PopulatedDocument`. Removes the `ObjectId` from the general union type generated
 * for ref documents (i.e. `mongoose.Types.ObjectId | UserDocument` -> `UserDocument`)
 */
type PopulatedProperty<Root, T extends keyof Root> = Omit<Root, T> & {
  [ref in T]: Root[T] extends mongoose.Types.Array<infer U>
    ? mongoose.Types.Array<Exclude<U, mongoose.Types.ObjectId>>
    : Exclude<Root[T], mongoose.Types.ObjectId>;
};

/**
 * Populate properties on a document type:
 * ```
 * import { PopulatedDocument } from "../interfaces/mongoose.gen.ts"
 *
 * function example(user: PopulatedDocument<UserDocument, "bestFriend">) {
 *   console.log(user.bestFriend._id) // typescript knows this is populated
 * }
 * ```
 */
export type PopulatedDocument<DocType, T> = T extends keyof DocType
  ? PopulatedProperty<DocType, T>
  : ParentProperty<T> extends keyof DocType
  ? Omit<DocType, ParentProperty<T>> & {
      [ref in ParentProperty<T>]: DocType[ParentProperty<T>] extends mongoose.Types.Array<infer U>
        ? mongoose.Types.Array<
            ChildProperty<T> extends keyof U
              ? PopulatedProperty<U, ChildProperty<T>>
              : PopulatedDocument<U, ChildProperty<T>>
          >
        : ChildProperty<T> extends keyof DocType[ParentProperty<T>]
        ? PopulatedProperty<DocType[ParentProperty<T>], ChildProperty<T>>
        : PopulatedDocument<DocType[ParentProperty<T>], ChildProperty<T>>;
    }
  : DocType;

/**
 * Helper types used by the populate overloads
 */
type Unarray<T> = T extends Array<infer U> ? U : T;
type Modify<T, R> = Omit<T, keyof R> & R;

/**
 * Augment mongoose with Query.populate overloads
 */
declare module 'mongoose' {
  interface Query<ResultType, DocType, THelpers = {}> {
    populate<T extends string>(
      path: T,
      select?: string | any,
      model?: string | Model<any, THelpers>,
      match?: any,
    ): Query<
      ResultType extends Array<DocType>
        ? Array<PopulatedDocument<Unarray<ResultType>, T>>
        : ResultType extends DocType
        ? PopulatedDocument<Unarray<ResultType>, T>
        : ResultType,
      DocType,
      THelpers
    > &
      THelpers;

    populate<T extends string>(
      options: Modify<PopulateOptions, { path: T }> | Array<PopulateOptions>,
    ): Query<
      ResultType extends Array<DocType>
        ? Array<PopulatedDocument<Unarray<ResultType>, T>>
        : ResultType extends DocType
        ? PopulatedDocument<Unarray<ResultType>, T>
        : ResultType,
      DocType,
      THelpers
    > &
      THelpers;
  }
}
francescov1 commented 2 years ago

Thanks for submitting and providing all the details. Definitely just an edge case of Mongoose syntax that isn't handled. Ill try to get to this over the weekend but might be a bit busy.

If you need this to be functional before then, I think removing default: {} would fix the problem (although you would need to set it manually then).

NelsonFrancisco commented 2 years ago

I cannot change the Schema. Lots of dependencies on it right now.

I just added @ts-ignore, which I hope to be temporary.

francescov1 commented 2 years ago

Apologies for the delay here I've been quite busy, hoping to get to this soon!

francescov1 commented 2 years ago

Hey @NelsonFrancisco , apologies for the delay.

I think the generator is actually correct in this case, {} is not an allowed value for a document (in your case the field booleanGroup). You can fix this by instantiating a document using a BooleanGroupModel:

import mongoose from 'mongoose';
import {
  TestDocument,
  TestModel,
  TestSchema,
  BooleanGroupDocument,
  BooleanGroupModel,
  BooleanGroupSchema
} from '../../../interfaces/mongoose.gen';

const { Schema } = mongoose;
const booleanGroupSchema: BooleanGroupSchema = new Schema(
  {
    editSignature: Boolean,
    editStatement: Boolean,
    reviewTemplates: Boolean,
  },
  { _id: false },
);

const testSchema: TestSchema = new Schema(
  {
    _id: { type: String },
    booleanGroup: {
      type: booleanGroupSchema,
      default: () => new BooleanGroup({}),
    },
  },
  { timestamps: true },
);

const Test: TestModel =
  mongoose.models.Test ||
  mongoose.model<TestDocument, TestModel>('Test', testSchema);

export const BooleanGroup: BooleanGroupModel =
  mongoose.models.Boolean ||
  mongoose.model<BooleanGroupDocument, BooleanGroupModel>('BooleanGroup', booleanGroupSchema);

export default Test;
francescov1 commented 2 years ago

@NelsonFrancisco Have you had a chance to review the previous answer? Would like to close this off if your issue is resolved

francescov1 commented 2 years ago

Closing this off, feel free to re-open if there is any issues still