chore(fulfillment, utils): Migrate module to DML (#10617)

**What**
- Allow to provide `foreignKeyName` option for hasOne and belongsTo relationships
  - `model.hasOne(() => OtherEntity, { foreignKey: true, foreignKeyName: 'other_entity_something_id' })`
  - The above will also output a generated type that takes into consideration the custom fk name 🔽 
- Update types to account for defined custom foreign key name
- Fix joiner config linkable generation to account for custom linkable keys that provide a public API for their model but are not part of the list of the models included in the MedusaService
  - This was supposed to be handled correctly but the implementation was not considering that custom linkable keys could reference models not part of the one provided to medusa service
- Migrate fulfillment module to DML
- Fix has one with fk behaviour and hooks (the relation should be assigned but not the fk)
- Fix has one belongsTo hooks (the relation should be assigned but not the fk)
- Fix hasOneWithFk and belongsTo non persisted fk to be selectable
- Allow to define `belongsTo` without other side definition for `ManyToOne` with no counter part defined
  - Meaning that if a user defined `belongsTo` on one side andnot mapped by and no counter part on the other entity it will be considered as a `ManyToOne`
- `orphanRemoval` on `OneToOne` have been removed, this means that when assigning a new object relation to an entity, the previous one gets deconected but not deleted automatically. This prevent removing data un volountarely

**NOTE**
As per our convention here are some information to keep in mind

**HasOne <> BelongsTo**
Define `OneToOne`, The foreign key is owned by the belongs to and the relation needs to be provided to cascade if wanted

**HasMany <> BelongsTo**
Define `OneToMane` <> `ManyToOne`, the foreign key is owned by the many to one and for those relation no cascade will be performed, the foreign key must be provided. For the `HasMany` the cascade is available

**HasOne (with FK)**
Will act similarly to belongs to with **HasOne <> BelongsTo**

Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2024-12-19 16:40:11 +00:00
committed by GitHub
co-authored by Carlos R. L. Rodrigues
parent 65007c49f6
commit 100da64242
39 changed files with 1154 additions and 1858 deletions
@@ -3018,6 +3018,7 @@ describe("Entity builder", () => {
persist: false,
name: "user_id",
nullable: false,
formula: expect.any(Function),
reference: "scalar",
setter: false,
type: "string",
@@ -3123,14 +3124,16 @@ describe("Entity builder", () => {
reference: "1:1",
name: "email",
entity: "Email",
fieldName: "email_id",
},
email_id: {
columnType: "text",
type: "string",
reference: "scalar",
name: "email_id",
formula: expect.any(Function),
nullable: false,
persist: true,
persist: false,
getter: false,
setter: false,
},
@@ -3233,14 +3236,16 @@ describe("Entity builder", () => {
name: "emails",
entity: "Email",
nullable: true,
fieldName: "emails_id",
},
emails_id: {
columnType: "text",
type: "string",
reference: "scalar",
name: "emails_id",
formula: expect.any(Function),
nullable: true,
persist: true,
persist: false,
getter: false,
setter: false,
},
@@ -3334,14 +3339,16 @@ describe("Entity builder", () => {
name: "email",
entity: "Email",
mappedBy: "owner",
fieldName: "email_id",
},
email_id: {
columnType: "text",
type: "string",
reference: "scalar",
name: "email_id",
formula: expect.any(Function),
nullable: false,
persist: true,
persist: false,
getter: false,
setter: false,
},
@@ -3439,14 +3446,16 @@ describe("Entity builder", () => {
entity: "Email",
cascade: ["persist", "soft-remove"],
mappedBy: "user",
fieldName: "email_id",
},
email_id: {
columnType: "text",
type: "string",
reference: "scalar",
name: "email_id",
formula: expect.any(Function),
nullable: false,
persist: true,
persist: false,
getter: false,
setter: false,
},
@@ -3614,14 +3623,16 @@ describe("Entity builder", () => {
entity: "Email",
cascade: ["persist", "soft-remove"],
mappedBy: "user",
fieldName: "email_id",
},
email_id: {
columnType: "text",
type: "string",
reference: "scalar",
formula: expect.any(Function),
name: "email_id",
nullable: false,
persist: true,
persist: false,
getter: false,
setter: false,
},
@@ -3704,6 +3715,7 @@ describe("Entity builder", () => {
name: "user_id",
nullable: false,
reference: "scalar",
formula: expect.any(Function),
setter: false,
type: "string",
persist: false,
@@ -4649,6 +4661,7 @@ describe("Entity builder", () => {
reference: "scalar",
persist: false,
type: "string",
formula: expect.any(Function),
columnType: "text",
nullable: false,
name: "user_id",
@@ -4848,6 +4861,7 @@ describe("Entity builder", () => {
type: "string",
columnType: "text",
nullable: true,
formula: expect.any(Function),
name: "user_id",
getter: false,
setter: false,
@@ -5268,23 +5282,6 @@ describe("Entity builder", () => {
})
})
test("throw error when other side relationship is missing", () => {
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
user: model.belongsTo(() => user),
})
const user = model.define("user", {
id: model.number(),
username: model.text(),
})
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 email = model.define("email", {
email: model.text(),
@@ -5482,6 +5479,7 @@ describe("Entity builder", () => {
type: "string",
persist: false,
columnType: "text",
formula: expect.any(Function),
nullable: false,
name: "user_id",
getter: false,
@@ -5682,6 +5680,7 @@ describe("Entity builder", () => {
type: "string",
columnType: "text",
nullable: false,
formula: expect.any(Function),
name: "user_id",
getter: false,
setter: false,
@@ -5903,6 +5902,7 @@ describe("Entity builder", () => {
type: "string",
columnType: "text",
reference: "scalar",
formula: expect.any(Function),
persist: false,
getter: false,
setter: false,
+23 -25
View File
@@ -3,15 +3,9 @@ import {
IDmlEntityConfig,
RelationshipOptions,
} from "@medusajs/types"
import { DmlEntity } from "./entity"
import {
createBigNumberProperties,
DMLSchemaWithBigNumber,
} from "./helpers/entity-builder/create-big-number-properties"
import {
createDefaultProperties,
DMLSchemaDefaults,
} from "./helpers/entity-builder/create-default-properties"
import { DmlEntity, DMLEntitySchemaBuilder } from "./entity"
import { createBigNumberProperties } from "./helpers/entity-builder/create-big-number-properties"
import { createDefaultProperties } from "./helpers/entity-builder/create-default-properties"
import { ArrayProperty } from "./properties/array"
import { AutoIncrementProperty } from "./properties/autoincrement"
import { BigNumberProperty } from "./properties/big-number"
@@ -131,20 +125,14 @@ export class EntityBuilder {
define<Schema extends DMLSchema, const TConfig extends IDmlEntityConfig>(
nameOrConfig: TConfig,
schema: Schema
): DmlEntity<
Schema & DMLSchemaWithBigNumber<Schema> & DMLSchemaDefaults,
TConfig
> {
): DmlEntity<DMLEntitySchemaBuilder<Schema>, TConfig> {
this.#disallowImplicitProperties(schema)
return new DmlEntity<Schema, TConfig>(nameOrConfig, {
...schema,
...createBigNumberProperties(schema),
...createDefaultProperties(),
}) as unknown as DmlEntity<
Schema & DMLSchemaWithBigNumber<Schema> & DMLSchemaDefaults,
TConfig
>
}) as unknown as DmlEntity<DMLEntitySchemaBuilder<Schema>, TConfig>
}
/**
@@ -253,7 +241,7 @@ export class EntityBuilder {
/**
* This method defines a float property that allows for
* values with decimal places
*
*
* @version 2.1.2
*
* @example
@@ -398,26 +386,31 @@ export class EntityBuilder {
*
* @customNamespace Relationship Methods
*/
hasOne<T>(
hasOne<T, const ForeignKeyName extends string | undefined = undefined>(
entityBuilder: T,
options: RelationshipOptions & {
foreignKey: true
foreignKeyName?: ForeignKeyName
}
): HasOneWithForeignKey<T>
): HasOneWithForeignKey<T, ForeignKeyName>
hasOne<T>(
entityBuilder: T,
options?: RelationshipOptions & {
foreignKey?: false
}
): HasOne<T>
hasOne<T>(
hasOne<T, const ForeignKeyName extends string | undefined = undefined>(
entityBuilder: T,
options?: RelationshipOptions & {
foreignKey?: boolean
foreignKeyName?: ForeignKeyName
}
): HasOneWithForeignKey<T> | HasOne<T> {
): HasOneWithForeignKey<T, ForeignKeyName> | HasOne<T> {
if (options?.foreignKey) {
return new HasOneWithForeignKey<T>(entityBuilder, options || {})
return new HasOneWithForeignKey<T, ForeignKeyName>(
entityBuilder,
options || {}
)
}
return new HasOne<T>(entityBuilder, options || {})
}
@@ -445,8 +438,13 @@ export class EntityBuilder {
*
* @customNamespace Relationship Methods
*/
belongsTo<T>(entityBuilder: T, options?: RelationshipOptions) {
return new BelongsTo<T>(entityBuilder, options || {})
belongsTo<T, const ForeignKeyName extends string | undefined = undefined>(
entityBuilder: T,
options?: RelationshipOptions & {
foreignKeyName?: ForeignKeyName
}
) {
return new BelongsTo<T, ForeignKeyName>(entityBuilder, options || {})
}
/**
+7
View File
@@ -12,9 +12,16 @@ import {
import { isObject, isString, toCamelCase, upperCaseFirst } from "../common"
import { transformIndexWhere } from "./helpers/entity-builder/build-indexes"
import { BelongsTo } from "./relations/belongs-to"
import {
DMLSchemaDefaults,
DMLSchemaWithBigNumber,
} from "./helpers/entity-builder"
const IsDmlEntity = Symbol.for("isDmlEntity")
export type DMLEntitySchemaBuilder<Schema extends DMLSchema> =
DMLSchemaWithBigNumber<Schema> & DMLSchemaDefaults & Schema
function extractNameAndTableName<const Config extends IDmlEntityConfig>(
nameOrConfig: Config
) {
@@ -7,6 +7,7 @@ import {
} from "@medusajs/types"
import {
BeforeCreate,
BeforeUpdate,
Cascade,
ManyToMany,
ManyToOne,
@@ -19,13 +20,13 @@ import {
} from "@mikro-orm/core"
import { camelToSnakeCase, pluralize } from "../../../common"
import { DmlEntity } from "../../entity"
import { BelongsTo } from "../../relations"
import { HasMany } from "../../relations/has-many"
import { HasOne } from "../../relations/has-one"
import { HasOneWithForeignKey } from "../../relations/has-one-fk"
import { ManyToMany as DmlManyToMany } from "../../relations/many-to-many"
import { applyEntityIndexes } from "../mikro-orm/apply-indexes"
import { parseEntityName } from "./parse-entity-name"
import { BelongsTo } from "../../relations"
type Context = {
MANY_TO_MANY_TRACKED_RELATIONS: Record<string, boolean>
@@ -181,7 +182,10 @@ export function defineHasOneWithFKRelationship(
{ relatedModelName }: { relatedModelName: string },
cascades: EntityCascades<string[], string[]>
) {
const foreignKeyName = camelToSnakeCase(`${relationship.name}Id`)
const foreignKeyName =
relationship.options.foreignKeyName ??
camelToSnakeCase(`${relationship.name}Id`)
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
let mappedBy: string | undefined = camelToSnakeCase(MikroORMEntity.name)
@@ -189,21 +193,83 @@ export function defineHasOneWithFKRelationship(
mappedBy = relationship.mappedBy
}
OneToOne({
const oneToOneOptions = {
entity: relatedModelName,
fieldName: foreignKeyName,
...(relationship.nullable ? { nullable: relationship.nullable } : {}),
...(mappedBy ? { mappedBy } : {}),
cascade: shouldRemoveRelated
? (["persist", "soft-remove"] as any)
: undefined,
} as OneToOneOptions<any, any>)(MikroORMEntity.prototype, relationship.name)
//orphanRemoval: true,
} as OneToOneOptions<any, any>
if (shouldRemoveRelated) {
oneToOneOptions.cascade = ["persist", "soft-remove"] as any
}
OneToOne(oneToOneOptions)(MikroORMEntity.prototype, relationship.name)
Property({
type: "string",
columnType: "text",
nullable: relationship.nullable,
persist: true,
persist: false,
formula(alias) {
return alias + "." + foreignKeyName
},
})(MikroORMEntity.prototype, foreignKeyName)
const hookFactory = function (
name: string,
type: "init" | "create" | "update",
hookFn: Function
) {
MikroORMEntity.prototype[name] = function (
this: typeof MikroORMEntity.prototype
) {
if (type !== "update") {
// During creation
const relationMeta = this.__meta.relations.find(
(relation) => relation.name === relationship.name
).targetMeta
this[relationship.name] ??= rel(
relationMeta.class,
this[foreignKeyName]
)
this[foreignKeyName] ??= this[relationship.name]?.id
return
}
if (this[relationship.name]) {
this[foreignKeyName] = this[relationship.name].id
}
if (this[relationship.name] === null) {
this[foreignKeyName] = null
}
return
}
hookFn()(MikroORMEntity.prototype, name)
}
/**
* Hook to handle foreign key assignation
*/
hookFactory(
`assignRelationFromForeignKeyValue${foreignKeyName}_init`,
"init",
OnInit
)
hookFactory(
`assignRelationFromForeignKeyValue${foreignKeyName}_create`,
"create",
BeforeCreate
)
hookFactory(
`assignRelationFromForeignKeyValue${foreignKeyName}_update`,
"update",
BeforeUpdate
)
}
/**
@@ -261,82 +327,108 @@ export function defineBelongsToRelationship(
*/
const shouldCascade = !!relationCascades.delete?.includes(mappedBy)
/**
* Ensure the mapped by is defined as relationship on the other side
*/
if (!otherSideRelation) {
throw new Error(
`Missing property "${mappedBy}" on "${relatedModelName}" entity. Make sure to define it as a relationship`
)
}
function applyForeignKeyAssignationHooks(foreignKeyName: string) {
const hookName = `assignRelationFromForeignKeyValue${foreignKeyName}`
/**
* Hook to handle foreign key assignation
*/
MikroORMEntity.prototype[hookName] = function () {
/**
* In case of has one relation, in order to be able to have both ways
* to associate a relation (through the relation or the foreign key) we need to handle it
* specifically
*/
if (
HasOne.isHasOne(otherSideRelation) ||
HasOneWithForeignKey.isHasOneWithForeignKey(otherSideRelation)
const hookFactory = function (
name: string,
type: "init" | "create" | "update",
hookFn: Function
) {
MikroORMEntity.prototype[name] = function (
this: typeof MikroORMEntity.prototype
) {
const relationMeta = this.__meta.relations.find(
(relation) => relation.name === relationship.name
).targetMeta
this[relationship.name] ??= rel(
relationMeta.class,
this[foreignKeyName]
)
this[relationship.name] ??= this[relationship.name]?.id
return
}
/**
* In case of has one relation, in order to be able to have both ways
* to associate a relation (through the relation or the foreign key) we need to handle it
* specifically
*/
if (
HasOne.isHasOne(otherSideRelation) ||
HasOneWithForeignKey.isHasOneWithForeignKey(otherSideRelation)
) {
if (type !== "update") {
// During creation
const relationMeta = this.__meta.relations.find(
(relation) => relation.name === relationship.name
).targetMeta
this[relationship.name] ??= rel(
relationMeta.class,
this[foreignKeyName]
)
this[foreignKeyName] ??= this[relationship.name]?.id
/**
* Do not override the existing foreign key value if
* exists
*/
if (this[foreignKeyName] !== undefined) {
return
}
return
}
/**
* Set the foreign key when the relationship is initialized
* as null
*/
if (this[relationship.name] === null) {
this[foreignKeyName] = null
return
}
if (this[relationship.name]) {
this[foreignKeyName] = this[relationship.name].id
}
/**
* Set the foreign key when the relationship is initialized
* and as the id
*/
if (this[relationship.name] && "id" in this[relationship.name]) {
this[foreignKeyName] = this[relationship.name].id
if (this[relationship.name] === null) {
this[foreignKeyName] = null
}
return
}
/**
* Do not override the existing foreign key value if
* exists
*/
if (this[foreignKeyName] !== undefined) {
return
}
/**
* Set the foreign key when the relationship is initialized
* as null
*/
if (this[relationship.name] === null) {
this[foreignKeyName] = null
return
}
/**
* Set the foreign key when the relationship is initialized
* and as the id
*/
if (this[relationship.name] && "id" in this[relationship.name]) {
this[foreignKeyName] = this[relationship.name].id
}
}
hookFn()(MikroORMEntity.prototype, name)
}
/**
* Execute hook via lifecycle decorators
* Hook to handle foreign key assignation
*/
BeforeCreate()(MikroORMEntity.prototype, hookName)
OnInit()(MikroORMEntity.prototype, hookName)
hookFactory(
`assignRelationFromForeignKeyValue${foreignKeyName}_init`,
"init",
OnInit
)
hookFactory(
`assignRelationFromForeignKeyValue${foreignKeyName}_create`,
"create",
BeforeCreate
)
hookFactory(
`assignRelationFromForeignKeyValue${foreignKeyName}_update`,
"update",
BeforeUpdate
)
}
/**
* Otherside is a has many. Hence we should defined a ManyToOne
*/
if (
!otherSideRelation ||
HasMany.isHasMany(otherSideRelation) ||
DmlManyToMany.isManyToMany(otherSideRelation)
) {
const foreignKeyName = camelToSnakeCase(`${relationship.name}Id`)
const foreignKeyName =
relationship.options.foreignKeyName ??
camelToSnakeCase(`${relationship.name}Id`)
const detachCascade =
!!relationship.mappedBy &&
relationCascades.detach?.includes(relationship.mappedBy)
@@ -391,20 +483,18 @@ export function defineBelongsToRelationship(
HasOne.isHasOne(otherSideRelation) ||
HasOneWithForeignKey.isHasOneWithForeignKey(otherSideRelation)
) {
const foreignKeyName = camelToSnakeCase(`${relationship.name}Id`)
Object.defineProperty(MikroORMEntity.prototype, foreignKeyName, {
value: null,
configurable: true,
enumerable: true,
writable: true,
})
const foreignKeyName =
relationship.options.foreignKeyName ??
camelToSnakeCase(`${relationship.name}Id`)
Property({
columnType: "text",
type: "string",
nullable: relationship.nullable,
persist: false,
formula(alias) {
return alias + "." + foreignKeyName
},
})(MikroORMEntity.prototype, foreignKeyName)
const oneToOneOptions: Parameters<typeof OneToOne>[0] = {
@@ -413,6 +503,7 @@ export function defineBelongsToRelationship(
mappedBy: mappedBy,
fieldName: foreignKeyName,
owner: true,
// orphanRemoval: true,
onDelete: shouldCascade ? "cascade" : undefined,
}
@@ -1,11 +1,15 @@
import { BaseRelationship } from "./base"
import { RelationNullableModifier } from "./nullable"
export class BelongsTo<T> extends BaseRelationship<T> {
export class BelongsTo<
T,
const OptionalForeignKeyName extends string | undefined = undefined
> extends BaseRelationship<T> {
type = "belongsTo" as const
declare $foreignKey: true
declare $foreignKeyName: OptionalForeignKeyName
static isBelongsTo<T>(relationship: any): relationship is BelongsTo<T> {
static isBelongsTo<T>(relationship: any): relationship is BelongsTo<T, any> {
return relationship?.type === "belongsTo"
}
@@ -13,6 +17,10 @@ export class BelongsTo<T> extends BaseRelationship<T> {
* Apply nullable modifier on the schema
*/
nullable() {
return new RelationNullableModifier<T, BelongsTo<T>, true>(this)
return new RelationNullableModifier<
T,
BelongsTo<T, OptionalForeignKeyName>,
true
>(this)
}
}
@@ -11,13 +11,17 @@ import { RelationNullableModifier } from "./nullable"
* You may use the "BelongsTo" relationship to define the inverse
* of the "HasOne" relationship
*/
export class HasOneWithForeignKey<T> extends BaseRelationship<T> {
export class HasOneWithForeignKey<
T,
const OptionalForeignKeyName extends string | undefined = undefined
> extends BaseRelationship<T> {
type = "hasOneWithFK" as const
declare $foreignKey: true
declare $foreignKeyName: OptionalForeignKeyName
static isHasOneWithForeignKey<T>(
relationship: any
): relationship is HasOneWithForeignKey<T> {
): relationship is HasOneWithForeignKey<T, any> {
return relationship?.type === "hasOneWithFK"
}
@@ -25,6 +29,10 @@ export class HasOneWithForeignKey<T> extends BaseRelationship<T> {
* Apply nullable modifier on the schema
*/
nullable() {
return new RelationNullableModifier<T, HasOneWithForeignKey<T>, true>(this)
return new RelationNullableModifier<
T,
HasOneWithForeignKey<T, OptionalForeignKeyName>,
true
>(this)
}
}