Skip to content

Commit

Permalink
fix(mongoose): fix dynamicRef and improve deserialization support
Browse files Browse the repository at this point in the history
Closes: #1767
  • Loading branch information
Romakita committed Mar 10, 2022
1 parent 3b7a421 commit 63566e4
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 17 deletions.
11 changes: 11 additions & 0 deletions docs/tutorials/mongoose.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<Tabs class="-code">
<Tabs label="Example">

<<< @/tutorials/snippets/mongoose/dynamic-references.ts

</Tab
<Tab label="JsonSchema">

<<< @/tutorials/snippets/mongoose/dynamic-references.json

</Tab>
</Tabs>

### Decimal Numbers

`@tsed/mongoose` supports `mongoose` 128-bit decimal floating points data type [Decimal128](https://mongoosejs.com/docs/api.html#mongoose_Mongoose-Decimal128).
Expand Down
66 changes: 66 additions & 0 deletions docs/tutorials/snippets/mongoose/dynamic-references.json
Original file line number Diff line number Diff line change
@@ -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"
}
37 changes: 28 additions & 9 deletions docs/tutorials/snippets/mongoose/dynamic-references.ts
Original file line number Diff line number Diff line change
@@ -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<ModelA | ModelB>;
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<ClickedLinkEventModel | SignedUpEventModel>;

@Enum("ClickedLinkEventModel", "SignedUpEventModel")
eventType: "ClickedLinkEventModel" | "SignedUpEventModel";
}
2 changes: 1 addition & 1 deletion packages/orm/mongoose/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion packages/orm/mongoose/src/decorators/dynamicRef.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
38 changes: 32 additions & 6 deletions packages/orm/mongoose/src/decorators/dynamicRef.ts
Original file line number Diff line number Diff line change
@@ -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).
Expand All @@ -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<any>[]): 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;
}

Expand Down
168 changes: 168 additions & 0 deletions packages/orm/mongoose/test/dynamicRef.integration.spec.ts
Original file line number Diff line number Diff line change
@@ -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<ClickedLinkEventModel | SignedUpEventModel>;

@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<MongooseModel<EventModel>>(EventModel);
const ClickedEventRepository = TestMongooseContext.get<MongooseModel<ClickedLinkEventModel>>(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<MongooseModel<EventModel>>(EventModel);
const SignedUpEventRepository = TestMongooseContext.get<MongooseModel<SignedUpEventModel>>(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<EventModel>(
{
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);
});
});
});
1 change: 1 addition & 0 deletions packages/specs/json-mapper/src/utils/deserialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export function plainObjectToClass<T = any>(src: any, options: JsonDeserializerO

value = deserialize(value, {
...itemOptions,
self: src,
type: value === src[key] ? itemOptions.type : undefined,
collectionType: propStore.collectionType
});
Expand Down
1 change: 1 addition & 0 deletions packages/specs/json-mapper/src/utils/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export function classToPlainObject(obj: any, options: JsonSerializerOptions<any,
let value = alterValue(schema, obj[key], {useAlias, ...props, self: obj});
value = serialize(value, {
useAlias,
self: obj,
type: value === obj[key] ? getType(propStore, value) : undefined,
collectionType: propStore.collectionType,
...props
Expand Down

0 comments on commit 63566e4

Please sign in to comment.