diff --git a/README.md b/README.md index 174abab..a7af603 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ When using mongoose and Typescript, you must define schemas and interfaces. Both All types as created from 1-liner functions and does not depend on decorators❗️. For example: -`Type.string()` returns `{type: String, required: true}`, which is the same definition required in the original mongoose library. +`Type.string({ required: true })` returns `{type: String, required: true}`, which is the same definition required in the original mongoose library. ## Example @@ -101,37 +101,43 @@ import { createSchema, Type, typedModel } from 'ts-mongoose'; const genders = ['male', 'female'] as const; -const AddressSchema = createSchema({ - city: Type.string(), - country: Type.optionalString(), - zip: Type.optionalString(), -}, { _id: false, timestamps: true }); +const AddressSchema = createSchema( + { + city: Type.string({ required: true }), + country: Type.string(), + zip: Type.string(), + }, + { _id: false, timestamps: true } +); const PhoneSchema = createSchema({ - phoneNumber: Type.number(), - name: Type.optionalString(), + phoneNumber: Type.number({ required: true }), + name: Type.string(), }); -const UserSchema = createSchema({ - title: Type.string(), - author: Type.string(), - body: Type.string(), - comments: Type.array().of({ - body: Type.string(), - date: Type.date(), - }), - date: Type.date({ default: Date.now as any }), - hidden: Type.boolean(), - meta: Type.object().of({ - votes: Type.number(), - favs: Type.number(), - }), - m: Type.mixed(), - gender: Type.string({ enum: genders }), - otherId: Type.objectId(), - address: Type.schema().of(AddressSchema), - phones: Type.array().of(PhoneSchema), -}, { timestamps: { createdAt: true } }); +const UserSchema = createSchema( + { + title: Type.string({ required: true }), + author: Type.string({ required: true }), + body: Type.string({ required: true }), + comments: Type.array().of({ + body: Type.string({ required: true }), + date: Type.date({ required: true }), + }), + date: Type.date({ default: Date.now as any }), + hidden: Type.boolean({ required: true }), + meta: Type.object().of({ + votes: Type.number({ required: true }), + favs: Type.number({ required: true }), + }), + m: Type.mixed({ required: true }), + gender: Type.string({ required: true, enum: genders }), + otherId: Type.objectId({ required: true }), + address: Type.schema({ required: true }).of(AddressSchema), + phones: Type.array({ required: true }).of(PhoneSchema), + }, + { timestamps: { createdAt: true } } +); const User = typedModel('User', UserSchema); User.findById('123').then(user => { @@ -148,9 +154,9 @@ User.findById('123').then(user => { ```ts { // same as {type: String} - firstName: Type.optionalString(), + firstName: Type.string(), // same as {type: String, required: true} - email: Type.string(), + email: Type.string({ required: true }), } ``` @@ -159,7 +165,7 @@ User.findById('123').then(user => { ```ts { // same as {type: String, required: true, unique: true, index: true} - email: Type.string({ unique: true, index: true }); + email: Type.string({ required: true, unique: true, index: true }); } ``` @@ -168,7 +174,7 @@ User.findById('123').then(user => { ```ts const genders = ['male', 'female'] as const; { - // same as {type: String, required: true, enum: ['male', 'female']} + // same as {type: String, enum: ['male', 'female']} gender: Type.string({ enum: genders }); } ``` @@ -178,7 +184,7 @@ const genders = ['male', 'female'] as const; ```ts { // same as {type: [String], required: true} - tags: Type.array().of(Type.string()); + tags: Type.array({ required: true }).of(Type.string({ required: true })); } ``` @@ -186,7 +192,7 @@ const genders = ['male', 'female'] as const; ```ts const AddressSchema = createSchema( - { city: Type.string() }, + { city: Type.string({ required: true }) }, { _id: false, timestamps: true } ); { @@ -200,12 +206,12 @@ const AddressSchema = createSchema( ```ts const PhoneSchema = createSchema( - { phoneNumber: Type.number() }, + { phoneNumber: Type.number({ required: true }) }, { _id: false } ); { // same as {type: [PhoneSchema]} - phones: Type.schema().of(PhoneSchema); + phones: Type.array().of(PhoneSchema); } // phones property has such methods as create(), id(), but also those typical for arrays like map(), filter() etc ``` @@ -248,9 +254,9 @@ import { } from 'ts-mongoose'; export const UserSchema = createSchema({ - email: Type.string(), - username: Type.string(), - isBlocked: Type.optionalBoolean(), + email: Type.string({ required: true }), + username: Type.string({ required: true }), + isBlocked: Type.boolean(), }); export const User = typedModel('User', UserSchema); @@ -289,8 +295,8 @@ It's only required if you add virtual fields or custom methods to the model. ```ts const UserSchema = createSchema({ - title: Type.string(), - author: Type.string(), + title: Type.string({ required: true }), + author: Type.string({ required: true }), ...({} as { generatedField: string; customFunction: () => number; @@ -304,35 +310,31 @@ Autocomplete popup: ### Static methods -If you need to have static custom methods on Model you can pass them in `statics` property, which is part of schema options. For functions returning instance/s of Model, use `ModelInstanceType` / `ModelInstancesType` interfaces as returning value. +If you need to have static custom methods on Model you can pass them as 5th parameter of `typedModel` function. It should automatically figured out returning value, but you can declare it too. ```ts -const UserSchema = createSchema( - { - name: Type.string(), - age: Type.number(), - }, - { - statics: { - findByName: function(name: string): ModelInstancesType { - return this.find({ name: name }); - }, - findOneByName: function(name: string): ModelInstanceType { - return this.findOne({ name: name }); - }, - countLetters: function(name: string, bonus?: number): number { - return name.length + (bonus ? bonus : 0); - }, - }, - } -); +const UserSchema = createSchema({ + name: Type.string({ required: true }), + age: Type.number({ required: true }), +}); -const User = typedModel('User', UserSchema); -User.countLetters('a'); +const User = typedModel('User', UserSchema, undefined, undefined, { + findByName: function(name: string) { + return this.find({ name }); + }, + findOneByName: function(name: string) { + return this.findOne({ name }); + }, + countLetters: function(name: string, bonus?: number) { + return name.length + (bonus ? bonus : 0); + }, +}); +const u = await User.findOne({}); +if (u) u.name; ``` ### TODO -- support types: Decimal128, Map +- support types: Map MIT diff --git a/__tests__/tests.test.ts b/__tests__/tests.test.ts index 176784e..ab2234d 100644 --- a/__tests__/tests.test.ts +++ b/__tests__/tests.test.ts @@ -3,26 +3,60 @@ import { Schema } from 'mongoose'; describe('string', () => { test('required', () => { - expect(Type.string()).toEqual({ + expect(Type.string({ required: true })).toEqual({ type: String, required: true, }); }); test('required with options', () => { + expect(Type.string({ required: true, unique: true })).toEqual({ + type: String, + required: true, + unique: true, + }); + }); + test('optional', () => { + expect(Type.string()).toEqual({ + type: String, + }); + }); + test('optional with options', () => { expect(Type.string({ unique: true })).toEqual({ type: String, + unique: true, + }); + }); +}); + +const genders = ['female', 'male'] as const; +describe('string enum', () => { + test('required', () => { + expect(Type.string({ required: true, enum: genders })).toEqual({ + type: String, + enum: ['female', 'male'], + required: true, + }); + }); + test('required with options', () => { + expect( + Type.string({ required: true, enum: genders, unique: true }) + ).toEqual({ + type: String, + enum: ['female', 'male'], required: true, unique: true, }); }); test('optional', () => { - expect(Type.optionalString()).toEqual({ + expect(Type.string({ enum: genders })).toEqual({ type: String, + enum: ['female', 'male'], }); }); test('optional with options', () => { - expect(Type.optionalString({ unique: true })).toEqual({ + expect(Type.string({ enum: genders, unique: true })).toEqual({ type: String, + enum: ['female', 'male'], unique: true, }); }); @@ -30,25 +64,25 @@ describe('string', () => { describe('number', () => { test('required', () => { - expect(Type.number()).toEqual({ + expect(Type.number({ required: true })).toEqual({ type: Number, required: true, }); }); test('required with options', () => { - expect(Type.number({ unique: true })).toEqual({ + expect(Type.number({ required: true, unique: true })).toEqual({ type: Number, required: true, unique: true, }); }); test('optional', () => { - expect(Type.optionalNumber()).toEqual({ + expect(Type.number()).toEqual({ type: Number, }); }); test('optional with options', () => { - expect(Type.optionalNumber({ unique: true })).toEqual({ + expect(Type.number({ unique: true })).toEqual({ type: Number, unique: true, }); @@ -57,25 +91,25 @@ describe('number', () => { describe('boolean', () => { test('required', () => { - expect(Type.boolean()).toEqual({ + expect(Type.boolean({ required: true })).toEqual({ type: Boolean, required: true, }); }); test('required with options', () => { - expect(Type.boolean({ unique: true })).toEqual({ + expect(Type.boolean({ required: true, unique: true })).toEqual({ type: Boolean, required: true, unique: true, }); }); test('optional', () => { - expect(Type.optionalBoolean()).toEqual({ + expect(Type.boolean()).toEqual({ type: Boolean, }); }); test('optional with options', () => { - expect(Type.optionalBoolean({ unique: true })).toEqual({ + expect(Type.boolean({ unique: true })).toEqual({ type: Boolean, unique: true, }); @@ -84,25 +118,25 @@ describe('boolean', () => { describe('date', () => { test('required', () => { - expect(Type.date()).toEqual({ + expect(Type.date({ required: true })).toEqual({ type: Date, required: true, }); }); test('required with options', () => { - expect(Type.date({ unique: true })).toEqual({ + expect(Type.date({ required: true, unique: true })).toEqual({ type: Date, required: true, unique: true, }); }); test('optional', () => { - expect(Type.optionalDate()).toEqual({ + expect(Type.date()).toEqual({ type: Date, }); }); test('optional with options', () => { - expect(Type.optionalDate({ unique: true })).toEqual({ + expect(Type.date({ unique: true })).toEqual({ type: Date, unique: true, }); @@ -111,25 +145,25 @@ describe('date', () => { describe('mixed', () => { test('required', () => { - expect(Type.mixed()).toEqual({ + expect(Type.mixed({ required: true })).toEqual({ type: Schema.Types.Mixed, required: true, }); }); test('required with options', () => { - expect(Type.mixed({ unique: true })).toEqual({ + expect(Type.mixed({ required: true, unique: true })).toEqual({ type: Schema.Types.Mixed, required: true, unique: true, }); }); test('optional', () => { - expect(Type.optionalMixed()).toEqual({ + expect(Type.mixed()).toEqual({ type: Schema.Types.Mixed, }); }); test('optional with options', () => { - expect(Type.optionalMixed({ unique: true })).toEqual({ + expect(Type.mixed({ unique: true })).toEqual({ type: Schema.Types.Mixed, unique: true, }); @@ -138,36 +172,63 @@ describe('mixed', () => { describe('objectId', () => { test('required', () => { - expect(Type.objectId()).toEqual({ + expect(Type.objectId({ required: true })).toEqual({ type: Schema.Types.ObjectId, required: true, }); }); test('required with options', () => { - expect(Type.objectId({ unique: true })).toEqual({ + expect(Type.objectId({ required: true, unique: true })).toEqual({ type: Schema.Types.ObjectId, required: true, unique: true, }); }); test('optional', () => { - expect(Type.optionalObjectId()).toEqual({ + expect(Type.objectId()).toEqual({ type: Schema.Types.ObjectId, }); }); test('optional with options', () => { - expect(Type.optionalObjectId({ unique: true })).toEqual({ + expect(Type.objectId({ unique: true })).toEqual({ type: Schema.Types.ObjectId, unique: true, }); }); }); +describe('decimal128', () => { + test('required', () => { + expect(Type.decimal128({ required: true })).toEqual({ + type: Schema.Types.Decimal128, + required: true, + }); + }); + test('required with options', () => { + expect(Type.decimal128({ required: true, unique: true })).toEqual({ + type: Schema.Types.Decimal128, + required: true, + unique: true, + }); + }); + test('optional', () => { + expect(Type.decimal128()).toEqual({ + type: Schema.Types.Decimal128, + }); + }); + test('optional with options', () => { + expect(Type.decimal128({ unique: true })).toEqual({ + type: Schema.Types.Decimal128, + unique: true, + }); + }); +}); + describe('object', () => { test('required', () => { expect( - Type.object().of({ - foo: Type.string(), + Type.object({ required: true }).of({ + foo: Type.string({ required: true }), }) ).toEqual({ type: { @@ -181,8 +242,8 @@ describe('object', () => { }); test('required with options', () => { expect( - Type.object({ unique: true }).of({ - foo: Type.string(), + Type.object({ required: true, unique: true }).of({ + foo: Type.string({ required: true }), }) ).toEqual({ type: { @@ -197,8 +258,8 @@ describe('object', () => { }); test('optional', () => { expect( - Type.optionalObject().of({ - foo: Type.string(), + Type.object().of({ + foo: Type.string({ required: true }), }) ).toEqual({ type: { @@ -211,8 +272,8 @@ describe('object', () => { }); test('optional with options', () => { expect( - Type.optionalObject({ unique: true }).of({ - foo: Type.string(), + Type.object({ unique: true }).of({ + foo: Type.string({ required: true }), }) ).toEqual({ type: { @@ -228,7 +289,9 @@ describe('object', () => { describe('array', () => { test('required', () => { - expect(Type.array().of(Type.string())).toEqual({ + expect( + Type.array({ required: true }).of(Type.string({ required: true })) + ).toEqual({ type: [ { type: String, @@ -239,7 +302,11 @@ describe('array', () => { }); }); test('required with options', () => { - expect(Type.array({ unique: true }).of(Type.string())).toEqual({ + expect( + Type.array({ required: true, unique: true }).of( + Type.string({ required: true }) + ) + ).toEqual({ type: [ { type: String, @@ -251,7 +318,7 @@ describe('array', () => { }); }); test('optional', () => { - expect(Type.optionalArray().of(Type.string())).toEqual({ + expect(Type.array().of(Type.string({ required: true }))).toEqual({ type: [ { type: String, @@ -261,7 +328,9 @@ describe('array', () => { }); }); test('optional with options', () => { - expect(Type.optionalArray({ unique: true }).of(Type.string())).toEqual({ + expect( + Type.array({ unique: true }).of(Type.string({ required: true })) + ).toEqual({ type: [ { type: String, @@ -278,25 +347,25 @@ describe('schema', () => { foo: Type.string(), }); test('required', () => { - expect(Type.schema().of(schema)).toEqual({ + expect(Type.schema({ required: true }).of(schema)).toEqual({ type: schema, required: true, }); }); test('required with options', () => { - expect(Type.schema({ unique: true }).of(schema)).toEqual({ + expect(Type.schema({ required: true, unique: true }).of(schema)).toEqual({ type: schema, required: true, unique: true, }); }); test('optional', () => { - expect(Type.optionalSchema().of(schema)).toEqual({ + expect(Type.schema().of(schema)).toEqual({ type: schema, }); }); test('optional with options', () => { - expect(Type.optionalSchema({ unique: true }).of(schema)).toEqual({ + expect(Type.schema({ unique: true }).of(schema)).toEqual({ type: schema, unique: true, }); @@ -304,14 +373,14 @@ describe('schema', () => { }); describe('ref', () => { - test('ref', () => { + test('required', () => { const CommentSchema = createSchema({ content: Type.string(), date: Type.date(), }); const schema = { - comments: Type.array().of( - Type.ref(Type.string()).to('Comment', CommentSchema) + comments: Type.array({ required: true }).of( + Type.ref(Type.string({ required: true })).to('Comment', CommentSchema) ), }; expect(schema).toEqual({ @@ -331,21 +400,15 @@ describe('ref', () => { describe('typedModel - statics', () => { test('should return Model with static function', () => { - const CommentSchema = createSchema( - { - content: Type.string(), - date: Type.date(), + const CommentSchema = createSchema({ + content: Type.string(), + date: Type.date(), + }); + const CommentModel = typedModel('cm', CommentSchema, undefined, undefined, { + countLetters: function(name: string, bonus?: number): number { + return name.length + (bonus ? bonus : 0); }, - { - statics: { - countLetters: function(name: string, bonus?: number): number { - return name.length + (bonus ? bonus : 0); - }, - }, - } - ); - const CommentModel = typedModel('cm', CommentSchema); + }); expect(typeof CommentModel.countLetters).toBe('function'); - expect(CommentSchema.options).not.toHaveProperty('statics'); }); }); diff --git a/example/example1.ts b/example/example1.ts index cf5659d..d222929 100644 --- a/example/example1.ts +++ b/example/example1.ts @@ -4,38 +4,38 @@ const genders = ['male', 'female'] as const; const AddressSchema = createSchema( { - city: Type.string(), - country: Type.optionalString(), - zip: Type.optionalString(), + city: Type.string({ required: true }), + country: Type.string(), + zip: Type.string(), }, { _id: false, timestamps: true } ); const PhoneSchema = createSchema({ - phone: Type.number(), - name: Type.optionalString(), + phoneNumber: Type.number({ required: true }), + name: Type.string(), }); const UserSchema = createSchema( { - title: Type.string(), - author: Type.string(), - body: Type.string(), + title: Type.string({ required: true }), + author: Type.string({ required: true }), + body: Type.string({ required: true }), comments: Type.array().of({ - body: Type.string(), - date: Type.date(), + body: Type.string({ required: true }), + date: Type.date({ required: true }), }), date: Type.date({ default: Date.now as any }), - hidden: Type.boolean(), + hidden: Type.boolean({ required: true }), meta: Type.object().of({ - votes: Type.number(), - favs: Type.number(), + votes: Type.number({ required: true }), + favs: Type.number({ required: true }), }), - m: Type.mixed(), - gender: Type.string({ enum: genders }), - otherId: Type.objectId(), - address: Type.schema().of(AddressSchema), - phones: Type.array().of(PhoneSchema), + m: Type.mixed({ required: true }), + gender: Type.string({ required: true, enum: genders }), + otherId: Type.objectId({ required: true }), + address: Type.schema({ required: true }).of(AddressSchema), + phones: Type.array({ required: true }).of(PhoneSchema), }, { timestamps: { createdAt: true } } ); @@ -49,4 +49,6 @@ User.findById('123').then(user => { type UserDoc = ExtractDoc; -function blockUser(user: UserDoc) {} +function blockUser(user: UserDoc) { + // user. +} diff --git a/example/example6.ts b/example/example6.ts index 91b3942..538fd58 100644 --- a/example/example6.ts +++ b/example/example6.ts @@ -1,35 +1,26 @@ -import { - createSchema, - Type, - typedModel, - ModelInstanceType, - ModelInstancesType, -} from '../src'; +import { createSchema, Type, typedModel } from '../src'; import '../src/plugin'; -const UserSchema = createSchema( - { - name: Type.string(), - age: Type.number(), - }, - { - statics: { - findByName: function(name: string): ModelInstancesType { - return this.find({ name: name }); - }, - findOneByName: function(name: string): ModelInstanceType { - return this.findOne({ name: name }); - }, - countLetters: function(name: string, bonus?: number): number { - return name.length + (bonus ? bonus : 0); - }, - }, - } -); +const UserSchema = createSchema({ + name: Type.string(), + age: Type.number(), +}); -const User = typedModel('User', UserSchema); +const User = typedModel('User', UserSchema, undefined, undefined, { + findByName: function(name: string) { + return this.find({ name }); + }, + findOneByName: function(name: string) { + return this.findOne({ name }); + }, + countLetters: function(name: string, bonus?: number) { + return name.length + (bonus ? bonus : 0); + }, +}); async function test() { + const u = await User.findOne({}); + if (u) u.name; const users = await User.findByName('John'); users[0].age; const user = await User.findOneByName('Michael'); @@ -38,4 +29,5 @@ async function test() { } const nameLength = User.countLetters('123', 3); + nameLength.toFixed(); } diff --git a/package.json b/package.json index 6469f67..50ca329 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-mongoose", - "version": "0.0.19", + "version": "0.0.20", "description": "", "main": "index.js", "module": "es/index.js", @@ -16,23 +16,27 @@ "url": "git+https://github.com/BetterCallSky/ts-mongoose.git" }, "author": "Łukasz Sentkiewicz", + "contributors": [ + "Łukasz Gosławski" + ], "license": "MIT", "bugs": { "url": "https://github.com/BetterCallSky/ts-mongoose/issues" }, "homepage": "https://github.com/BetterCallSky/ts-mongoose#readme", "devDependencies": { - "@types/mongoose": "^5.3.2", - "mongoose": "^5.3.14", - "@types/jest": "^23.3.9", - "jest": "^23.6.0", - "prettier": "^1.14.3", - "ts-jest": "^23.10.4", - "typescript": "^3.1.6" + "@types/mongoose": "^5.5.21", + "@types/node": "^12.7.12", + "mongoose": "^5.7.5", + "@types/jest": "^24.0.19", + "jest": "^24.9.0", + "prettier": "^1.18.2", + "ts-jest": "^24.1.0", + "typescript": "^3.6.3" }, "dependencies": {}, "peerDependencies": { - "@types/mongoose": "^5.3.2", - "mongoose": "^5.3.14" + "@types/mongoose": "^5.5.21", + "mongoose": "^5.7.5" } } diff --git a/scripts/build.sh b/scripts/build.sh index 9741908..bfed4ff 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,6 +1,11 @@ rm -rf ./dist +mkdir pre_dist mkdir dist -yarn run tsc --declaration true --module esnext --outDir "./dist/es" -yarn run tsc --declaration true --module commonjs --outDir "./dist" +mkdir dist/es +yarn run tsc --declaration true --module esnext --outDir "./pre_dist/es" +yarn run tsc --declaration true --module commonjs --outDir "./pre_dist" +mv ./pre_dist/es/src/* ./dist/es +mv ./pre_dist/src/* ./dist +rm -rf ./pre_dist cp package.json ./dist/package.json cp README.md ./dist/README.md diff --git a/src/Type.ts b/src/Type.ts index 56f4dfd..16b5b58 100644 --- a/src/Type.ts +++ b/src/Type.ts @@ -1,119 +1,67 @@ -import { SchemaTypeOpts, Schema, Types } from 'mongoose'; +import { Schema, Types } from 'mongoose'; import { - Extract, - ConvertObject, - ExtractSchema, ArrayOfElements, + Convert, EnumOrString, - Omit, + Extract, + GetType, + TypeOptions, + GetSubDocument, + OptionalField, + Optional, + ArrElement, + Definition, + DefinitionField, } from './types'; -const createType = (type: any) => (options: SchemaTypeOpts = {}) => { - return ({ - required: true, - ...options, - type, - } as any) as T; -}; - -const createOptionalType = (type: any) => ( - options: SchemaTypeOpts = {} -) => { - return ({ - ...options, - type, - } as any) as T | null | undefined; -}; +const createType = (type: any) => >(options?: O) => + (({ ...(options ? options : {}), type } as unknown) as GetType< + O, + T extends string ? EnumOrString : T + >); export const Type = { number: createType(Number), - optionalNumber: createOptionalType(Number), boolean: createType(Boolean), - optionalBoolean: createOptionalType(Boolean), date: createType(Date), - optionalDate: createOptionalType(Date), mixed: createType(Schema.Types.Mixed), - optionalMixed: createOptionalType(Schema.Types.Mixed), objectId: createType(Schema.Types.ObjectId), - optionalObjectId: createOptionalType(Schema.Types.ObjectId), - string: >( - options: Omit, 'enum'> & { - enum?: T; - } = {} - ) => { - return ({ - required: true, - ...options, - type: String, - } as unknown) as EnumOrString; - }, - optionalString: >( - options: Omit, 'enum'> & { - enum?: T; - } = {} - ) => { - return ({ - ...options, - type: String, - } as unknown) as EnumOrString | null | undefined; - }, - object: (options: SchemaTypeOpts = {}) => ({ - of(schema: T) { - return ({ - required: true, - ...options, - type: schema, - } as any) as ConvertObject; - }, - }), - optionalObject: (options: SchemaTypeOpts = {}) => ({ - of(schema: T) { + string: createType(String), + decimal128: createType(Schema.Types.Decimal128), + object: >(options?: O) => ({ + of(schema: T) { return ({ - ...options, + ...(options ? options : {}), type: schema, - } as any) as ConvertObject | null | undefined; + } as unknown) as GetType]: Convert[P] }>; }, }), - array: (options: SchemaTypeOpts> = {}) => ({ + array: >>(options?: O) => ({ of(schema: T) { return ({ - required: true, - ...options, + ...(options ? options : {}), type: [schema], - } as any) as ArrayOfElements; + } as unknown) as GetType>>; }, }), - optionalArray: (options: SchemaTypeOpts> = {}) => ({ - of(schema: T) { - return ({ - ...options, - type: [schema], - } as any) as ArrayOfElements | null | undefined; - }, - }), - schema: (options: SchemaTypeOpts = {}) => ({ - of(schema: T) { - return ({ - required: true, - ...options, - type: schema, - } as any) as ExtractSchema; - }, - }), - optionalSchema: (options: SchemaTypeOpts = {}) => ({ - of(schema: T) { + schema: >(options?: O) => ({ + of(schema: T) { return ({ - ...options, + ...(options ? options : {}), type: schema, - } as any) as ExtractSchema | null | undefined; + } as unknown) as GetType & GetSubDocument>; }, }), ref: (schema: T) => ({ - to(name: string, refSchema: TSchema) { + to(name: string, refSchema: TSchema) { return ({ ...(schema as any), ref: name, - } as any) as Extract | T; + } as unknown) as + | T + | (T extends Record + ? Optional + : TSchema[DefinitionField]); }, }), }; diff --git a/src/createSchema.ts b/src/createSchema.ts index d2648a2..b2ffe21 100644 --- a/src/createSchema.ts +++ b/src/createSchema.ts @@ -1,21 +1,14 @@ import { SchemaOptions, Schema } from 'mongoose'; -import { ConvertObject, TypeWithTimestamps } from './types'; +import { GetSchemaType } from './types'; -type SchemaOpts = SchemaOptions & { statics?: { [x: string]: any } }; - -type CreateSchema = ( +type CreateSchema = ( definition?: T, - options?: O + options?: O // TODO: to be fixed ) => Schema & { - definition: ConvertObject>; + definition: { [P in keyof GetSchemaType]: GetSchemaType[P] }; options: O; }; export const createSchema: CreateSchema = (definition?, options?) => { - if (!options) return new Schema(definition, options) as any; - - const { statics, ...opts } = options; - const schema = new Schema(definition, opts) as any; - if (statics) schema.statics = statics; - return schema; + return new Schema(definition, options) as any; }; diff --git a/src/index.ts b/src/index.ts index ea53078..79cb7ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -export * from './createSchema'; -export * from './types'; export * from './Type'; +export * from './createSchema'; export * from './typedModel'; +export * from './types'; diff --git a/src/typedModel.ts b/src/typedModel.ts index 83cb788..5e451f1 100644 --- a/src/typedModel.ts +++ b/src/typedModel.ts @@ -1,11 +1,16 @@ import { Schema, Document, Model, model } from 'mongoose'; -import { Extract, ExtractStatics } from './types'; +import { Extract } from './types'; -export function typedModel( +export function typedModel< + T extends Schema, + S extends { [name: string]: Function } +>( name: string, schema?: T, collection?: string, - skipInit?: boolean -): ExtractStatics>, Document & Extract> { + skipInit?: boolean, + statics?: S & ThisType>> +): Model> & S { + if (schema && statics) schema.statics = statics; return model(name, schema, collection, skipInit) as any; } diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index fdba8cb..0000000 --- a/src/types.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Types, Document } from 'mongoose'; - -export type Extract = T extends { definition: infer U } ? U : never; -export type Omit = Pick>; - -export type EnumOrString< - T extends ReadonlyArray | undefined -> = T extends ReadonlyArray ? T[number] : undefined; - -type ExtractOptions = T extends { options: infer U } ? U : never; -type DisabledIdOption = { _id: false }; -type IsSchemaType = 0 extends (1 & T) - ? NOT - : T extends { definition: any } - ? IS - : NOT; -type SubdocumentsArrayWithoutId = { - [P in keyof Types.DocumentArray]: Omit; -}; - -export type ExtractSchema = Extract & - (ExtractOptions extends DisabledIdOption - ? Omit - : Types.Subdocument); -export type ArrayOfElements = IsSchemaType< - T, - ExtractOptions extends DisabledIdOption - ? SubdocumentsArrayWithoutId & Types.Subdocument> - : Types.DocumentArray & Types.Subdocument>, - Array ->; - -type ExcludeBaseType = Exclude; - -export type ExcludeFromArray = T extends Array - ? Array> - : ExcludeBaseType; - -export type PopulateItem = Omit & - { [x in P]: ExcludeFromArray }; - -export type GetArrayItem = T extends Array ? U : never; - -export type PopulateArray> = Array< - PopulateItem, P> ->; - -export type MaybeItem = T extends Array ? U : T; - -export type Populate = T extends Array - ? P extends keyof U - ? Array> - : T - : P extends keyof T - ? PopulateItem - : T; - -export type ExtractProps = T extends { definition: infer D } ? D : never; -export type ExtractDoc = T extends { definition: infer D } - ? D & Document - : never; - -export type OptionalPropNames = { - [P in keyof T]: null extends T[P] ? P : never; -}[keyof T]; - -export type RequiredPropNames = { - [P in keyof T]: null extends T[P] ? never : P; -}[keyof T]; - -export type OptionalProps = { [P in OptionalPropNames]: T[P] }; -export type RequiredProps = { [P in RequiredPropNames]: T[P] }; - -export type MakeOptional = { [P in keyof T]?: T[P] }; - -export type ConvertObject = { [P in RequiredPropNames]: T[P] } & - { [P in OptionalPropNames]?: T[P] }; - -// timestamp types -type CreatedAtType = { createdAt: Date }; -type UpdatedAtType = { updatedAt: Date }; -type TimestampsPresent = { - timestamps: true; -}; -type TimestampsEachPresent = { - timestamps: { - createdAt: true; - updatedAt: true; - }; -}; -type TimestampCreatedByPresent = { - timestamps: { - createdAt: true; - }; -}; -type TimestampUpdatedByPresent = { - timestamps: { - updatedAt: true; - }; -}; - -export type TypeWithTimestamps = Opts extends ( - | TimestampsPresent - | TimestampsEachPresent) - ? T & CreatedAtType & UpdatedAtType - : Opts extends TimestampCreatedByPresent - ? T & CreatedAtType - : Opts extends TimestampUpdatedByPresent - ? T & UpdatedAtType - : T; - -// statics -export type ModelInstanceType = 'ModelInstanceType'; -export type ModelInstancesType = 'ModelInstancesType'; - -type ArgumentTypes = T extends (...args: infer U) => infer R ? U : never; -type ReplaceReturnType = (...a: ArgumentTypes) => TNewReturn; - -export type ExtractStatics = T extends { options: infer O } - ? O extends { statics: infer S } - ? { - [P in keyof S]: S[P] extends (...args: any) => any - ? ReturnType extends ModelInstanceType - ? ReplaceReturnType> - : ReturnType extends ModelInstancesType - ? ReplaceReturnType> - : S[P] - : S[P]; - } & - Rest - : Rest - : Rest; diff --git a/src/types/_shared.ts b/src/types/_shared.ts new file mode 100644 index 0000000..1499378 --- /dev/null +++ b/src/types/_shared.ts @@ -0,0 +1,45 @@ +import { Types } from 'mongoose'; + +// schema variables +export type Id = '_id'; +export type VerOption = 'versionKey'; +export type VerKey = '__v'; +export type Timestamps = 'timestamps'; +export type CreatedAt = 'createdAt'; +export type UpdatedAt = 'updatedAt'; + +// type option variables +export type Required = 'required'; +export type Select = 'select'; +export type Enum = 'enum'; + +// subDocument +export interface SubDocumentNoId extends Omit {} +export interface SubDocument extends Types.Subdocument {} + +// subDocument array +export interface SubDocumentArray + extends Types.DocumentArray { + filter( + callbackfn: ( + value: T, + index: number, + array: SubDocumentArray + ) => unknown, + thisArg?: any + ): SubDocumentArray; +} +export interface SubDocumentArrayNoId + extends Types.Array { + create(obj: any): T; + inspect(): T[]; + toObject(options?: any): T[]; + filter( + callbackfn: ( + value: T, + index: number, + array: SubDocumentArrayNoId + ) => unknown, + thisArg?: any + ): SubDocumentArrayNoId; +} diff --git a/src/types/extract.ts b/src/types/extract.ts new file mode 100644 index 0000000..f1f2e05 --- /dev/null +++ b/src/types/extract.ts @@ -0,0 +1,53 @@ +import { Document, Types } from 'mongoose'; +import { + SubDocument, + SubDocumentNoId, + SubDocumentArray, + SubDocumentArrayNoId, +} from './_shared'; +import { Convert, Definition, DefinitionField } from './schema'; + +export type Extract = T extends Record + ? Convert + : never; +export type ExtractProps = DeepExtractObjProps< + T[DefinitionField] +>; +export type ExtractFromReq = { [P in keyof T]: DeepExtractFromReq }; +export type ExtractDoc = T[DefinitionField] & Document; + +type DeepExtractProps = T extends ( + | (infer R & SubDocument) + | (infer R & SubDocumentNoId)) + ? R + : T extends ( + | SubDocumentArray + | SubDocumentArrayNoId) + ? Array<{ [P in keyof DeepExtractObjProps]: DeepExtractObjProps[P] }> + : T extends Date + ? Date + : T extends Types.ObjectId + ? Types.ObjectId + : T extends Types.Decimal128 + ? Types.Decimal128 + : T extends {} + ? { [P in keyof DeepExtractObjProps]: DeepExtractObjProps[P] } + : T; + +type DeepExtractObjProps = { [P in keyof T]: DeepExtractProps }; + +type DeepExtractFromReq = 0 extends (1 & T) // any + ? any + : T extends (Date | Types.ObjectId | Types.Decimal128) // date or objectId + ? string + : T extends Array + ? Array< + 0 extends (1 & R) // any + ? any + : R extends (Date | Types.ObjectId) // date or objectId + ? string + : { [P in keyof ExtractFromReq]: ExtractFromReq[P] } + > + : T extends {} + ? { [P in keyof ExtractFromReq]: ExtractFromReq[P] } + : T; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..8c8220f --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,4 @@ +export * from './extract'; +export * from './populate'; +export * from './schema'; +export * from './singleType'; diff --git a/src/types/populate.ts b/src/types/populate.ts new file mode 100644 index 0000000..98f289f --- /dev/null +++ b/src/types/populate.ts @@ -0,0 +1,20 @@ +import { Types } from 'mongoose'; + +type ExcludeBaseType = Exclude; + +type ExcludeFromArray = T extends Array + ? Array> + : ExcludeBaseType; + +type PopulateItem = Omit & + { [x in P]: ExcludeFromArray }; + +export type MaybeItem = T extends Array ? U : T; + +export type Populate = T extends Array + ? P extends keyof U + ? Array> + : T + : P extends keyof T + ? PopulateItem + : T; diff --git a/src/types/schema.ts b/src/types/schema.ts new file mode 100644 index 0000000..7c5d6ec --- /dev/null +++ b/src/types/schema.ts @@ -0,0 +1,105 @@ +import { Types } from 'mongoose'; +import { + CreatedAt, + Id, + Timestamps, + UpdatedAt, + VerKey, + VerOption, +} from './_shared'; + +type IsNever = 0 extends (1 & T) ? NOT : T extends never ? IS : NOT; + +// convert optional/required fields +export type OptionalField = 'notRequired'; +export type Optional = Record; + +type OptionalKeys = { + [K in keyof T]: T[K] extends Record + ? Record extends T[K] + ? K + : never + : never; +}[keyof T]; +type RequiredKeys = { + [K in keyof T]: IsNever< + T[K], + never, + T[K] extends Record + ? Record extends T[K] + ? never + : K + : K + >; +}[keyof T]; + +export type Convert = { [P in RequiredKeys]: T[P] } & + { + [P in OptionalKeys]?: T[P] extends Record + ? Record extends T[P] + ? (O | undefined) + : never + : never; + }; + +// id option +export type OptionIdDisabled = Record; +export type OptionIdAsDefault = Record; + +// version option +type OptionVerDisabled = Record; +type OptionVerString = Record>; + +type OptionVerAsDefault = Record; +/** + * Add numbers! + */ +type OptionVerAsCustom = Record< + Opts[VerOption], + number +>; + +// timestamp option +type TimestampsEnabled = Record; +type TimestampsEnabledEach = TimestampEnabled & + TimestampEnabled; + +type TimestampEnabled = Record>; +type TimestampString = Record< + Timestamps, + Record> +>; + +type TimestampAsDefault = Record; +type TimestampAsCustom< + U extends string, + Opts extends TimestampString +> = Record; + +// get schema by each option +type GetSchemaId = Opts extends OptionIdDisabled ? {} : OptionIdAsDefault; +type GetSchemaVer = Opts extends OptionVerDisabled + ? {} + : Opts extends OptionVerString + ? OptionVerAsCustom + : OptionVerAsDefault; + +type GetSchemaTimestamp = Opts extends ( + | TimestampsEnabled + | TimestampsEnabledEach + | TimestampEnabled) + ? TimestampAsDefault + : Opts extends TimestampString + ? TimestampAsCustom + : {}; + +// definition +export type DefinitionField = 'definition'; +export type Definition = Record; + +// combine options into T +export type GetSchemaType = GetSchemaId & + Convert & + GetSchemaTimestamp & + GetSchemaTimestamp & + GetSchemaVer; diff --git a/src/types/singleType.ts b/src/types/singleType.ts new file mode 100644 index 0000000..0721cd6 --- /dev/null +++ b/src/types/singleType.ts @@ -0,0 +1,78 @@ +import { SchemaTypeOpts, Types } from 'mongoose'; +import { + SubDocument, + SubDocumentArray, + SubDocumentArrayNoId, + SubDocumentNoId, + Required, + Select, +} from './_shared'; +import { + OptionIdDisabled, + Optional, + Convert, + OptionalField, + Definition, +} from './schema'; +import { Extract } from './extract'; + +type Primitives = number | string | boolean | Date | Types.ObjectId; + +type ExtractOptions = T extends { options: infer U } ? U : never; + +type RequiredOpt = Record; +type NoSelectOpt = Record; +type SharedTypeOptions = Partial< + Record & Record +>; +type SharedTypeOptionsWithEnum = SharedTypeOptions & { + enum?: ReadonlyArray; +}; + +export type TypeOptions = (T extends string + ? Omit, keyof SharedTypeOptionsWithEnum> & + SharedTypeOptionsWithEnum + : Omit, keyof SharedTypeOptions>) & + SharedTypeOptions; + +// type: string +export type EnumOrString< + T extends { enum?: ReadonlyArray } | undefined +> = T extends { enum?: infer U } + ? U extends ReadonlyArray + ? U[number] + : string + : string; + +// type: array +type IsSchemaType = 0 extends (1 & T) + ? NOT + : T extends Definition + ? IS + : NOT; +export type ArrayOfElements = IsSchemaType< + T, + ExtractOptions extends OptionIdDisabled + ? SubDocumentArrayNoId & SubDocumentNoId> + : SubDocumentArray & SubDocument>, + Array +>; + +export type ArrElement = T extends Record + ? R extends (Primitives | Array) + ? R + : { [P in keyof Convert]: Convert[P] } + : T extends (Primitives | Array) + ? T + : { [P in keyof Convert]: Convert[P] }; + +// type: schema +export type GetSubDocument = ExtractOptions extends OptionIdDisabled + ? SubDocumentNoId + : SubDocument; + +// get type +export type GetType = + // Opts extends NoSelectOpt + // ? Optional : + Opts extends RequiredOpt ? T : Optional; diff --git a/ts_test/createSchema.tstest.ts b/ts_test/createSchema.tstest.ts new file mode 100644 index 0000000..00d29d8 --- /dev/null +++ b/ts_test/createSchema.tstest.ts @@ -0,0 +1,155 @@ +import { createSchema, typedModel, Type } from '../src'; +import { Types, Document } from 'mongoose'; +import { UpdatedAt, CreatedAt, VerKey, Id } from '../src/types/_shared'; + +type NotPresent = T & Partial>; + +const TimestampsTests = () => { + type CreatedAtObj = Record; + type UpdatedAtObj = Record; + type CreatedAtCustomObj = Record<'customCreatedAt', Date>; + type UpdatedAtCustomObj = Record<'customUpdatedAt', Date>; + function createdAtNotPresent(_: NotPresent) {} + function updatedAtNotPresent(_: NotPresent) {} + function createdAtPresent(_: CreatedAtObj) {} + function updatedAtPresent(_: UpdatedAtObj) {} + function createdAtCustomPresent(_: CreatedAtCustomObj) {} + function updatedAtCustomPresent(_: UpdatedAtCustomObj) {} + + // Timestamps should not be present by default + typedModel('', createSchema({})) + .findOne() + .then(val => val && createdAtNotPresent(val) && updatedAtNotPresent(val)); + + // Timestamps should not be present if set false + typedModel('', createSchema({}, { timestamps: false })) + .findOne() + .then(val => val && createdAtNotPresent(val) && updatedAtNotPresent(val)); + + // Timestamps should be present if timestamps set true + typedModel('', createSchema({}, { timestamps: true })) + .findOne() + .then(val => val && createdAtPresent(val) && updatedAtPresent(val)); + + // Timestamps should be present if both set true + typedModel( + '', + createSchema({}, { timestamps: { createdAt: true, updatedAt: true } }) + ) + .findOne() + .then(val => val && createdAtPresent(val) && updatedAtPresent(val)); + + // CreatedAt should be present if createdAt set true, updatedAt should not be present by default + typedModel('', createSchema({}, { timestamps: { createdAt: true } })) + .findOne() + .then(val => val && createdAtPresent(val) && updatedAtNotPresent(val)); + + // CreatedAt should be present if createdAt set true, updatedAt should not be present if set false + typedModel( + '', + createSchema({}, { timestamps: { createdAt: true, updatedAt: false } }) + ) + .findOne() + .then(val => val && createdAtPresent(val) && updatedAtNotPresent(val)); + + // CreatedAt should not be present if createdAt set false, updatedAt should not be present by default + typedModel('', createSchema({}, { timestamps: { createdAt: false } })) + .findOne() + .then(val => val && createdAtNotPresent(val) && updatedAtNotPresent(val)); + + // CreatedAt should not be present by default, updateAt should be present if updatedAt set true + typedModel('', createSchema({}, { timestamps: { updatedAt: true } })) + .findOne() + .then(val => val && createdAtNotPresent(val) && updatedAtPresent(val)); + + // CreatedAt should not be present by default, updateAt should not be present if updatedAt set false + typedModel('', createSchema({}, { timestamps: { updatedAt: false } })) + .findOne() + .then(val => val && createdAtNotPresent(val) && updatedAtNotPresent(val)); + + // Custom createAt present + typedModel( + '', + createSchema({}, { timestamps: { createdAt: 'customCreatedAt' as const } }) + ) + .findOne() + .then( + val => val && createdAtCustomPresent(val) && updatedAtNotPresent(val) + ); + + // Custom updatedAt present + typedModel( + '', + createSchema({}, { timestamps: { updatedAt: 'customUpdatedAt' as const } }) + ) + .findOne() + .then( + val => val && createdAtNotPresent(val) && updatedAtCustomPresent(val) + ); + + // Custom createdAt and custom updatedAt present + typedModel( + '', + createSchema( + {}, + { + timestamps: { + createdAt: 'customCreatedAt' as const, + updatedAt: 'customUpdatedAt' as const, + }, + } + ) + ) + .findOne() + .then( + val => val && createdAtCustomPresent(val) && updatedAtCustomPresent(val) + ); +}; + +const VersionTests = () => { + type VersionObj = Record; + type VersionAtCustomObj = Record<'customVer', number>; + function VersionNotPresent(_: NotPresent) {} + function VersionPresent(_: VersionObj) {} + function VersionCustomPresent(_: VersionAtCustomObj) {} + + // __v should be present by default + typedModel('', createSchema({})) + .findOne() + .then(val => val && VersionPresent(val)); + + // __v should be present if set true + typedModel('', createSchema({}, { versionKey: true })) + .findOne() + .then(val => val && VersionPresent(val)); + + // __v should not be present if set false + typedModel('', createSchema({}, { versionKey: false })) + .findOne() + .then(val => val && VersionNotPresent(val)); + + // __v should be present with 'customVer' key if set 'customVer' + typedModel('', createSchema({}, { versionKey: 'customVer' as const })) + .findOne() + .then(val => val && VersionCustomPresent(val)); +}; + +const IdTest = () => { + type IdObj = Record; + function IdPresent(_: IdObj) {} + + // _id should be present by default + typedModel('', createSchema({})) + .findOne() + .then(val => val && IdPresent(val)); + + // _id should be present if set true + typedModel('', createSchema({}, { id: true })) + .findOne() + .then(val => val && IdPresent(val)); + + // _id should be present if set false + typedModel('', createSchema({}, { id: false })) + .findOne() + .then(val => val && IdPresent(val)); +}; diff --git a/ts_test/singleTypes/0.string.ts b/ts_test/singleTypes/0.string.ts new file mode 100644 index 0000000..18bb249 --- /dev/null +++ b/ts_test/singleTypes/0.string.ts @@ -0,0 +1,22 @@ +import { createSchema, Type, typedModel } from '../../src'; + +const schema = createSchema({ + a: Type.string(), + b: Type.string({ required: true }), + c: Type.string({ enum: ['a', 'b'] as const }), + d: Type.string({ required: true, enum: ['a', 'b'] as const }), +}); + +typedModel('', schema) + .findOne() + .then(val => { + if (val) { + val.a && onlyString(val.a); + onlyString(val.b); + val.c && onlyAB(val.c); + onlyAB(val.d); + } + }); + +function onlyString(_: string) {} +function onlyAB(_: 'a' | 'b') {} diff --git a/ts_test/singleTypes/1.number.ts b/ts_test/singleTypes/1.number.ts new file mode 100644 index 0000000..5d51395 --- /dev/null +++ b/ts_test/singleTypes/1.number.ts @@ -0,0 +1,17 @@ +import { createSchema, Type, typedModel } from '../../src'; + +const schema = createSchema({ + a: Type.number(), + b: Type.number({ required: true }), +}); + +typedModel('', schema) + .findOne() + .then(val => { + if (val) { + val.a && onlyNumber(val.a); + onlyNumber(val.b); + } + }); + +function onlyNumber(_: number) {} diff --git a/ts_test/singleTypes/2.boolean.ts b/ts_test/singleTypes/2.boolean.ts new file mode 100644 index 0000000..9c6bbe4 --- /dev/null +++ b/ts_test/singleTypes/2.boolean.ts @@ -0,0 +1,17 @@ +import { createSchema, Type, typedModel } from '../../src'; + +const schema = createSchema({ + a: Type.boolean(), + b: Type.boolean({ required: true }), +}); + +typedModel('', schema) + .findOne() + .then(val => { + if (val) { + val.a && onlyBoolean(val.a); + onlyBoolean(val.b); + } + }); + +function onlyBoolean(_: boolean) {} diff --git a/ts_test/singleTypes/3.date.ts b/ts_test/singleTypes/3.date.ts new file mode 100644 index 0000000..96f973d --- /dev/null +++ b/ts_test/singleTypes/3.date.ts @@ -0,0 +1,17 @@ +import { createSchema, Type, typedModel } from '../../src'; + +const schema = createSchema({ + a: Type.date(), + b: Type.date({ required: true }), +}); + +typedModel('', schema) + .findOne() + .then(val => { + if (val) { + val.a && onlyDate(val.a); + onlyDate(val.b); + } + }); + +function onlyDate(_: Date) {} diff --git a/ts_test/singleTypes/4.objectId.ts b/ts_test/singleTypes/4.objectId.ts new file mode 100644 index 0000000..5891376 --- /dev/null +++ b/ts_test/singleTypes/4.objectId.ts @@ -0,0 +1,18 @@ +import { createSchema, Type, typedModel } from '../../src'; +import { Types } from 'mongoose'; + +const schema = createSchema({ + a: Type.objectId(), + b: Type.objectId({ required: true }), +}); + +typedModel('', schema) + .findOne() + .then(val => { + if (val) { + val.a && onlyObjectId(val.a); + onlyObjectId(val.b); + } + }); + +function onlyObjectId(_: Types.ObjectId) {} diff --git a/ts_test/singleTypes/5.mixed.ts b/ts_test/singleTypes/5.mixed.ts new file mode 100644 index 0000000..a68f707 --- /dev/null +++ b/ts_test/singleTypes/5.mixed.ts @@ -0,0 +1,26 @@ +import { createSchema, Type, typedModel } from '../../src'; + +const schema = createSchema({ + a: Type.mixed(), + b: Type.mixed({ required: true }), +}); + +typedModel('', schema) + .findOne() + .then(val => { + if (val) { + val.a && onlyMixed(val.a); + onlyMixed(val.b); + } + }); + +export type NotAny = + | string + | number + | boolean + | Date + | Array + | {} + | object + | Function; +function onlyMixed(_: Exclude) {} diff --git a/ts_test/singleTypes/6.decimal128.ts b/ts_test/singleTypes/6.decimal128.ts new file mode 100644 index 0000000..e48ca74 --- /dev/null +++ b/ts_test/singleTypes/6.decimal128.ts @@ -0,0 +1,18 @@ +import { createSchema, Type, typedModel } from '../../src'; +import { Types } from 'mongoose'; + +const schema = createSchema({ + a: Type.decimal128(), + b: Type.decimal128({ required: true }), +}); + +typedModel('', schema) + .findOne() + .then(val => { + if (val) { + val.a && onlyDecimal128(val.a); + onlyDecimal128(val.b); + } + }); + +function onlyDecimal128(_: Types.Decimal128) {} diff --git a/ts_test/singleTypes/7.schema.ts b/ts_test/singleTypes/7.schema.ts new file mode 100644 index 0000000..50fdac9 --- /dev/null +++ b/ts_test/singleTypes/7.schema.ts @@ -0,0 +1,33 @@ +import { createSchema, Type, typedModel } from '../../src'; +import { Types } from 'mongoose'; + +const SubSchema = createSchema({ + a: Type.number({ required: true }), +}); +const SubSchemaNoId = createSchema( + { + a: Type.boolean({ required: true }), + }, + { _id: false } +); + +const schema = createSchema({ + a: Type.schema().of(SubSchema), + b: Type.schema({ required: true }).of(SubSchema), + c: Type.schema().of(SubSchemaNoId), + d: Type.schema({ required: true }).of(SubSchemaNoId), +}); + +typedModel('', schema) + .findOne() + .then(val => { + if (val) { + val.a && onlySchema(val.a); + onlySchema(val.b); + val.c && onlySchemaNoId(val.c); + onlySchemaNoId(val.d); + } + }); + +function onlySchema(_: { _id: Types.ObjectId; __v: number; a: number }) {} +function onlySchemaNoId(_: { __v: number; a: boolean }) {} diff --git a/ts_test/singleTypes/8.object.ts b/ts_test/singleTypes/8.object.ts new file mode 100644 index 0000000..5000bc0 --- /dev/null +++ b/ts_test/singleTypes/8.object.ts @@ -0,0 +1,94 @@ +import { createSchema, Type, typedModel } from '../../src'; +import { Types } from 'mongoose'; +import { NotAny } from './5.mixed'; + +const str = { a1: Type.string({ required: true }) }; +const num = { a1: Type.number({ required: true }) }; +const bool = { a1: Type.boolean({ required: true }) }; +const date = { a1: Type.date({ required: true }) }; +const objId = { a1: Type.objectId({ required: true }) }; +const mix = { a1: Type.mixed({ required: true }) }; +const obj = { + a1: Type.object({ required: true }).of({ + b1: Type.string({ required: true }), + }), +}; +const arr = { a1: Type.array({ required: true }).of(Type.string()) }; +const subSchema = createSchema({ a1: Type.string() }); + +const schema = createSchema({ + // of string + a: Type.object().of(str), + b: Type.object({ required: true }).of(str), + + // of number + c: Type.object().of(num), + d: Type.object({ required: true }).of(num), + + // of boolean + e: Type.object().of(bool), + f: Type.object({ required: true }).of(bool), + + // of date + g: Type.object().of(date), + h: Type.object({ required: true }).of(date), + + // of objectId + i: Type.object().of(objId), + j: Type.object({ required: true }).of(objId), + + // of mixed + k: Type.object().of(mix), + l: Type.object({ required: true }).of(mix), + + // of object + m: Type.object().of(obj), + n: Type.object({ required: true }).of(obj), + + // of array + o: Type.object().of(arr), + p: Type.object({ required: true }).of(arr), + + // of schema + q: Type.object().of(subSchema), + r: Type.object({ required: true }).of(subSchema), +}); + +typedModel('', schema) + .findOne() + .then(val => { + if (val) { + val.a && onlyA1String(val.a); + onlyA1String(val.b); + + val.c && onlyA1Number(val.c); + onlyA1Number(val.d); + + val.e && onlyA1Boolean(val.e); + onlyA1Boolean(val.f); + + val.g && onlyA1Date(val.g); + onlyA1Date(val.h); + + val.i && onlyA1ObjectId(val.i); + onlyA1ObjectId(val.j); + + val.k && onlyA1Mix(val.k); + onlyA1Mix(val.l); + + val.m && onlyA1Obj(val.m); + onlyA1Obj(val.n); + + val.o && onlyA1Arr(val.o); + onlyA1Arr(val.p); + } + }); + +function onlyA1String(_: { a1: string }) {} +function onlyA1Number(_: { a1: number }) {} +function onlyA1Boolean(_: { a1: boolean }) {} +function onlyA1Date(_: { a1: Date }) {} +function onlyA1ObjectId(_: { a1: Types.ObjectId }) {} +function onlyA1Mix(_: { a1: Exclude }) {} +function onlyA1Obj(_: { a1: { b1: string } }) {} +function onlyA1Arr(_: { a1: string[] }) {} diff --git a/ts_test/singleTypes/9.array.ts b/ts_test/singleTypes/9.array.ts new file mode 100644 index 0000000..a60cd51 --- /dev/null +++ b/ts_test/singleTypes/9.array.ts @@ -0,0 +1,118 @@ +import { createSchema, Type, typedModel } from '../../src'; +import { Types } from 'mongoose'; +import { NotAny } from './5.mixed'; +import { + SubDocumentArray, + SubDocumentArrayNoId, + SubDocument, + SubDocumentNoId, +} from '../../src/types/_shared'; + +const str = Type.string({ required: true }); +const num = Type.number({ required: true }); +const bool = Type.boolean({ required: true }); +const date = Type.date({ required: true }); +const objId = Type.objectId({ required: true }); +const mix = Type.mixed({ required: true }); +const obj = { b1: Type.string({ required: true }) }; +const arr = Type.array({ required: true }).of(Type.string()); +const subSchema = createSchema({ a1: Type.string({ required: true }) }); +const subSchemaNoId = createSchema( + { a1: Type.string({ required: true }) }, + { _id: false } +); + +const schema = createSchema({ + // of string + a: Type.array().of(str), + b: Type.array({ required: true }).of(str), + + // of number + c: Type.array().of(num), + d: Type.array({ required: true }).of(num), + + // of boolean + e: Type.array().of(bool), + f: Type.array({ required: true }).of(bool), + + // of date + g: Type.array().of(date), + h: Type.array({ required: true }).of(date), + + // of arrayId + i: Type.array().of(objId), + j: Type.array({ required: true }).of(objId), + + // of mixed + k: Type.array().of(mix), + l: Type.array({ required: true }).of(mix), + + // of array + m: Type.array().of(obj), + n: Type.array({ required: true }).of(obj), + + // of array + o: Type.array().of(arr), + p: Type.array({ required: true }).of(arr), + + // of schema with id + q: Type.array().of(subSchema), + r: Type.array({ required: true }).of(subSchema), + + // of schema with id + s: Type.array().of(subSchemaNoId), + t: Type.array({ required: true }).of(subSchemaNoId), +}); + +typedModel('', schema) + .findOne() + .then(val => { + if (val) { + val.a && onlyArrOfString(val.a); + onlyArrOfString(val.b); + + val.c && onlyArrOfNumber(val.c); + onlyArrOfNumber(val.d); + + val.e && onlyArrOfBoolean(val.e); + onlyArrOfBoolean(val.f); + + val.g && onlyArrOfDate(val.g); + onlyArrOfDate(val.h); + + val.i && onlyArrOfObjectId(val.i); + onlyArrOfObjectId(val.j); + + val.k && onlyArrOfMix(val.k); + onlyArrOfMix(val.l); + + val.m && onlyArrOfObj(val.m); + onlyArrOfObj(val.n); + + val.o && onlyArrOfArr(val.o); + onlyArrOfArr(val.p); + + val.q && onlyArrOfSchema(val.q); + onlyArrOfSchema(val.r); + + val.s && onlyArrOfSchemaNoId(val.s); + onlyArrOfSchemaNoId(val.t); + } + }); + +function onlyArrOfString(_: string[]) {} +function onlyArrOfNumber(_: number[]) {} +function onlyArrOfBoolean(_: boolean[]) {} +function onlyArrOfDate(_: Date[]) {} +function onlyArrOfObjectId(_: Types.ObjectId[]) {} +function onlyArrOfMix(_: Exclude[]) {} +function onlyArrOfObj(_: { b1: string }[]) {} +function onlyArrOfArr(_: Array>) {} +function onlyArrOfSchema( + _: SubDocumentArray< + { _id: Types.ObjectId; __v: number; a1: string } & SubDocument + > +) {} +function onlyArrOfSchemaNoId( + _: SubDocumentArrayNoId<{ __v: number; a1: string } & SubDocumentNoId> +) {} diff --git a/ts_test/singleTypes/a.ref.ts b/ts_test/singleTypes/a.ref.ts new file mode 100644 index 0000000..cebba01 --- /dev/null +++ b/ts_test/singleTypes/a.ref.ts @@ -0,0 +1,37 @@ +import { createSchema, Type, typedModel } from '../../src'; +import { Types } from 'mongoose'; + +const SubSchema = createSchema({ + a: Type.number({ required: true }), +}); +const SubSchemaNoId = createSchema( + { + a: Type.boolean({ required: true }), + }, + { _id: false } +); + +const schema = createSchema({ + a: Type.ref(Type.objectId()).to('ref', SubSchema), + b: Type.ref(Type.objectId({ required: true })).to('ref', SubSchema), + c: Type.ref(Type.objectId()).to('ref', SubSchemaNoId), + d: Type.ref(Type.objectId({ required: true })).to('ref', SubSchemaNoId), +}); + +typedModel('', schema) + .findOne() + .then(val => { + if (val) { + val.a && objectIdOrSchema(val.a); + objectIdOrSchema(val.b); + val.c && objectIdOrSchemaNoId(val.c); + objectIdOrSchemaNoId(val.d); + } + }); + +function objectIdOrSchema( + _: Types.ObjectId | { _id: Types.ObjectId; __v: number; a: number } +) {} +function objectIdOrSchemaNoId( + _: Types.ObjectId | { __v: number; a: boolean } +) {} diff --git a/tsconfig.json b/tsconfig.json index 14050df..fe78fdf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -60,5 +60,5 @@ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ }, - "include": ["src"] + "include": ["src", "example", "ts_test"] }