diff --git a/docs/tutorials/mongoose.md b/docs/tutorials/mongoose.md index 51769d23eca..337608e5d95 100644 --- a/docs/tutorials/mongoose.md +++ b/docs/tutorials/mongoose.md @@ -177,8 +177,19 @@ The same rules for Circular References apply (**See above**); This works by having a field with the referenced object model's name and a field with the referenced field. + + + <<< @/tutorials/snippets/mongoose/dynamic-references.ts + + +<<< @/tutorials/snippets/mongoose/dynamic-references.json + + + + ### Decimal Numbers `@tsed/mongoose` supports `mongoose` 128-bit decimal floating points data type [Decimal128](https://mongoosejs.com/docs/api.html#mongoose_Mongoose-Decimal128). diff --git a/docs/tutorials/snippets/mongoose/dynamic-references.json b/docs/tutorials/snippets/mongoose/dynamic-references.json new file mode 100644 index 00000000000..af72223256c --- /dev/null +++ b/docs/tutorials/snippets/mongoose/dynamic-references.json @@ -0,0 +1,66 @@ +{ + "definitions": { + "ClickedLinkEventModel": { + "properties": { + "id": { + "description": "Mongoose ObjectId", + "examples": ["5ce7ad3028890bd71749d477"], + "pattern": "^[0-9a-fA-F]{24}$", + "type": "string" + }, + "url": { + "minLength": 1, + "type": "string" + } + }, + "required": ["url"], + "type": "object" + }, + "SignedUpEventModel": { + "properties": { + "id": { + "description": "Mongoose ObjectId", + "examples": ["5ce7ad3028890bd71749d477"], + "pattern": "^[0-9a-fA-F]{24}$", + "type": "string" + }, + "user": { + "minLength": 1, + "type": "string" + } + }, + "required": ["user"], + "type": "object" + } + }, + "properties": { + "event": { + "description": "Mongoose Ref ObjectId", + "examples": ["5ce7ad3028890bd71749d477"], + "oneOf": [ + { + "description": "Mongoose Ref ObjectId", + "examples": ["5ce7ad3028890bd71749d477"], + "type": "string" + }, + { + "$ref": "#/definitions/ClickedLinkEventModel" + }, + { + "$ref": "#/definitions/SignedUpEventModel" + } + ] + }, + "eventType": { + "enum": ["ClickedLinkEventModel", "SignedUpEventModel"], + "type": "string" + }, + "id": { + "description": "Mongoose ObjectId", + "examples": ["5ce7ad3028890bd71749d477"], + "pattern": "^[0-9a-fA-F]{24}$", + "type": "string" + } + }, + "type": "object" +} diff --git a/docs/tutorials/snippets/mongoose/dynamic-references.ts b/docs/tutorials/snippets/mongoose/dynamic-references.ts index 3925babcb38..6e3fe592aca 100644 --- a/docs/tutorials/snippets/mongoose/dynamic-references.ts +++ b/docs/tutorials/snippets/mongoose/dynamic-references.ts @@ -1,13 +1,32 @@ -import {DynamicRef, Model, Ref} from "@tsed/mongoose"; -import {Enum} from "@tsed/schema"; -import {ModelA} from "./modelA"; -import {ModelB} from "./ModelB"; +import {DynamicRef, Model, ObjectID} from "@tsed/mongoose"; +import {Enum, Required} from "@tsed/schema"; @Model() -export class MyModel { - @DynamicRef("type") - dynamicRef: Ref; +class ClickedLinkEventModel { + @ObjectID("id") + _id: string; - @Enum("Mode lA", "ModelB") - type: string; // This field has to match the referenced model's name + @Required() + url: string; +} + +@Model() +class SignedUpEventModel { + @ObjectID("id") + _id: string; + + @Required() + user: string; +} + +@Model() +class EventModel { + @ObjectID("id") + _id: string; + + @DynamicRef("eventType", ClickedLinkEventModel, SignedUpEventModel) + event: DynamicRef; + + @Enum("ClickedLinkEventModel", "SignedUpEventModel") + eventType: "ClickedLinkEventModel" | "SignedUpEventModel"; } diff --git a/packages/orm/mongoose/jest.config.js b/packages/orm/mongoose/jest.config.js index d0e3a66bf33..922aa8534e1 100644 --- a/packages/orm/mongoose/jest.config.js +++ b/packages/orm/mongoose/jest.config.js @@ -5,7 +5,7 @@ module.exports = { ...require("@tsed/jest-config")(__dirname, "mongoose"), coverageThreshold: { global: { - branches: 94.67, + branches: 93.63, functions: 98.9, lines: 98.9, statements: 98.93 diff --git a/packages/orm/mongoose/src/decorators/dynamicRef.spec.ts b/packages/orm/mongoose/src/decorators/dynamicRef.spec.ts index 3053748064a..8715ae13ea3 100644 --- a/packages/orm/mongoose/src/decorators/dynamicRef.spec.ts +++ b/packages/orm/mongoose/src/decorators/dynamicRef.spec.ts @@ -23,7 +23,13 @@ describe("@DynamicRef()", () => { test: { description: "Mongoose Ref ObjectId", examples: ["5ce7ad3028890bd71749d477"], - type: "string" + oneOf: [ + { + description: "Mongoose Ref ObjectId", + examples: ["5ce7ad3028890bd71749d477"], + type: "string" + } + ] } }, type: "object" diff --git a/packages/orm/mongoose/src/decorators/dynamicRef.ts b/packages/orm/mongoose/src/decorators/dynamicRef.ts index 6acaea4b8de..2855b698824 100644 --- a/packages/orm/mongoose/src/decorators/dynamicRef.ts +++ b/packages/orm/mongoose/src/decorators/dynamicRef.ts @@ -1,7 +1,17 @@ -import {StoreMerge, useDecorators} from "@tsed/core"; -import {Description, Example, Property} from "@tsed/schema"; +import {classOf, isArrowFn, isString, StoreMerge, Type, useDecorators} from "@tsed/core"; +import {Description, Example, JsonHookContext, OneOf, Property, string} from "@tsed/schema"; import {Schema as MongooseSchema} from "mongoose"; import {MONGOOSE_SCHEMA} from "../constants/constants"; +import {deserialize, OnDeserialize, OnSerialize, serialize} from "@tsed/json-mapper"; +import {MongooseModels} from "../registries/MongooseModels"; + +function isRef(value: undefined | string | any) { + return (value && value._bsontype) || isString(value); +} + +function getType(refPath: string, ctx: JsonHookContext) { + return (ctx?.self[refPath] && MongooseModels.get(ctx.self[refPath] as string)) || Object; +} /** * Define a property as mongoose reference to other Model (decorated with @Model). @@ -28,21 +38,37 @@ import {MONGOOSE_SCHEMA} from "../constants/constants"; * } * ``` * - * @param refPath + * @param refPath {String} the path to apply the correct model + * @param types {Type} the classes to generate the correct json schema * @returns {Function} * @decorator * @mongoose * @property */ -export function DynamicRef(refPath: string): PropertyDecorator { +export function DynamicRef(refPath: string, ...types: Type[]): PropertyDecorator { return useDecorators( - Property(String), + Property(Object), Example("5ce7ad3028890bd71749d477"), Description("Mongoose Ref ObjectId"), StoreMerge(MONGOOSE_SCHEMA, { type: MongooseSchema.Types.ObjectId, refPath - }) + }), + OnDeserialize((value, ctx) => { + if (isRef(value)) { + return value.toString(); + } + + return deserialize(value, {...ctx, type: getType(refPath, ctx)}); + }), + OnSerialize((value: any, ctx) => { + if (isRef(value)) { + return value.toString(); + } + + return serialize(value, {...ctx, type: getType(refPath, ctx)}); + }), + OneOf(string().example("5ce7ad3028890bd71749d477").description("Mongoose Ref ObjectId"), ...types) ) as PropertyDecorator; } diff --git a/packages/orm/mongoose/test/dynamicRef.integration.spec.ts b/packages/orm/mongoose/test/dynamicRef.integration.spec.ts new file mode 100644 index 00000000000..b4be5c3471b --- /dev/null +++ b/packages/orm/mongoose/test/dynamicRef.integration.spec.ts @@ -0,0 +1,168 @@ +import {Enum, getJsonSchema, Required} from "@tsed/schema"; +import {TestMongooseContext} from "@tsed/testing-mongoose"; +import {deserialize, serialize} from "@tsed/json-mapper"; +import {Server} from "./helpers/Server"; +import {DynamicRef, MongooseModel, ObjectID} from "../src"; +import {Model} from "../src/decorators/model"; + +describe("DynamicRef Integration", () => { + @Model() + class ClickedLinkEventModel { + @ObjectID("id") + _id: string; + + @Required() + url: string; + } + + @Model() + class SignedUpEventModel { + @ObjectID("id") + _id: string; + + @Required() + user: string; + } + + @Model() + class EventModel { + @ObjectID("id") + _id: string; + + @DynamicRef("eventType", ClickedLinkEventModel, SignedUpEventModel) + event: DynamicRef; + + @Enum("ClickedLinkEventModel", "SignedUpEventModel") + eventType: "ClickedLinkEventModel" | "SignedUpEventModel"; + } + + beforeEach(TestMongooseContext.bootstrap(Server)); + afterEach(TestMongooseContext.clearDatabase); + afterEach(TestMongooseContext.reset); + + describe("JsonSchema", () => { + it("should return the json schema", () => { + expect(getJsonSchema(EventModel)).toEqual({ + definitions: { + ClickedLinkEventModel: { + properties: { + id: { + description: "Mongoose ObjectId", + examples: ["5ce7ad3028890bd71749d477"], + pattern: "^[0-9a-fA-F]{24}$", + type: "string" + }, + url: { + minLength: 1, + type: "string" + } + }, + required: ["url"], + type: "object" + }, + SignedUpEventModel: { + properties: { + id: { + description: "Mongoose ObjectId", + examples: ["5ce7ad3028890bd71749d477"], + pattern: "^[0-9a-fA-F]{24}$", + type: "string" + }, + user: { + minLength: 1, + type: "string" + } + }, + required: ["user"], + type: "object" + } + }, + properties: { + event: { + description: "Mongoose Ref ObjectId", + examples: ["5ce7ad3028890bd71749d477"], + oneOf: [ + { + description: "Mongoose Ref ObjectId", + examples: ["5ce7ad3028890bd71749d477"], + type: "string" + }, + { + $ref: "#/definitions/ClickedLinkEventModel" + }, + { + $ref: "#/definitions/SignedUpEventModel" + } + ] + }, + eventType: { + enum: ["ClickedLinkEventModel", "SignedUpEventModel"], + type: "string" + }, + id: { + description: "Mongoose ObjectId", + examples: ["5ce7ad3028890bd71749d477"], + pattern: "^[0-9a-fA-F]{24}$", + type: "string" + } + }, + type: "object" + }); + }); + }); + + describe("serialize()", () => { + it("should serialize (clickedEvent)", async () => { + const EventRepository = TestMongooseContext.get>(EventModel); + const ClickedEventRepository = TestMongooseContext.get>(ClickedLinkEventModel); + + const clickedEvent = await new ClickedEventRepository({url: "https://www.tsed.io"}).save(); + const event1 = await new EventRepository({eventType: "ClickedLinkEventModel", event: clickedEvent}).save(); + + const result1 = serialize(event1, {type: EventModel}); + + expect(result1).toEqual({ + id: event1.id, + eventType: "ClickedLinkEventModel", + event: {id: clickedEvent.id, url: "https://www.tsed.io"} + }); + }); + it("should serialize (SignedUpEventModel)", async () => { + const EventRepository = TestMongooseContext.get>(EventModel); + const SignedUpEventRepository = TestMongooseContext.get>(SignedUpEventModel); + + const signedUpEvent = await new SignedUpEventRepository({user: "test"}).save(); + const event = await new EventRepository({eventType: "SignedUpEventModel", event: signedUpEvent}).save(); + + const result = serialize(event, {type: EventModel}); + + expect(result).toStrictEqual({ + id: event.id, + eventType: "SignedUpEventModel", + event: {id: signedUpEvent.id, user: "test"} + }); + }); + }); + describe("deserialize()", () => { + it("should serialize (clickedEvent)", async () => { + const result = deserialize( + { + id: "6229a38b4e23ac98aad216d6", + eventType: "ClickedLinkEventModel", + event: {id: "6229a38b4e23ac98aad216d5", url: "https://www.tsed.io"} + }, + {type: EventModel} + ); + + expect(result).toEqual({ + _id: "6229a38b4e23ac98aad216d6", + event: { + _id: "6229a38b4e23ac98aad216d5", + url: "https://www.tsed.io" + }, + eventType: "ClickedLinkEventModel" + }); + expect(result.event).toBeInstanceOf(ClickedLinkEventModel); + }); + }); +}); diff --git a/packages/specs/json-mapper/src/utils/deserialize.ts b/packages/specs/json-mapper/src/utils/deserialize.ts index 106d1ae438f..c63d50d958d 100644 --- a/packages/specs/json-mapper/src/utils/deserialize.ts +++ b/packages/specs/json-mapper/src/utils/deserialize.ts @@ -137,6 +137,7 @@ export function plainObjectToClass(src: any, options: JsonDeserializerO value = deserialize(value, { ...itemOptions, + self: src, type: value === src[key] ? itemOptions.type : undefined, collectionType: propStore.collectionType }); diff --git a/packages/specs/json-mapper/src/utils/serialize.ts b/packages/specs/json-mapper/src/utils/serialize.ts index 98a186ea064..cdbc0706c83 100644 --- a/packages/specs/json-mapper/src/utils/serialize.ts +++ b/packages/specs/json-mapper/src/utils/serialize.ts @@ -85,6 +85,7 @@ export function classToPlainObject(obj: any, options: JsonSerializerOptions