diff --git a/packages/core/types/src/dml/index.ts b/packages/core/types/src/dml/index.ts index 1c46dc7a30..05d29e9708 100644 --- a/packages/core/types/src/dml/index.ts +++ b/packages/core/types/src/dml/index.ts @@ -97,17 +97,36 @@ export interface EntityConstructor extends Function { new (): Props } +/** + * From a IDmlEntity, infer the foreign keys name and type for belongsTo relation meaning hasOne and ManyToOne + */ +export type InferForeignKeys = T extends IDmlEntity + ? { + [K in keyof Schema as Schema[K] extends RelationshipType + ? Schema[K]["type"] extends "belongsTo" + ? `${K & string}_id` + : K + : K]: Schema[K] extends RelationshipType + ? Schema[K]["type"] extends "belongsTo" + ? string + : Schema[K] + : Schema[K] + } + : never + /** * Helper to infer the schema type of a DmlEntity */ export type Infer = T extends IDmlEntity - ? EntityConstructor<{ - [K in keyof Schema]: Schema[K]["$dataType"] extends () => infer R - ? Infer - : Schema[K]["$dataType"] extends (() => infer R) | null - ? Infer | null - : Schema[K]["$dataType"] - }> + ? EntityConstructor< + { + [K in keyof Schema]: Schema[K]["$dataType"] extends () => infer R + ? Infer + : Schema[K]["$dataType"] extends (() => infer R) | null + ? Infer | null + : Schema[K]["$dataType"] + } & InferForeignKeys + > : never /** @@ -133,7 +152,7 @@ export type EntityCascades = { } /** - * Helper to infer the instance type of a DmlEntity once converted as an Entity + * Helper to infer the instance type of a IDmlEntity once converted as an Entity */ export type InferTypeOf> = InstanceType> diff --git a/packages/core/utils/src/common/file-system.ts b/packages/core/utils/src/common/file-system.ts index 6a83ba0643..48742be6aa 100644 --- a/packages/core/utils/src/common/file-system.ts +++ b/packages/core/utils/src/common/file-system.ts @@ -1,12 +1,12 @@ import { dirname, join } from "path" import { - promises, constants, type Dirent, + type MakeDirectoryOptions, + promises, type RmOptions, type StatOptions, type WriteFileOptions, - type MakeDirectoryOptions, } from "fs" const { rm, stat, mkdir, access, readdir, readFile, writeFile } = promises @@ -31,7 +31,7 @@ export class FileSystem { * Cleanup directory */ async cleanup(options?: RmOptions) { - return rm(this.basePath, { + return await rm(this.basePath, { recursive: true, maxRetries: 10, force: true, @@ -61,7 +61,7 @@ export class FileSystem { * Remove a file */ async remove(filePath: string, options?: RmOptions) { - return rm(this.makePath(filePath), { + return await rm(this.makePath(filePath), { recursive: true, force: true, maxRetries: 2, @@ -103,7 +103,7 @@ export class FileSystem { * Returns file contents */ async contents(filePath: string) { - return readFile(this.makePath(filePath), "utf-8") + return await readFile(this.makePath(filePath), "utf-8") } /** @@ -139,14 +139,14 @@ export class FileSystem { async createJson(filePath: string, contents: any, options?: JSONFileOptions) { if (options && typeof options === "object") { const { replacer, spaces, ...rest } = options - return this.create( + return await this.create( filePath, JSON.stringify(contents, replacer, spaces), rest ) } - return this.create(filePath, JSON.stringify(contents), options) + return await this.create(filePath, JSON.stringify(contents), options) } /** diff --git a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts index c98d12dd5d..920a007c33 100644 --- a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts +++ b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts @@ -1,12 +1,13 @@ +import { EntityConstructor } from "@medusajs/types" import { MetadataStorage } from "@mikro-orm/core" import { expectTypeOf } from "expect-type" import { DmlEntity } from "../entity" -import { EntityBuilder } from "../entity-builder" +import { model } from "../entity-builder" import { createMikrORMEntity, + toMikroOrmEntities, toMikroORMEntity, } from "../helpers/create-mikro-orm-entity" -import { EntityConstructor } from "../types" describe("Entity builder", () => { beforeEach(() => { @@ -15,7 +16,6 @@ describe("Entity builder", () => { describe("Entity builder | properties", () => { test("should identify a DML entity correctly", () => { - const model = new EntityBuilder() const user = model.define("user", { id: model.number(), username: model.text(), @@ -30,7 +30,6 @@ describe("Entity builder", () => { }) test("define an entity", () => { - const model = new EntityBuilder() const user = model.define("user", { id: model.number(), username: model.text(), @@ -38,8 +37,7 @@ describe("Entity builder", () => { spend_limit: model.bigNumber(), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: number @@ -148,7 +146,6 @@ describe("Entity builder", () => { }) test("define a property with default value", () => { - const model = new EntityBuilder() const user = model.define("user", { id: model.number(), username: model.text().default("foo"), @@ -156,8 +153,7 @@ describe("Entity builder", () => { spend_limit: model.bigNumber().default(500.4), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: number username: string @@ -263,7 +259,6 @@ describe("Entity builder", () => { }) test("mark property nullable", () => { - const model = new EntityBuilder() const user = model.define("user", { id: model.number(), username: model.text().nullable(), @@ -398,7 +393,6 @@ describe("Entity builder", () => { }) test("define an entity with enum property", () => { - const model = new EntityBuilder() const user = model.define("user", { id: model.number(), username: model.text(), @@ -406,8 +400,7 @@ describe("Entity builder", () => { role: model.enum(["moderator", "admin", "guest"]), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: number username: string @@ -507,7 +500,6 @@ describe("Entity builder", () => { }) test("define enum property with default value", () => { - const model = new EntityBuilder() const user = model.define("user", { id: model.number(), username: model.text(), @@ -515,8 +507,7 @@ describe("Entity builder", () => { role: model.enum(["moderator", "admin", "guest"]).default("guest"), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: number username: string @@ -616,7 +607,6 @@ describe("Entity builder", () => { }) test("mark enum property nullable", () => { - const model = new EntityBuilder() const user = model.define("user", { id: model.number(), username: model.text(), @@ -624,8 +614,7 @@ describe("Entity builder", () => { role: model.enum(["moderator", "admin", "guest"]).nullable(), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: number username: string @@ -730,7 +719,6 @@ describe("Entity builder", () => { }) test("disallow defining created_at and updated_at timestamps", () => { - const model = new EntityBuilder() expect(() => model.define("user", { id: model.number(), @@ -745,7 +733,6 @@ describe("Entity builder", () => { }) test("disallow defining deleted_at timestamp", () => { - const model = new EntityBuilder() expect(() => model.define("user", { id: model.number(), @@ -759,15 +746,13 @@ describe("Entity builder", () => { }) test("define pg schema name in the entity name", () => { - const model = new EntityBuilder() const user = model.define("public.user", { id: model.number(), username: model.text(), email: model.text(), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: number username: string @@ -857,15 +842,13 @@ describe("Entity builder", () => { describe("Entity builder | id", () => { test("define an entity with id property", () => { - const model = new EntityBuilder() const user = model.define("user", { id: model.id(), username: model.text(), email: model.text(), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: string username: string @@ -956,15 +939,13 @@ describe("Entity builder", () => { }) test("mark id as non-primary", () => { - const model = new EntityBuilder() const user = model.define("user", { id: model.id({ primaryKey: false }), username: model.text(), email: model.text(), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: string username: string @@ -1061,15 +1042,13 @@ describe("Entity builder", () => { }) test("define prefix for the id", () => { - const model = new EntityBuilder() const user = model.define("user", { id: model.id({ primaryKey: false, prefix: "us" }), username: model.text(), email: model.text(), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: string username: string @@ -1168,7 +1147,6 @@ describe("Entity builder", () => { describe("Entity builder | primaryKey", () => { test("should create both id fields and primaryKey fields", () => { - const model = new EntityBuilder() const user = model.define("user", { id: model.id({ primaryKey: false }), email: model.text().primaryKey(), @@ -1271,15 +1249,13 @@ describe("Entity builder", () => { describe("Entity builder | indexes", () => { test("define index on a field", () => { - const model = new EntityBuilder() const user = model.define("user", { id: model.number().index(), username: model.text(), email: model.text().unique(), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: number username: string @@ -1377,15 +1353,13 @@ describe("Entity builder", () => { }) test("define index when entity is using an explicit pg Schema", () => { - const model = new EntityBuilder() const user = model.define("platform.user", { id: model.number().index(), username: model.text(), email: model.text().unique(), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: number username: string @@ -1486,7 +1460,6 @@ describe("Entity builder", () => { describe("Entity builder | hasOne", () => { test("define hasOne relationship", () => { - const model = new EntityBuilder() const email = model.define("email", { email: model.text(), isVerified: model.boolean(), @@ -1498,8 +1471,7 @@ describe("Entity builder", () => { email: model.hasOne(() => email), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: number username: string @@ -1576,7 +1548,6 @@ describe("Entity builder", () => { }) test("mark hasOne relationship as nullable", () => { - const model = new EntityBuilder() const email = model.define("email", { email: model.text(), isVerified: model.boolean(), @@ -1588,8 +1559,7 @@ describe("Entity builder", () => { emails: model.hasOne(() => email).nullable(), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: number @@ -1667,7 +1637,6 @@ describe("Entity builder", () => { }) test("define custom mappedBy key for relationship", () => { - const model = new EntityBuilder() const email = model.define("email", { email: model.text(), isVerified: model.boolean(), @@ -1679,8 +1648,7 @@ describe("Entity builder", () => { email: model.hasOne(() => email, { mappedBy: "owner" }), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: number username: string @@ -1752,7 +1720,6 @@ describe("Entity builder", () => { }) test("define delete cascades for the entity", () => { - const model = new EntityBuilder() const email = model.define("email", { email: model.text(), isVerified: model.boolean(), @@ -1768,8 +1735,7 @@ describe("Entity builder", () => { delete: ["email"], }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: number username: string @@ -1840,7 +1806,7 @@ describe("Entity builder", () => { }, }) - const Email = entityBuilder(email) + const Email = toMikroORMEntity(email) const emailMetaData = MetadataStorage.getMetadataFromDecorator(Email) expect(emailMetaData.className).toEqual("Email") expect(emailMetaData.path).toEqual("Email") @@ -1899,7 +1865,6 @@ describe("Entity builder", () => { }) test("define delete cascades with belongsTo on the other end", () => { - const model = new EntityBuilder() const email = model.define("email", { email: model.text(), isVerified: model.boolean(), @@ -1916,8 +1881,7 @@ describe("Entity builder", () => { delete: ["email"], }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: number username: string @@ -1995,7 +1959,7 @@ describe("Entity builder", () => { }, }) - const Email = entityBuilder(email) + const Email = toMikroORMEntity(email) const emailMetaData = MetadataStorage.getMetadataFromDecorator(Email) expect(emailMetaData.className).toEqual("Email") expect(emailMetaData.path).toEqual("Email") @@ -2027,6 +1991,15 @@ describe("Entity builder", () => { owner: true, reference: "1:1", }, + user_id: { + columnType: "text", + getter: false, + name: "user_id", + nullable: false, + reference: "scalar", + setter: false, + type: "string", + }, created_at: { reference: "scalar", type: "date", @@ -2065,7 +2038,6 @@ describe("Entity builder", () => { describe("Entity builder | hasMany", () => { test("define hasMany relationship", () => { - const model = new EntityBuilder() const email = model.define("email", { email: model.text(), isVerified: model.boolean(), @@ -2077,8 +2049,7 @@ describe("Entity builder", () => { emails: model.hasMany(() => email), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: number username: string @@ -2150,7 +2121,6 @@ describe("Entity builder", () => { }) test("define custom mappedBy property name for hasMany relationship", () => { - const model = new EntityBuilder() const email = model.define("email", { email: model.text(), isVerified: model.boolean(), @@ -2164,8 +2134,7 @@ describe("Entity builder", () => { }), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: number @@ -2238,7 +2207,6 @@ describe("Entity builder", () => { }) test("define delete cascades for the entity", () => { - const model = new EntityBuilder() const email = model.define("email", { email: model.text(), isVerified: model.boolean(), @@ -2254,8 +2222,7 @@ describe("Entity builder", () => { delete: ["emails"], }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) + const User = toMikroORMEntity(user) expectTypeOf(new User()).toMatchTypeOf<{ id: number username: string @@ -2328,7 +2295,6 @@ describe("Entity builder", () => { }) test("define delete cascades with belongsTo on the other end", () => { - const model = new EntityBuilder() const email = model.define("email", { email: model.text(), isVerified: model.boolean(), @@ -2345,9 +2311,8 @@ describe("Entity builder", () => { delete: ["emails"], }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) - const Email = entityBuilder(email) + const User = toMikroORMEntity(user) + const Email = toMikroORMEntity(email) expectTypeOf(new User()).toMatchTypeOf<{ id: number username: string @@ -2441,10 +2406,11 @@ describe("Entity builder", () => { setter: false, }, user: { - reference: "m:1", - name: "user", entity: "User", + name: "user", + nullable: false, persist: false, + reference: "m:1", }, user_id: { columnType: "text", @@ -2453,8 +2419,8 @@ describe("Entity builder", () => { mapToPk: true, name: "user_id", nullable: false, - reference: "m:1", onDelete: "cascade", + reference: "m:1", }, created_at: { reference: "scalar", @@ -2494,8 +2460,6 @@ describe("Entity builder", () => { describe("Entity builder | belongsTo", () => { test("define belongsTo relationship with hasOne", () => { - const model = new EntityBuilder() - const email = model.define("email", { email: model.text(), isVerified: model.boolean(), @@ -2508,9 +2472,7 @@ describe("Entity builder", () => { email: model.hasOne(() => email), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) - const Email = entityBuilder(email) + const [User, Email] = [user, email].map(toMikroORMEntity) expectTypeOf(new User()).toMatchTypeOf<{ id: number @@ -2630,12 +2592,21 @@ describe("Entity builder", () => { setter: false, }, user: { - reference: "1:1", name: "user", + reference: "1:1", entity: "User", nullable: false, - owner: true, mappedBy: "email", + owner: true, + }, + user_id: { + reference: "scalar", + type: "string", + columnType: "text", + nullable: false, + name: "user_id", + getter: false, + setter: false, }, created_at: { reference: "scalar", @@ -2673,8 +2644,6 @@ describe("Entity builder", () => { }) test("mark belongsTo with hasOne as nullable", () => { - const model = new EntityBuilder() - const email = model.define("email", { email: model.text(), isVerified: model.boolean(), @@ -2687,9 +2656,8 @@ describe("Entity builder", () => { email: model.hasOne(() => email), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) - const Email = entityBuilder(email) + const User = toMikroORMEntity(user) + const Email = toMikroORMEntity(email) expectTypeOf(new User()).toMatchTypeOf<{ id: number @@ -2803,12 +2771,21 @@ describe("Entity builder", () => { setter: false, }, user: { - reference: "1:1", name: "user", + reference: "1:1", entity: "User", nullable: true, - owner: true, mappedBy: "email", + owner: true, + }, + user_id: { + reference: "scalar", + type: "string", + columnType: "text", + nullable: true, + name: "user_id", + getter: false, + setter: false, }, created_at: { reference: "scalar", @@ -2846,8 +2823,6 @@ describe("Entity builder", () => { }) test("define belongsTo relationship with hasMany", () => { - const model = new EntityBuilder() - const email = model.define("email", { email: model.text(), isVerified: model.boolean(), @@ -2860,9 +2835,8 @@ describe("Entity builder", () => { emails: model.hasMany(() => email), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) - const Email = entityBuilder(email) + const User = toMikroORMEntity(user) + const Email = toMikroORMEntity(email) expectTypeOf(new User()).toMatchTypeOf<{ id: number @@ -2976,19 +2950,20 @@ describe("Entity builder", () => { setter: false, }, user: { - reference: "m:1", name: "user", + reference: "m:1", entity: "User", persist: false, + nullable: false, }, user_id: { - columnType: "text", - entity: "User", - fieldName: "user_id", - mapToPk: true, name: "user_id", - nullable: false, reference: "m:1", + entity: "User", + columnType: "text", + mapToPk: true, + fieldName: "user_id", + nullable: false, }, created_at: { reference: "scalar", @@ -3026,8 +3001,6 @@ describe("Entity builder", () => { }) test("define belongsTo with hasMany as nullable", () => { - const model = new EntityBuilder() - const email = model.define("email", { email: model.text(), isVerified: model.boolean(), @@ -3040,9 +3013,8 @@ describe("Entity builder", () => { emails: model.hasMany(() => email), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) - const Email = entityBuilder(email) + const User = toMikroORMEntity(user) + const Email = toMikroORMEntity(email) expectTypeOf(new User()).toMatchTypeOf<{ id: number @@ -3156,19 +3128,20 @@ describe("Entity builder", () => { setter: false, }, user: { - reference: "m:1", name: "user", + reference: "m:1", entity: "User", persist: false, + nullable: true, }, user_id: { - columnType: "text", - entity: "User", - fieldName: "user_id", - mapToPk: true, name: "user_id", - nullable: true, reference: "m:1", + entity: "User", + columnType: "text", + mapToPk: true, + fieldName: "user_id", + nullable: true, }, created_at: { reference: "scalar", @@ -3206,8 +3179,6 @@ describe("Entity builder", () => { }) test("throw error when other side relationship is missing", () => { - const model = new EntityBuilder() - const email = model.define("email", { email: model.text(), isVerified: model.boolean(), @@ -3219,15 +3190,12 @@ describe("Entity builder", () => { username: model.text(), }) - const entityBuilder = createMikrORMEntity() - expect(() => entityBuilder(email)).toThrow( + expect(() => toMikroORMEntity(email)).toThrow( 'Missing property "email" on "User" entity. Make sure to define it as a relationship' ) }) test("throw error when other side relationship is invalid", () => { - const model = new EntityBuilder() - const email = model.define("email", { email: model.text(), isVerified: model.boolean(), @@ -3240,15 +3208,12 @@ describe("Entity builder", () => { email: model.belongsTo(() => email), }) - const entityBuilder = createMikrORMEntity() - expect(() => entityBuilder(email)).toThrow( + expect(() => toMikroORMEntity(email)).toThrow( 'Invalid relationship reference for "email" on "User" entity. Make sure to define a hasOne or hasMany relationship' ) }) test("throw error when cascading a parent from a child", () => { - const model = new EntityBuilder() - const user = model.define("user", { id: model.number(), username: model.text(), @@ -3272,8 +3237,6 @@ describe("Entity builder", () => { }) test("define relationships when entity names has pg schema name", () => { - const model = new EntityBuilder() - const email = model.define("platform.email", { email: model.text(), isVerified: model.boolean(), @@ -3286,9 +3249,8 @@ describe("Entity builder", () => { email: model.hasOne(() => email), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) - const Email = entityBuilder(email) + const User = toMikroORMEntity(user) + const Email = toMikroORMEntity(email) expectTypeOf(new User()).toMatchTypeOf<{ id: number @@ -3410,12 +3372,21 @@ describe("Entity builder", () => { setter: false, }, user: { - reference: "1:1", name: "user", + reference: "1:1", entity: "User", nullable: false, - owner: true, mappedBy: "email", + owner: true, + }, + user_id: { + reference: "scalar", + type: "string", + columnType: "text", + nullable: false, + name: "user_id", + getter: false, + setter: false, }, created_at: { reference: "scalar", @@ -3453,8 +3424,6 @@ describe("Entity builder", () => { }) test("define relationships between cross pg schemas entities", () => { - const model = new EntityBuilder() - const email = model.define("platform.email", { email: model.text(), isVerified: model.boolean(), @@ -3467,9 +3436,8 @@ describe("Entity builder", () => { email: model.hasOne(() => email), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) - const Email = entityBuilder(email) + const User = toMikroORMEntity(user) + const Email = toMikroORMEntity(email) expectTypeOf(new User()).toMatchTypeOf<{ id: number @@ -3591,12 +3559,21 @@ describe("Entity builder", () => { setter: false, }, user: { - reference: "1:1", name: "user", + reference: "1:1", entity: "User", nullable: false, - owner: true, mappedBy: "email", + owner: true, + }, + user_id: { + reference: "scalar", + type: "string", + columnType: "text", + nullable: false, + name: "user_id", + getter: false, + setter: false, }, created_at: { reference: "scalar", @@ -3636,7 +3613,6 @@ describe("Entity builder", () => { describe("Entity builder | manyToMany", () => { test("define manyToMany relationship", () => { - const model = new EntityBuilder() const team = model.define("team", { id: model.number(), name: model.text(), @@ -3649,9 +3625,8 @@ describe("Entity builder", () => { teams: model.manyToMany(() => team), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) - const Team = entityBuilder(team) + const User = toMikroORMEntity(user) + const Team = toMikroORMEntity(team) expectTypeOf(new User()).toMatchTypeOf<{ id: number @@ -3805,7 +3780,6 @@ describe("Entity builder", () => { }) test("define mappedBy on one side", () => { - const model = new EntityBuilder() const team = model.define("team", { id: model.number(), name: model.text(), @@ -3818,9 +3792,8 @@ describe("Entity builder", () => { teams: model.manyToMany(() => team, { mappedBy: "users" }), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) - const Team = entityBuilder(team) + const User = toMikroORMEntity(user) + const Team = toMikroORMEntity(team) expectTypeOf(new User()).toMatchTypeOf<{ id: number @@ -3975,7 +3948,6 @@ describe("Entity builder", () => { }) test("throw error when unable to locate relationship via mappedBy", () => { - const model = new EntityBuilder() const team = model.define("team", { id: model.number(), name: model.text(), @@ -3987,14 +3959,12 @@ describe("Entity builder", () => { teams: model.manyToMany(() => team, { mappedBy: "users" }), }) - const entityBuilder = createMikrORMEntity() - expect(() => entityBuilder(user)).toThrow( + expect(() => toMikroORMEntity(user)).toThrow( 'Missing property "users" on "Team" entity. Make sure to define it as a relationship' ) }) test("throw error when mappedBy relationship is not a manyToMany", () => { - const model = new EntityBuilder() const team = model.define("team", { id: model.number(), name: model.text(), @@ -4007,14 +3977,12 @@ describe("Entity builder", () => { teams: model.manyToMany(() => team, { mappedBy: "users" }), }) - const entityBuilder = createMikrORMEntity() - expect(() => entityBuilder(user)).toThrow( + expect(() => toMikroORMEntity(user)).toThrow( 'Invalid relationship reference for "users" on "Team" entity. Make sure to define a manyToMany relationship' ) }) test("define mappedBy on both sides", () => { - const model = new EntityBuilder() const team = model.define("team", { id: model.number(), name: model.text(), @@ -4027,9 +3995,7 @@ describe("Entity builder", () => { teams: model.manyToMany(() => team, { mappedBy: "users" }), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) - const Team = entityBuilder(team) + const [User, Team] = toMikroOrmEntities([user, team, {}]) expectTypeOf(new User()).toMatchTypeOf<{ id: number @@ -4189,7 +4155,6 @@ describe("Entity builder", () => { }) test("define mappedBy on both sides and reverse order of registering entities", () => { - const model = new EntityBuilder() const team = model.define("team", { id: model.number(), name: model.text(), @@ -4364,7 +4329,6 @@ describe("Entity builder", () => { }) test("define multiple many to many relationships to the same entity", () => { - const model = new EntityBuilder() const team = model.define("team", { id: model.number(), name: model.text(), @@ -4571,7 +4535,6 @@ describe("Entity builder", () => { }) test("define manyToMany relationship when entity names has pg schema name", () => { - const model = new EntityBuilder() const team = model.define("platform.team", { id: model.number(), name: model.text(), @@ -4584,9 +4547,8 @@ describe("Entity builder", () => { teams: model.manyToMany(() => team), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) - const Team = entityBuilder(team) + const User = toMikroORMEntity(user) + const Team = toMikroORMEntity(team) expectTypeOf(new User()).toMatchTypeOf<{ id: number @@ -4742,7 +4704,6 @@ describe("Entity builder", () => { }) test("define custom pivot table name", () => { - const model = new EntityBuilder() const team = model.define("team", { id: model.number(), name: model.text(), @@ -4757,9 +4718,8 @@ describe("Entity builder", () => { teams: model.manyToMany(() => team, { pivotTable: "users_teams" }), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) - const Team = entityBuilder(team) + const User = toMikroORMEntity(user) + const Team = toMikroORMEntity(team) expectTypeOf(new User()).toMatchTypeOf<{ id: number @@ -4913,7 +4873,6 @@ describe("Entity builder", () => { }) test("define custom pivot entity", () => { - const model = new EntityBuilder() const team = model.define("team", { id: model.number(), name: model.text(), @@ -4934,10 +4893,7 @@ describe("Entity builder", () => { teams: model.manyToMany(() => team, { pivotEntity: () => squad }), }) - const entityBuilder = createMikrORMEntity() - const User = entityBuilder(user) - const Team = entityBuilder(team) - const Squad = entityBuilder(squad) + const [User, Team, Squad] = toMikroOrmEntities([user, team, squad]) expectTypeOf(new User()).toMatchTypeOf<{ id: number @@ -4973,74 +4929,78 @@ describe("Entity builder", () => { expect(squadMetaData.properties).toEqual({ id: { reference: "scalar", - type: "number", columnType: "integer", - name: "id", + type: "number", nullable: false, + name: "id", getter: false, setter: false, }, - team: { - entity: "Team", - name: "team", - persist: false, + user_id: { + name: "user_id", reference: "m:1", - }, - team_id: { + entity: "User", columnType: "text", - entity: "Team", - fieldName: "team_id", mapToPk: true, - name: "team_id", + fieldName: "user_id", nullable: false, - onDelete: undefined, - reference: "m:1", }, user: { - entity: "User", - name: "user", + reference: "scalar", + type: "User", persist: false, - reference: "m:1", - }, - user_id: { - columnType: "text", - entity: "User", - fieldName: "user_id", - mapToPk: true, - name: "user_id", nullable: false, - onDelete: undefined, + name: "user", + getter: false, + setter: false, + }, + team_id: { + name: "team_id", reference: "m:1", + entity: "Team", + columnType: "text", + mapToPk: true, + fieldName: "team_id", + nullable: false, + }, + team: { + reference: "scalar", + type: "Team", + persist: false, + nullable: false, + name: "team", + getter: false, + setter: false, }, created_at: { reference: "scalar", - type: "date", columnType: "timestamptz", - name: "created_at", - defaultRaw: "now()", - onCreate: expect.any(Function), + type: "date", nullable: false, + onCreate: expect.any(Function), + defaultRaw: "now()", + name: "created_at", getter: false, setter: false, }, updated_at: { reference: "scalar", - type: "date", columnType: "timestamptz", - name: "updated_at", - defaultRaw: "now()", + type: "date", + nullable: false, onCreate: expect.any(Function), onUpdate: expect.any(Function), - nullable: false, + defaultRaw: "now()", + name: "updated_at", getter: false, setter: false, }, deleted_at: { reference: "scalar", - type: "date", columnType: "timestamptz", - name: "deleted_at", + type: "date", nullable: true, + name: "deleted_at", getter: false, setter: false, }, diff --git a/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts b/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts index f96eb0df7b..62b3d327bf 100644 --- a/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts +++ b/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts @@ -154,6 +154,7 @@ export function createMikrORMEntity() { * - [user.teams]: true // the teams relationship on user is an owner * - [team.users] // cannot be an owner */ + // TODO: if we use the util toMikroOrmEntities then a new builder will be used each time, lets think about this. Currently if means that with many to many we need to use the same builder const MANY_TO_MANY_TRACKED_REALTIONS: Record = {} /** @@ -390,6 +391,23 @@ export function createMikrORMEntity() { ) } + function applyForeignKeyAssignationHooks(foreignKeyName: string) { + const hookName = `assignRelationFromForeignKeyValue${foreignKeyName}` + /** + * Hook to handle foreign key assignation + */ + MikroORMEntity.prototype[hookName] = function () { + this[relationship.name] ??= this[foreignKeyName] + this[foreignKeyName] ??= this[relationship.name]?.id + } + + /** + * Execute hook via lifecycle decorators + */ + BeforeCreate()(MikroORMEntity.prototype, hookName) + OnInit()(MikroORMEntity.prototype, hookName) + } + /** * Otherside is a has many. Hence we should defined a ManyToOne */ @@ -397,6 +415,8 @@ export function createMikrORMEntity() { otherSideRelation instanceof HasMany || otherSideRelation instanceof DmlManyToMany ) { + const foreignKeyName = camelToSnakeCase(`${relationship.name}Id`) + ManyToOne({ entity: relatedModelName, columnType: "text", @@ -406,10 +426,22 @@ export function createMikrORMEntity() { onDelete: shouldCascade ? "cascade" : undefined, })(MikroORMEntity.prototype, camelToSnakeCase(`${relationship.name}Id`)) - ManyToOne({ - entity: relatedModelName, - persist: false, - })(MikroORMEntity.prototype, relationship.name) + if (otherSideRelation instanceof DmlManyToMany) { + Property({ + type: relatedModelName, + persist: false, + nullable: relationship.nullable, + })(MikroORMEntity.prototype, relationship.name) + } else { + // HasMany case + ManyToOne({ + entity: relatedModelName, + persist: false, + nullable: relationship.nullable, + })(MikroORMEntity.prototype, relationship.name) + } + + applyForeignKeyAssignationHooks(foreignKeyName) return } @@ -417,6 +449,8 @@ export function createMikrORMEntity() { * Otherside is a has one. Hence we should defined a OneToOne */ if (otherSideRelation instanceof HasOne) { + const foreignKeyName = camelToSnakeCase(`${relationship.name}Id`) + OneToOne({ entity: relatedModelName, nullable: relationship.nullable, @@ -424,6 +458,23 @@ export function createMikrORMEntity() { owner: true, onDelete: shouldCascade ? "cascade" : undefined, })(MikroORMEntity.prototype, relationship.name) + + if (relationship.nullable) { + Object.defineProperty(MikroORMEntity.prototype, foreignKeyName, { + value: null, + configurable: true, + enumerable: true, + writable: true, + }) + } + + Property({ + type: "string", + columnType: "text", + nullable: relationship.nullable, + })(MikroORMEntity.prototype, foreignKeyName) + + applyForeignKeyAssignationHooks(foreignKeyName) return } @@ -681,12 +732,18 @@ export function createMikrORMEntity() { * return the input idempotently * @param entity */ -export const toMikroORMEntity = (entity: any) => { +export const toMikroORMEntity = ( + entity: T +): T extends DmlEntity ? EntityConstructor : T => { + let mikroOrmEntity: T | EntityConstructor = entity + if (DmlEntity.isDmlEntity(entity)) { - return createMikrORMEntity()(entity) + mikroOrmEntity = createMikrORMEntity()(entity) } - return entity + return mikroOrmEntity as T extends DmlEntity + ? EntityConstructor + : T } /** @@ -694,6 +751,16 @@ export const toMikroORMEntity = (entity: any) => { * This action is idempotent if non of the entities are DmlEntity * @param entities */ -export const toMikroOrmEntities = function (entities: any[]) { - return entities.map(toMikroORMEntity) +export const toMikroOrmEntities = function (entities: T) { + const entityBuilder = createMikrORMEntity() + + return entities.map((entity) => { + if (DmlEntity.isDmlEntity(entity)) { + return entityBuilder(entity) + } + + return entity + }) as { + [K in keyof T]: T[K] extends DmlEntity ? EntityConstructor : T[K] + } } diff --git a/packages/core/utils/src/dml/integration-tests/many-to-many.spec.ts b/packages/core/utils/src/dml/integration-tests/many-to-many.spec.ts new file mode 100644 index 0000000000..6e7525a2e1 --- /dev/null +++ b/packages/core/utils/src/dml/integration-tests/many-to-many.spec.ts @@ -0,0 +1,193 @@ +import { MetadataStorage, MikroORM } from "@mikro-orm/core" +import { model } from "../entity-builder" +import { toMikroOrmEntities } from "../helpers/create-mikro-orm-entity" +import { createDatabase, dropDatabase } from "pg-god" +import { mikroOrmSerializer, TSMigrationGenerator } from "../../dal" +import { FileSystem } from "../../common" +import { join } from "path" +import { EntityConstructor } from "@medusajs/types" + +const DB_HOST = process.env.DB_HOST +const DB_USERNAME = process.env.DB_USERNAME +const DB_PASSWORD = process.env.DB_PASSWORD + +const pgGodCredentials = { + user: DB_USERNAME, + password: DB_PASSWORD, + host: DB_HOST, +} + +const fileSystem = new FileSystem(join(__dirname, "../../migrations")) + +describe("manyToMany - manyToMany", () => { + const dbName = "EntityBuilder-ManyToMany" + + let orm!: MikroORM + + let Team: EntityConstructor, + User: EntityConstructor, + Squad: EntityConstructor + + afterAll(() => { + fileSystem.cleanup() + }) + + beforeEach(async () => { + MetadataStorage.clear() + + const team = model.define("team", { + id: model.id(), + name: model.text(), + users: model.manyToMany(() => user, { + pivotEntity: () => squad, + mappedBy: "squads", + }), + }) + + const squad = model.define("teamUsers", { + id: model.id(), + user: model.belongsTo(() => user, { mappedBy: "squads" }), + squad: model.belongsTo(() => team, { mappedBy: "users" }), + }) + + const user = model.define("user", { + id: model.id(), + username: model.text(), + squads: model.manyToMany(() => team, { + pivotEntity: () => squad, + mappedBy: "users", + }), + }) + + ;[User, Squad, Team] = toMikroOrmEntities([user, squad, team]) + + await createDatabase({ databaseName: dbName }, pgGodCredentials) + + orm = await MikroORM.init({ + entities: [Team, User, Squad], + tsNode: true, + dbName, + debug: true, + type: "postgresql", + migrations: { + generator: TSMigrationGenerator, + }, + }) + + const migrator = orm.getMigrator() + await migrator.createMigration() + await migrator.up() + }) + + afterEach(async () => { + await orm.close() + + await dropDatabase( + { databaseName: dbName, errorIfNonExist: false }, + pgGodCredentials + ) + }) + + it(`should handle the relation properly`, async () => { + let manager = orm.em.fork() + + const user1 = manager.create(User, { + username: "User 1", + }) + const user2 = manager.create(User, { + username: "User 2", + }) + + await manager.persistAndFlush([user1, user2]) + manager = orm.em.fork() + + const team1 = manager.create(Team, { + name: "Team 1", + }) + const team2 = manager.create(Team, { + name: "Team 2", + }) + + await manager.persistAndFlush([team1, team2]) + manager = orm.em.fork() + + const squad1 = manager.create(Squad, { + user_id: user1.id, + squad_id: team1.id, + }) + const squad2 = manager.create(Squad, { + user_id: user2.id, + squad_id: team1.id, + }) + + await manager.persistAndFlush([squad1, squad2]) + manager = orm.em.fork() + + const team = await manager.findOne( + Team, + { + id: team1.id, + }, + { + populate: ["users"], + } + ) + + const serializedSquad = mikroOrmSerializer>(team) + + expect(serializedSquad.users).toHaveLength(2) + expect(serializedSquad).toEqual({ + id: team1.id, + name: "Team 1", + created_at: expect.any(Date), + updated_at: expect.any(Date), + deleted_at: null, + users: expect.arrayContaining([ + { + id: user1.id, + username: "User 1", + created_at: expect.any(Date), + updated_at: expect.any(Date), + deleted_at: null, + }, + { + id: user2.id, + username: "User 2", + created_at: expect.any(Date), + updated_at: expect.any(Date), + deleted_at: null, + }, + ]), + }) + + const user = await manager.findOne( + User, + { + id: user1.id, + }, + { + populate: ["squads"], + } + ) + + const serializedUser = mikroOrmSerializer>(user) + + expect(serializedUser.squads).toHaveLength(1) + expect(serializedUser).toEqual({ + id: user1.id, + username: "User 1", + created_at: expect.any(Date), + updated_at: expect.any(Date), + deleted_at: null, + squads: expect.arrayContaining([ + { + id: team1.id, + name: "Team 1", + created_at: expect.any(Date), + updated_at: expect.any(Date), + deleted_at: null, + }, + ]), + }) + }) +}) diff --git a/packages/core/utils/src/dml/integration-tests/many-to-one.spec.ts b/packages/core/utils/src/dml/integration-tests/many-to-one.spec.ts new file mode 100644 index 0000000000..2f54b291e4 --- /dev/null +++ b/packages/core/utils/src/dml/integration-tests/many-to-one.spec.ts @@ -0,0 +1,152 @@ +import { MetadataStorage, MikroORM } from "@mikro-orm/core" +import { model } from "../entity-builder" +import { toMikroOrmEntities } from "../helpers/create-mikro-orm-entity" +import { createDatabase, dropDatabase } from "pg-god" +import { mikroOrmSerializer } from "../../dal" +import { FileSystem } from "../../common" +import { join } from "path" +import { EntityConstructor } from "@medusajs/types" + +const DB_HOST = process.env.DB_HOST +const DB_USERNAME = process.env.DB_USERNAME +const DB_PASSWORD = process.env.DB_PASSWORD + +const pgGodCredentials = { + user: DB_USERNAME, + password: DB_PASSWORD, + host: DB_HOST, +} + +const fileSystem = new FileSystem(join(__dirname, "../../migrations")) + +describe("manyToOne - belongTo", () => { + const dbName = "EntityBuilder-ManyToOne" + + let orm!: MikroORM + let Team: EntityConstructor, User: EntityConstructor + + afterAll(() => { + fileSystem.cleanup() + }) + + beforeEach(async () => { + MetadataStorage.clear() + + const team = model.define("team", { + id: model.id(), + name: model.text(), + user: model.belongsTo(() => user, { mappedBy: "teams" }), + }) + + const user = model.define("user", { + id: model.id(), + username: model.text(), + teams: model.hasMany(() => team, { mappedBy: "user" }), + }) + + ;[User, Team] = toMikroOrmEntities([user, team]) + + await createDatabase({ databaseName: dbName }, pgGodCredentials) + + orm = await MikroORM.init({ + entities: [Team, User], + tsNode: true, + dbName, + debug: true, + type: "postgresql", + }) + + const migrator = orm.getMigrator() + await migrator.createMigration() + await migrator.up() + }) + + afterEach(async () => { + await orm.close() + + await dropDatabase( + { databaseName: dbName, errorIfNonExist: false }, + pgGodCredentials + ) + }) + + it(`should handle the relation properly`, async () => { + let manager = orm.em.fork() + + const user1 = manager.create(User, { + username: "User 1", + }) + const user2 = manager.create(User, { + username: "User 2", + }) + + await manager.persistAndFlush([user1, user2]) + manager = orm.em.fork() + + const team1 = manager.create(Team, { + name: "Team 1", + user_id: user1.id, + }) + const team2 = manager.create(Team, { + name: "Team 2", + user_id: user2.id, + }) + + await manager.persistAndFlush([team1, team2]) + manager = orm.em.fork() + + const team = await manager.findOne( + Team, + { + id: team1.id, + }, + { + populate: ["user"], + } + ) + + expect(mikroOrmSerializer>(team)).toEqual({ + id: team1.id, + name: "Team 1", + created_at: expect.any(Date), + updated_at: expect.any(Date), + deleted_at: null, + user_id: user1.id, + user: { + id: user1.id, + username: "User 1", + created_at: expect.any(Date), + updated_at: expect.any(Date), + deleted_at: null, + }, + }) + + const user = await manager.findOne( + User, + { + id: user1.id, + }, + { + populate: ["teams"], + } + ) + + expect(mikroOrmSerializer>(user)).toEqual({ + id: user1.id, + username: "User 1", + created_at: expect.any(Date), + updated_at: expect.any(Date), + deleted_at: null, + teams: [ + { + id: team1.id, + name: "Team 1", + created_at: expect.any(Date), + updated_at: expect.any(Date), + deleted_at: null, + user_id: user1.id, + }, + ], + }) + }) +}) diff --git a/packages/core/utils/src/modules-sdk/index.ts b/packages/core/utils/src/modules-sdk/index.ts index d44eaabeaa..aeb2ceb573 100644 --- a/packages/core/utils/src/modules-sdk/index.ts +++ b/packages/core/utils/src/modules-sdk/index.ts @@ -13,3 +13,4 @@ export * from "./medusa-internal-service" export * from "./medusa-service" export * from "./migration-scripts" export * from "./mikro-orm-cli-config-builder" + diff --git a/packages/core/utils/src/modules-sdk/mikro-orm-cli-config-builder.ts b/packages/core/utils/src/modules-sdk/mikro-orm-cli-config-builder.ts index 4d3a835a27..c5433e4f7c 100644 --- a/packages/core/utils/src/modules-sdk/mikro-orm-cli-config-builder.ts +++ b/packages/core/utils/src/modules-sdk/mikro-orm-cli-config-builder.ts @@ -1,8 +1,12 @@ import { MikroORMOptions } from "@mikro-orm/core/utils/Configuration" import { DmlEntity, toMikroORMEntity } from "../dml" import { TSMigrationGenerator } from "../dal" -import { AnyEntity, EntityClassGroup, EntitySchema } from "@mikro-orm/core" -import { EntityClass } from "@mikro-orm/core/typings" +import type { + AnyEntity, + EntityClass, + EntityClassGroup, +} from "@mikro-orm/core/typings" +import type { EntitySchema } from "@mikro-orm/core/metadata/EntitySchema" type Options = Partial & { entities: ( diff --git a/packages/modules/fulfillment/mikro-orm.config.dev.ts b/packages/modules/fulfillment/mikro-orm.config.dev.ts index 905695bc8d..28c4309322 100644 --- a/packages/modules/fulfillment/mikro-orm.config.dev.ts +++ b/packages/modules/fulfillment/mikro-orm.config.dev.ts @@ -1,12 +1,7 @@ import * as entities from "./src/models" -import { TSMigrationGenerator } from "@medusajs/utils" +import { defineMikroOrmCliConfig } from "@medusajs/utils" -module.exports = { +module.exports = defineMikroOrmCliConfig({ entities: Object.values(entities), - schema: "public", - clientUrl: "postgres://postgres@localhost/medusa-fulfillment", - type: "postgresql", - migrations: { - generator: TSMigrationGenerator, - }, -} + databaseName: "medusa-fulfillment", +})