diff --git a/.changeset/healthy-ears-agree.md b/.changeset/healthy-ears-agree.md new file mode 100644 index 0000000000..de0c5ee5c6 --- /dev/null +++ b/.changeset/healthy-ears-agree.md @@ -0,0 +1,7 @@ +--- +"@medusajs/customer": patch +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +Feat/customer dml diff --git a/packages/core/types/src/dml/index.ts b/packages/core/types/src/dml/index.ts index 3dbf20968d..7009bc019f 100644 --- a/packages/core/types/src/dml/index.ts +++ b/packages/core/types/src/dml/index.ts @@ -242,12 +242,13 @@ export type Infer = T extends IDmlEntity * The actions to cascade from a given entity to its * relationship. */ -export type EntityCascades = { +export type EntityCascades = { /** * The related models to delete when a record of this data model * is deleted. */ - delete?: Relationships + delete?: DeletableRelationships + detach?: DetachableRelationships } /** 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 22130997b8..713d776954 100644 --- a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts +++ b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts @@ -6231,6 +6231,86 @@ describe("Entity builder", () => { }) }) + test("should define onDelete cascade on pivot entity when applying detach cascade", () => { + const teamUser = model.define("teamUser", { + id: model.number(), + user: model.belongsTo(() => user, { mappedBy: "teams" }), + team: model.belongsTo(() => team, { mappedBy: "users" }), + }) + const user = model + .define("user", { + id: model.number(), + username: model.text(), + teams: model.manyToMany(() => team, { + pivotEntity: () => teamUser, + }), + }) + .cascades({ + detach: ["teams"], + }) + + const team = model + .define("team", { + id: model.number(), + name: model.text(), + users: model.manyToMany(() => user, { + pivotEntity: () => teamUser, + }), + }) + .cascades({ + detach: ["users"], + }) + + type CascadeDetach = Parameters<(typeof team)["cascades"]>[0]["detach"] + + expectTypeOf().toEqualTypeOf<"users"[] | undefined>() + + const [, , TeamUserEntity] = toMikroOrmEntities([user, team, teamUser]) + + const teamUserMetadata = + MetadataStorage.getMetadataFromDecorator(TeamUserEntity) + expect(teamUserMetadata.properties).toEqual( + expect.objectContaining({ + user_id: { + reference: "scalar", + type: "User", + columnType: "text", + fieldName: "user_id", + nullable: false, + name: "user_id", + getter: false, + setter: false, + }, + user: { + name: "user", + reference: "m:1", + entity: "User", + nullable: false, + persist: false, + onDelete: "cascade", + }, + team_id: { + reference: "scalar", + type: "Team", + columnType: "text", + fieldName: "team_id", + nullable: false, + name: "team_id", + getter: false, + setter: false, + }, + team: { + name: "team", + reference: "m:1", + entity: "Team", + nullable: false, + persist: false, + onDelete: "cascade", + }, + }) + ) + }) + test("throw error when unable to locate relationship via mappedBy", () => { const team = model.define("team", { id: model.number(), diff --git a/packages/core/utils/src/dml/entity.ts b/packages/core/utils/src/dml/entity.ts index b4d00755b6..ba77e613de 100644 --- a/packages/core/utils/src/dml/entity.ts +++ b/packages/core/utils/src/dml/entity.ts @@ -71,7 +71,7 @@ export class DmlEntity< schema: Schema readonly #tableName: string - #cascades: EntityCascades = {} + #cascades: EntityCascades = {} #indexes: EntityIndex[] = [] #checks: CheckConstraint[] = [] @@ -100,7 +100,7 @@ export class DmlEntity< name: InferDmlEntityNameFromConfig tableName: string schema: DMLSchema - cascades: EntityCascades + cascades: EntityCascades indexes: EntityIndex[] checks: CheckConstraint[] } { @@ -139,7 +139,8 @@ export class DmlEntity< */ cascades( options: EntityCascades< - ExtractEntityRelations + ExtractEntityRelations, + ExtractEntityRelations > ) { const childToParentCascades = options.delete?.filter((relationship) => { diff --git a/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts b/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts index 8f95e0e03f..0d506b91f3 100644 --- a/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts +++ b/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts @@ -144,7 +144,7 @@ export function defineHasOneRelationship( any >, { relatedModelName }: { relatedModelName: string }, - cascades: EntityCascades + cascades: EntityCascades ) { const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name) const { schema: relationSchema } = relatedEntity.parse() @@ -179,7 +179,7 @@ export function defineHasOneWithFKRelationship( MikroORMEntity: EntityConstructor, relationship: RelationshipMetadata, { relatedModelName }: { relatedModelName: string }, - cascades: EntityCascades + cascades: EntityCascades ) { const foreignKeyName = camelToSnakeCase(`${relationship.name}Id`) const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name) @@ -213,7 +213,7 @@ export function defineHasManyRelationship( MikroORMEntity: EntityConstructor, relationship: RelationshipMetadata, { relatedModelName }: { relatedModelName: string }, - cascades: EntityCascades + cascades: EntityCascades ) { const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name) @@ -259,7 +259,7 @@ export function defineBelongsToRelationship( * define a onDelete: cascade when we are included in the delete * list of parent cascade. */ - const shouldCascade = relationCascades.delete?.includes(mappedBy) + const shouldCascade = !!relationCascades.delete?.includes(mappedBy) /** * Ensure the mapped by is defined as relationship on the other side @@ -337,6 +337,9 @@ export function defineBelongsToRelationship( DmlManyToMany.isManyToMany(otherSideRelation) ) { const foreignKeyName = camelToSnakeCase(`${relationship.name}Id`) + const detachCascade = + !!relationship.mappedBy && + relationCascades.detach?.includes(relationship.mappedBy) if (DmlManyToMany.isManyToMany(otherSideRelation)) { Property({ @@ -350,6 +353,7 @@ export function defineBelongsToRelationship( entity: relatedModelName, nullable: relationship.nullable, persist: false, + onDelete: shouldCascade || detachCascade ? "cascade" : undefined, })(MikroORMEntity.prototype, relationship.name) } else { ManyToOne({ @@ -660,7 +664,7 @@ export function defineRelationship( MikroORMEntity: EntityConstructor, entity: DmlEntity, relationship: RelationshipMetadata, - cascades: EntityCascades, + cascades: EntityCascades, context: Context ) { /** diff --git a/packages/modules/customer/integration-tests/__tests__/services/customer-module/index.spec.ts b/packages/modules/customer/integration-tests/__tests__/services/customer-module/index.spec.ts index 275b64238b..530a9904ce 100644 --- a/packages/modules/customer/integration-tests/__tests__/services/customer-module/index.spec.ts +++ b/packages/modules/customer/integration-tests/__tests__/services/customer-module/index.spec.ts @@ -16,9 +16,9 @@ moduleIntegrationTestRunner({ expect(Object.keys(linkable)).toEqual([ "customerAddress", - "customerGroupCustomer", - "customerGroup", "customer", + "customerGroup", + "customerGroupCustomer", ]) Object.keys(linkable).forEach((key) => { diff --git a/packages/modules/customer/src/migrations/.snapshot-medusa-customer.json b/packages/modules/customer/src/migrations/.snapshot-medusa-customer.json index e5c88e7365..7a56c83375 100644 --- a/packages/modules/customer/src/migrations/.snapshot-medusa-customer.json +++ b/packages/modules/customer/src/migrations/.snapshot-medusa-customer.json @@ -1,5 +1,7 @@ { - "namespaces": ["public"], + "namespaces": [ + "public" + ], "name": "public", "tables": [ { @@ -77,6 +79,15 @@ "nullable": true, "mappedType": "json" }, + "created_by": { + "name": "created_by", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, "created_at": { "name": "created_at", "type": "timestamptz", @@ -108,20 +119,19 @@ "nullable": true, "length": 6, "mappedType": "datetime" - }, - "created_by": { - "name": "created_by", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "text" } }, "name": "customer", "schema": "public", "indexes": [ + { + "keyName": "IDX_customer_deleted_at", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_customer_deleted_at\" ON \"customer\" (deleted_at) WHERE deleted_at IS NULL" + }, { "keyName": "IDX_customer_email_has_account_unique", "columnNames": [], @@ -132,7 +142,9 @@ }, { "keyName": "customer_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -181,15 +193,6 @@ "default": "false", "mappedType": "boolean" }, - "customer_id": { - "name": "customer_id", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "mappedType": "text" - }, "company": { "name": "company", "type": "text", @@ -289,6 +292,15 @@ "nullable": true, "mappedType": "json" }, + "customer_id": { + "name": "customer_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, "created_at": { "name": "created_at", "type": "timestamptz", @@ -310,17 +322,36 @@ "length": 6, "default": "now()", "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" } }, "name": "customer_address", "schema": "public", "indexes": [ { - "columnNames": ["customer_id"], - "composite": false, "keyName": "IDX_customer_address_customer_id", + "columnNames": [], + "composite": false, "primary": false, - "unique": false + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_customer_address_customer_id\" ON \"customer_address\" (customer_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_customer_address_deleted_at", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_customer_address_deleted_at\" ON \"customer_address\" (deleted_at) WHERE deleted_at IS NULL" }, { "keyName": "IDX_customer_address_unique_customer_billing", @@ -328,7 +359,7 @@ "composite": false, "primary": false, "unique": false, - "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_customer_address_unique_customer_billing\" ON \"customer_address\" (customer_id) WHERE \"is_default_billing\" = true" + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_customer_address_unique_customer_billing\" ON \"customer_address\" (customer_id) WHERE \"is_default_billing\" = true AND deleted_at IS NULL" }, { "keyName": "IDX_customer_address_unique_customer_shipping", @@ -336,11 +367,13 @@ "composite": false, "primary": false, "unique": false, - "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_customer_address_unique_customer_shipping\" ON \"customer_address\" (customer_id) WHERE \"is_default_shipping\" = true" + "expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_customer_address_unique_customer_shipping\" ON \"customer_address\" (customer_id) WHERE \"is_default_shipping\" = true AND deleted_at IS NULL" }, { "keyName": "customer_address_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -350,9 +383,13 @@ "foreignKeys": { "customer_address_customer_id_foreign": { "constraintName": "customer_address_customer_id_foreign", - "columnNames": ["customer_id"], + "columnNames": [ + "customer_id" + ], "localTableName": "public.customer_address", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.customer", "deleteRule": "cascade", "updateRule": "cascade" @@ -433,9 +470,17 @@ "name": "customer_group", "schema": "public", "indexes": [ + { + "keyName": "IDX_customer_group_deleted_at", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_customer_group_deleted_at\" ON \"customer_group\" (deleted_at) WHERE deleted_at IS NULL" + }, { "keyName": "IDX_customer_group_name_unique", - "columnNames": ["name"], + "columnNames": [], "composite": false, "primary": false, "unique": false, @@ -443,7 +488,9 @@ }, { "keyName": "customer_group_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -463,6 +510,24 @@ "nullable": false, "mappedType": "text" }, + "created_by": { + "name": "created_by", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, "customer_id": { "name": "customer_id", "type": "text", @@ -481,15 +546,6 @@ "nullable": false, "mappedType": "text" }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "json" - }, "created_at": { "name": "created_at", "type": "timestamptz", @@ -512,60 +568,56 @@ "default": "now()", "mappedType": "datetime" }, - "created_by": { - "name": "created_by", - "type": "text", + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", "unsigned": false, "autoincrement": false, "primary": false, "nullable": true, - "mappedType": "text" + "length": 6, + "mappedType": "datetime" } }, "name": "customer_group_customer", "schema": "public", "indexes": [ { - "columnNames": ["customer_group_id"], + "keyName": "IDX_customer_group_customer_customer_id", + "columnNames": [], "composite": false, - "keyName": "IDX_customer_group_customer_group_id", "primary": false, - "unique": false + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_customer_group_customer_customer_id\" ON \"customer_group_customer\" (customer_id) WHERE deleted_at IS NULL" }, { - "columnNames": ["customer_id"], + "keyName": "IDX_customer_group_customer_customer_group_id", + "columnNames": [], "composite": false, - "keyName": "IDX_customer_group_customer_customer_id", "primary": false, - "unique": false + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_customer_group_customer_customer_group_id\" ON \"customer_group_customer\" (customer_group_id) WHERE deleted_at IS NULL" + }, + { + "keyName": "IDX_customer_group_customer_deleted_at", + "columnNames": [], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_customer_group_customer_deleted_at\" ON \"customer_group_customer\" (deleted_at) WHERE deleted_at IS NULL" }, { "keyName": "customer_group_customer_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true } ], "checks": [], - "foreignKeys": { - "customer_group_customer_customer_group_id_foreign": { - "constraintName": "customer_group_customer_customer_group_id_foreign", - "columnNames": ["customer_group_id"], - "localTableName": "public.customer_group_customer", - "referencedColumnNames": ["id"], - "referencedTableName": "public.customer_group", - "deleteRule": "cascade" - }, - "customer_group_customer_customer_id_foreign": { - "constraintName": "customer_group_customer_customer_id_foreign", - "columnNames": ["customer_id"], - "localTableName": "public.customer_group_customer", - "referencedColumnNames": ["id"], - "referencedTableName": "public.customer", - "deleteRule": "cascade" - } - } + "foreignKeys": {} } ] } diff --git a/packages/modules/customer/src/migrations/Migration20241211074630.ts b/packages/modules/customer/src/migrations/Migration20241211074630.ts new file mode 100644 index 0000000000..e4751ad12a --- /dev/null +++ b/packages/modules/customer/src/migrations/Migration20241211074630.ts @@ -0,0 +1,75 @@ +import { Migration } from "@mikro-orm/migrations" + +export class Migration20241211074630 extends Migration { + async up(): Promise { + this.addSql( + 'alter table if exists "customer_group_customer" drop constraint if exists "customer_group_customer_customer_group_id_foreign";' + ) + this.addSql( + 'alter table if exists "customer_group_customer" drop constraint if exists "customer_group_customer_customer_id_foreign";' + ) + + this.addSql( + 'CREATE INDEX IF NOT EXISTS "IDX_customer_deleted_at" ON "customer" (deleted_at) WHERE deleted_at IS NULL;' + ) + + this.addSql( + 'alter table if exists "customer_address" add column if not exists "deleted_at" timestamptz null;' + ) + this.addSql( + 'CREATE INDEX IF NOT EXISTS "IDX_customer_address_deleted_at" ON "customer_address" (deleted_at) WHERE deleted_at IS NULL;' + ) + + this.addSql( + 'CREATE INDEX IF NOT EXISTS "IDX_customer_group_deleted_at" ON "customer_group" (deleted_at) WHERE deleted_at IS NULL;' + ) + + this.addSql( + 'alter table if exists "customer_group_customer" add column if not exists "deleted_at" timestamptz null;' + ) + this.addSql('drop index if exists "IDX_customer_group_customer_group_id";') + this.addSql( + 'alter table if exists "customer_group_customer" add constraint "customer_group_customer_customer_group_id_foreign" foreign key ("customer_group_id") references "customer_group" ("id") on update cascade on delete cascade;' + ) + this.addSql( + 'alter table if exists "customer_group_customer" add constraint "customer_group_customer_customer_id_foreign" foreign key ("customer_id") references "customer" ("id") on update cascade on delete cascade;' + ) + + this.addSql( + 'CREATE INDEX IF NOT EXISTS "IDX_customer_group_customer_customer_group_id" ON "customer_group_customer" (customer_group_id) WHERE deleted_at IS NULL;' + ) + this.addSql( + 'CREATE INDEX IF NOT EXISTS "IDX_customer_group_customer_deleted_at" ON "customer_group_customer" (deleted_at) WHERE deleted_at IS NULL;' + ) + } + + async down(): Promise { + this.addSql('drop index if exists "IDX_customer_deleted_at";') + + this.addSql('drop index if exists "IDX_customer_address_deleted_at";') + this.addSql( + 'alter table if exists "customer_address" drop column if exists "deleted_at";' + ) + + this.addSql('drop index if exists "IDX_customer_group_deleted_at";') + + this.addSql( + 'drop index if exists "IDX_customer_group_customer_customer_group_id";' + ) + this.addSql( + 'drop index if exists "IDX_customer_group_customer_deleted_at";' + ) + this.addSql( + 'alter table if exists "customer_group_customer" drop column if exists "deleted_at";' + ) + this.addSql( + 'alter table if exists "customer_group_customer" add constraint "customer_group_customer_customer_group_id_foreign" foreign key ("customer_group_id") references "customer_group" ("id") on delete cascade;' + ) + this.addSql( + 'alter table if exists "customer_group_customer" add constraint "customer_group_customer_customer_id_foreign" foreign key ("customer_id") references "customer" ("id") on delete cascade;' + ) + this.addSql( + 'create index if not exists "IDX_customer_group_customer_group_id" on "customer_group_customer" ("customer_group_id");' + ) + } +} diff --git a/packages/modules/customer/src/models/address.ts b/packages/modules/customer/src/models/address.ts index 81f9d92b66..c4653fa098 100644 --- a/packages/modules/customer/src/models/address.ts +++ b/packages/modules/customer/src/models/address.ts @@ -1,133 +1,40 @@ -import { DAL } from "@medusajs/framework/types" -import { - createPsqlIndexStatementHelper, - generateEntityId, - Searchable, -} from "@medusajs/framework/utils" -import { - BeforeCreate, - Cascade, - Entity, - ManyToOne, - OnInit, - OptionalProps, - PrimaryKey, - Property, -} from "@mikro-orm/core" +import { model } from "@medusajs/framework/utils" import Customer from "./customer" -type OptionalAddressProps = DAL.ModelDateColumns // TODO: To be revisited when more clear - -const CustomerAddressUniqueCustomerShippingAddress = - createPsqlIndexStatementHelper({ - name: "IDX_customer_address_unique_customer_shipping", - tableName: "customer_address", - columns: "customer_id", - unique: true, - where: '"is_default_shipping" = true', +const CustomerAddress = model + .define("CustomerAddress", { + id: model.id({ prefix: "cuaddr" }).primaryKey(), + address_name: model.text().searchable().nullable(), + is_default_shipping: model.boolean().default(false), + is_default_billing: model.boolean().default(false), + company: model.text().searchable().nullable(), + first_name: model.text().searchable().nullable(), + last_name: model.text().searchable().nullable(), + address_1: model.text().searchable().nullable(), + address_2: model.text().searchable().nullable(), + city: model.text().searchable().nullable(), + country_code: model.text().nullable(), + province: model.text().searchable().nullable(), + postal_code: model.text().searchable().nullable(), + phone: model.text().nullable(), + metadata: model.json().nullable(), + customer: model.belongsTo(() => Customer, { + mappedBy: "addresses", + }), }) + .indexes([ + { + name: "IDX_customer_address_unique_customer_billing", + on: ["customer_id"], + unique: true, + where: '"is_default_billing" = true', + }, + { + name: "IDX_customer_address_unique_customer_shipping", + on: ["customer_id"], + unique: true, + where: '"is_default_shipping" = true', + }, + ]) -const CustomerAddressUniqueCustomerBillingAddress = - createPsqlIndexStatementHelper({ - name: "IDX_customer_address_unique_customer_billing", - tableName: "customer_address", - columns: "customer_id", - unique: true, - where: '"is_default_billing" = true', - }) - -@Entity({ tableName: "customer_address" }) -@CustomerAddressUniqueCustomerShippingAddress.MikroORMIndex() -@CustomerAddressUniqueCustomerBillingAddress.MikroORMIndex() -export default class CustomerAddress { - [OptionalProps]: OptionalAddressProps - - @PrimaryKey({ columnType: "text" }) - id!: string - - @Searchable() - @Property({ columnType: "text", nullable: true }) - address_name: string | null = null - - @Property({ columnType: "boolean", default: false }) - is_default_shipping: boolean = false - - @Property({ columnType: "boolean", default: false }) - is_default_billing: boolean = false - - @Property({ columnType: "text" }) - customer_id: string - - @ManyToOne(() => Customer, { - fieldName: "customer_id", - index: "IDX_customer_address_customer_id", - cascade: [Cascade.REMOVE, Cascade.PERSIST], - }) - customer: Customer - - @Searchable() - @Property({ columnType: "text", nullable: true }) - company: string | null = null - - @Searchable() - @Property({ columnType: "text", nullable: true }) - first_name: string | null = null - - @Searchable() - @Property({ columnType: "text", nullable: true }) - last_name: string | null = null - - @Searchable() - @Property({ columnType: "text", nullable: true }) - address_1: string | null = null - - @Searchable() - @Property({ columnType: "text", nullable: true }) - address_2: string | null = null - - @Searchable() - @Property({ columnType: "text", nullable: true }) - city: string | null = null - - @Property({ columnType: "text", nullable: true }) - country_code: string | null = null - - @Searchable() - @Property({ columnType: "text", nullable: true }) - province: string | null = null - - @Searchable() - @Property({ columnType: "text", nullable: true }) - postal_code: string | null = null - - @Property({ columnType: "text", nullable: true }) - phone: string | null = null - - @Property({ columnType: "jsonb", nullable: true }) - metadata: Record | null = null - - @Property({ - onCreate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", - }) - created_at: Date - - @Property({ - onCreate: () => new Date(), - onUpdate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", - }) - updated_at: Date - - @BeforeCreate() - onCreate() { - this.id = generateEntityId(this.id, "cuaddr") - } - - @OnInit() - onInit() { - this.id = generateEntityId(this.id, "cuaddr") - } -} +export default CustomerAddress diff --git a/packages/modules/customer/src/models/customer-group-customer.ts b/packages/modules/customer/src/models/customer-group-customer.ts index 83fc538306..fdd2d6dc11 100644 --- a/packages/modules/customer/src/models/customer-group-customer.ts +++ b/packages/modules/customer/src/models/customer-group-customer.ts @@ -1,78 +1,17 @@ -import { DAL } from "@medusajs/framework/types" -import { generateEntityId } from "@medusajs/framework/utils" -import { - BeforeCreate, - Cascade, - Entity, - ManyToOne, - OnInit, - OptionalProps, - PrimaryKey, - Property, - Rel, -} from "@mikro-orm/core" +import { model } from "@medusajs/framework/utils" import Customer from "./customer" import CustomerGroup from "./customer-group" -type OptionalGroupProps = "customer_group" | "customer" | DAL.ModelDateColumns // TODO: To be revisited when more clear +const CustomerGroupCustomer = model.define("CustomerGroupCustomer", { + id: model.id({ prefix: "cusgc" }).primaryKey(), + created_by: model.text().nullable(), + metadata: model.json().nullable(), + customer: model.belongsTo(() => Customer, { + mappedBy: "groups", + }), + customer_group: model.belongsTo(() => CustomerGroup, { + mappedBy: "customers", + }), +}) -@Entity({ tableName: "customer_group_customer" }) -export default class CustomerGroupCustomer { - [OptionalProps]: OptionalGroupProps - - @PrimaryKey({ columnType: "text" }) - id!: string - - @Property({ columnType: "text" }) - customer_id: string - - @Property({ columnType: "text" }) - customer_group_id: string - - @ManyToOne({ - entity: () => Customer, - fieldName: "customer_id", - index: "IDX_customer_group_customer_customer_id", - cascade: [Cascade.REMOVE], - }) - customer: Rel - - @ManyToOne({ - entity: () => CustomerGroup, - fieldName: "customer_group_id", - index: "IDX_customer_group_customer_group_id", - cascade: [Cascade.REMOVE], - }) - customer_group: Rel - - @Property({ columnType: "jsonb", nullable: true }) - metadata: Record | null = null - - @Property({ - onCreate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", - }) - created_at: Date - - @Property({ - onCreate: () => new Date(), - onUpdate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", - }) - updated_at: Date - - @Property({ columnType: "text", nullable: true }) - created_by: string | null = null - - @BeforeCreate() - onCreate() { - this.id = generateEntityId(this.id, "cusgc") - } - - @OnInit() - onInit() { - this.id = generateEntityId(this.id, "cusgc") - } -} +export default CustomerGroupCustomer diff --git a/packages/modules/customer/src/models/customer-group.ts b/packages/modules/customer/src/models/customer-group.ts index 3afe8ba8a3..a43389672e 100644 --- a/packages/modules/customer/src/models/customer-group.ts +++ b/packages/modules/customer/src/models/customer-group.ts @@ -1,84 +1,27 @@ -import { DAL } from "@medusajs/framework/types" -import { - DALUtils, - Searchable, - createPsqlIndexStatementHelper, - generateEntityId, -} from "@medusajs/framework/utils" -import { - BeforeCreate, - Collection, - Entity, - Filter, - ManyToMany, - OnInit, - OptionalProps, - PrimaryKey, - Property, - Rel, -} from "@mikro-orm/core" +import { model } from "@medusajs/framework/utils" import Customer from "./customer" -import CustomerGroupCustomer from "./customer-group-customer" +import { CustomerGroupCustomer } from "@models" -type OptionalGroupProps = DAL.SoftDeletableModelDateColumns // TODO: To be revisited when more clear - -const CustomerGroupUniqueName = createPsqlIndexStatementHelper({ - tableName: "customer_group", - columns: ["name"], - unique: true, - where: "deleted_at IS NULL", -}) - -@Entity({ tableName: "customer_group" }) -@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) -export default class CustomerGroup { - [OptionalProps]: OptionalGroupProps - - @PrimaryKey({ columnType: "text" }) - id!: string - - @Searchable() - @CustomerGroupUniqueName.MikroORMIndex() - @Property({ columnType: "text" }) - name: string - - @ManyToMany({ - entity: () => Customer, - pivotEntity: () => CustomerGroupCustomer, +const CustomerGroup = model + .define("CustomerGroup", { + id: model.id({ prefix: "cusgroup" }).primaryKey(), + name: model.text().searchable(), + metadata: model.json().nullable(), + created_by: model.text().nullable(), + customers: model.manyToMany(() => Customer, { + mappedBy: "groups", + pivotEntity: () => CustomerGroupCustomer, + }), }) - customers = new Collection>(this) - - @Property({ columnType: "jsonb", nullable: true }) - metadata: Record | null = null - - @Property({ columnType: "text", nullable: true }) - created_by: string | null = null - - @Property({ - onCreate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", + .indexes([ + { + on: ["name"], + unique: true, + where: "deleted_at IS NULL", + }, + ]) + .cascades({ + detach: ["customers"], }) - created_at: Date - @Property({ - onCreate: () => new Date(), - onUpdate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", - }) - updated_at: Date - - @Property({ columnType: "timestamptz", nullable: true }) - deleted_at: Date | null = null - - @BeforeCreate() - onCreate() { - this.id = generateEntityId(this.id, "cusgroup") - } - - @OnInit() - onInit() { - this.id = generateEntityId(this.id, "cusgroup") - } -} +export default CustomerGroup diff --git a/packages/modules/customer/src/models/customer.ts b/packages/modules/customer/src/models/customer.ts index 018f09cc2a..1d8508043f 100644 --- a/packages/modules/customer/src/models/customer.ts +++ b/packages/modules/customer/src/models/customer.ts @@ -1,115 +1,37 @@ -import { DAL } from "@medusajs/framework/types" -import { - createPsqlIndexStatementHelper, - DALUtils, - generateEntityId, - Searchable, -} from "@medusajs/framework/utils" -import { - BeforeCreate, - Cascade, - Collection, - Entity, - Filter, - ManyToMany, - OneToMany, - OnInit, - OptionalProps, - PrimaryKey, - Property, - Rel, -} from "@mikro-orm/core" +import { model } from "@medusajs/framework/utils" import CustomerAddress from "./address" import CustomerGroup from "./customer-group" import CustomerGroupCustomer from "./customer-group-customer" -type OptionalCustomerProps = - | "groups" - | "addresses" - | DAL.SoftDeletableModelDateColumns - -const CustomerUniqueEmail = createPsqlIndexStatementHelper({ - tableName: "customer", - columns: ["email", "has_account"], - unique: true, - where: "deleted_at IS NULL", -}) - -@Entity({ tableName: "customer" }) -@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) -@CustomerUniqueEmail.MikroORMIndex() -export default class Customer { - [OptionalProps]?: OptionalCustomerProps - - @PrimaryKey({ columnType: "text" }) - id: string - - @Searchable() - @Property({ columnType: "text", nullable: true }) - company_name: string | null = null - - @Searchable() - @Property({ columnType: "text", nullable: true }) - first_name: string | null = null - - @Searchable() - @Property({ columnType: "text", nullable: true }) - last_name: string | null = null - - @Searchable() - @Property({ columnType: "text", nullable: true }) - email: string | null = null - - @Searchable() - @Property({ columnType: "text", nullable: true }) - phone: string | null = null - - @Property({ columnType: "boolean", default: false }) - has_account: boolean = false - - @Property({ columnType: "jsonb", nullable: true }) - metadata: Record | null = null - - @ManyToMany({ - mappedBy: "customers", - entity: () => CustomerGroup, - pivotEntity: () => CustomerGroupCustomer, +const Customer = model + .define("Customer", { + id: model.id({ prefix: "cus" }).primaryKey(), + company_name: model.text().searchable().nullable(), + first_name: model.text().searchable().nullable(), + last_name: model.text().searchable().nullable(), + email: model.text().searchable().nullable(), + phone: model.text().searchable().nullable(), + has_account: model.boolean().default(false), + metadata: model.json().nullable(), + created_by: model.text().nullable(), + groups: model.manyToMany(() => CustomerGroup, { + mappedBy: "customers", + pivotEntity: () => CustomerGroupCustomer, + }), + addresses: model.hasMany(() => CustomerAddress, { + mappedBy: "customer", + }), }) - groups = new Collection>(this) - - @OneToMany(() => CustomerAddress, (address) => address.customer, { - cascade: [Cascade.REMOVE], + .cascades({ + delete: ["addresses"], + detach: ["groups"], }) - addresses = new Collection>(this) + .indexes([ + { + on: ["email", "has_account"], + unique: true, + where: "deleted_at IS NULL", + }, + ]) - @Property({ - onCreate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", - }) - created_at: Date - - @Property({ - onCreate: () => new Date(), - onUpdate: () => new Date(), - columnType: "timestamptz", - defaultRaw: "now()", - }) - updated_at: Date - - @Property({ columnType: "timestamptz", nullable: true }) - deleted_at: Date | null = null - - @Property({ columnType: "text", nullable: true }) - created_by: string | null = null - - @BeforeCreate() - onCreate() { - this.id = generateEntityId(this.id, "cus") - } - - @OnInit() - onInit() { - this.id = generateEntityId(this.id, "cus") - } -} +export default Customer diff --git a/packages/modules/customer/src/services/customer-module.ts b/packages/modules/customer/src/services/customer-module.ts index 8ffc4ff074..8e5f82c66d 100644 --- a/packages/modules/customer/src/services/customer-module.ts +++ b/packages/modules/customer/src/services/customer-module.ts @@ -7,6 +7,7 @@ import { CustomerTypes, DAL, ICustomerModuleService, + InferEntityType, InternalModuleDeclaration, ModuleJoinerConfig, ModulesSdkTypes, @@ -51,10 +52,18 @@ export default class CustomerModuleService implements ICustomerModuleService { protected baseRepository_: DAL.RepositoryService - protected customerService_: ModulesSdkTypes.IMedusaInternalService - protected customerAddressService_: ModulesSdkTypes.IMedusaInternalService - protected customerGroupService_: ModulesSdkTypes.IMedusaInternalService - protected customerGroupCustomerService_: ModulesSdkTypes.IMedusaInternalService + protected customerService_: ModulesSdkTypes.IMedusaInternalService< + InferEntityType + > + protected customerAddressService_: ModulesSdkTypes.IMedusaInternalService< + InferEntityType + > + protected customerGroupService_: ModulesSdkTypes.IMedusaInternalService< + InferEntityType + > + protected customerGroupCustomerService_: ModulesSdkTypes.IMedusaInternalService< + InferEntityType + > constructor( { @@ -117,8 +126,14 @@ export default class CustomerModuleService @MedusaContext() sharedContext: Context = {} ): Promise { const data = Array.isArray(dataOrArray) ? dataOrArray : [dataOrArray] + const customerAttributes = data.map(({ addresses, ...rest }) => { + return rest + }) - const customers = await this.customerService_.create(data, sharedContext) + const customers = await this.customerService_.create( + customerAttributes, + sharedContext + ) const addressDataWithCustomerIds = data .map(({ addresses }, i) => { @@ -320,9 +335,11 @@ export default class CustomerModuleService ) if (Array.isArray(data)) { - return (groupCustomers as unknown as CustomerGroupCustomer[]).map( - (gc) => ({ id: gc.id }) - ) + return ( + groupCustomers as unknown as InferEntityType< + typeof CustomerGroupCustomer + >[] + ).map((gc) => ({ id: gc.id })) } return { id: groupCustomers.id }