feat(fulfillment): Soft deletes (#6630)

**What**
- Ensure soft delete works properly according to the soft delete configuration and validate all relation of the entire data model
- Add is_enabled to the providers in order to manage new providers to enabled or disabled
- include joiner config update

FIXES CORE-1853
FIXES CORE-1830
FIXES CORE-1719
This commit is contained in:
Adrien de Peretti
2024-03-11 15:56:08 +00:00
committed by GitHub
parent 78e5ec459a
commit d9d5afc3cf
23 changed files with 985 additions and 696 deletions
+56 -4
View File
@@ -1,10 +1,20 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { MapToConfig } from "@medusajs/utils"
import {
Fulfillment,
FulfillmentSet,
GeoZone,
ServiceZone,
ShippingOption,
ShippingProfile,
} from "@models"
// TODO manage the config
export const LinkableKeys: Record<string, string> = {}
export const LinkableKeys: Record<string, string> = {
fulfillment_id: Fulfillment.name,
fulfillment_set_id: FulfillmentSet.name,
shipping_option_id: ShippingOption.name,
}
const entityLinkableKeysMap: MapToConfig = {}
Object.entries(LinkableKeys).forEach(([key, value]) => {
@@ -21,5 +31,47 @@ export const joinerConfig: ModuleJoinerConfig = {
serviceName: Modules.FULFILLMENT,
primaryKeys: ["id"],
linkableKeys: LinkableKeys,
alias: [],
alias: [
{
name: ["fulfillment_set", "fulfillment_sets"],
args: {
entity: FulfillmentSet.name,
},
},
{
name: ["shipping_option", "shipping_options"],
args: {
entity: ShippingOption.name,
methodSuffix: "ShippingOptions",
},
},
{
name: ["shipping_profile", "shipping_profiles"],
args: {
entity: ShippingProfile.name,
methodSuffix: "ShippingProfiles",
},
},
{
name: ["fulfillment", "fulfillments"],
args: {
entity: Fulfillment.name,
methodSuffix: "Fulfillments",
},
},
{
name: ["service_zone", "service_zones"],
args: {
entity: ServiceZone.name,
methodSuffix: "ServiceZones",
},
},
{
name: ["geo_zone", "geo_zones"],
args: {
entity: GeoZone.name,
methodSuffix: "GeoZones",
},
},
],
} as ModuleJoinerConfig
+36 -13
View File
@@ -2,7 +2,7 @@ import { moduleProviderLoader } from "@medusajs/modules-sdk"
import { LoaderOptions, ModuleProvider, ModulesSdkTypes } from "@medusajs/types"
import { asFunction, asValue, Lifetime } from "awilix"
import { FulfillmentIdentifiersRegistrationName } from "@types"
import { lowerCaseFirst } from "@medusajs/utils"
import { lowerCaseFirst, promiseAll } from "@medusajs/utils"
import { FulfillmentProviderService } from "@services"
import { ContainerRegistrationKeys } from "@medusajs/utils/src"
@@ -56,7 +56,8 @@ async function syncDatabaseProviders({ container }) {
FulfillmentProviderService.name
)
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
const logger = container.resolve(ContainerRegistrationKeys.LOGGER) ?? console
try {
const providerIdentifiers: string[] = (
container.resolve(FulfillmentIdentifiersRegistrationName) ?? []
@@ -65,22 +66,44 @@ async function syncDatabaseProviders({ container }) {
const providerService: ModulesSdkTypes.InternalModuleService<any> =
container.resolve(providerServiceRegistrationKey)
const providers = await providerService.list({
id: providerIdentifiers,
})
const providers = await providerService.list({})
const loadedProvidersMap = new Map(providers.map((p) => [p.id, p]))
const providersToCreate: any[] = []
for (const identifier of providerIdentifiers) {
if (loadedProvidersMap.has(identifier)) {
continue
}
const providersToCreate = providerIdentifiers.filter(
(id) => !loadedProvidersMap.has(id)
)
const providersToEnabled = providerIdentifiers.filter((id) =>
loadedProvidersMap.has(id)
)
const providersToDisable = providers.filter(
(p) => !providerIdentifiers.includes(p.id)
)
providersToCreate.push({ id: identifier })
const promises: Promise<any>[] = []
if (providersToCreate.length) {
promises.push(
providerService.create(providersToCreate.map((id) => ({ id })))
)
}
await providerService.create(providersToCreate)
if (providersToEnabled.length) {
promises.push(
providerService.update(
providersToEnabled.map((id) => ({ id, is_enabled: true }))
)
)
}
if (providersToDisable.length) {
promises.push(
providerService.update(
providersToDisable.map((p) => ({ id: p.id, is_enabled: false }))
)
)
}
await promiseAll(promises)
} catch (error) {
logger.error(`Error syncing providers: ${error.message}`)
}
@@ -184,61 +184,20 @@
"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",
"is_enabled": {
"name": "is_enabled",
"type": "boolean",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"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"
"default": "true",
"mappedType": "boolean"
}
},
"name": "fulfillment_provider",
"schema": "public",
"indexes": [
{
"keyName": "IDX_fulfillment_provider_deleted_at",
"columnNames": [
"deleted_at"
],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_fulfillment_provider_deleted_at\" ON \"fulfillment_provider\" (deleted_at) WHERE deleted_at IS NOT NULL"
},
{
"keyName": "fulfillment_provider_pkey",
"columnNames": [
@@ -485,6 +444,7 @@
"id"
],
"referencedTableName": "public.fulfillment_set",
"deleteRule": "cascade",
"updateRule": "cascade"
}
}
@@ -678,6 +638,7 @@
"id"
],
"referencedTableName": "public.service_zone",
"deleteRule": "cascade",
"updateRule": "cascade"
}
}
@@ -971,7 +932,7 @@
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"nullable": false,
"mappedType": "text"
},
"created_at": {
@@ -1091,6 +1052,7 @@
"id"
],
"referencedTableName": "public.service_zone",
"deleteRule": "cascade",
"updateRule": "cascade"
},
"shipping_option_shipping_profile_id_foreign": {
@@ -1269,6 +1231,7 @@
"id"
],
"referencedTableName": "public.shipping_option",
"deleteRule": "cascade",
"updateRule": "cascade"
}
}
@@ -1348,7 +1311,7 @@
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"nullable": true,
"mappedType": "text"
},
"shipping_option_id": {
@@ -1375,7 +1338,7 @@
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"nullable": true,
"mappedType": "text"
},
"created_at": {
@@ -1485,6 +1448,7 @@
"id"
],
"referencedTableName": "public.fulfillment_provider",
"deleteRule": "set null",
"updateRule": "cascade"
},
"fulfillment_shipping_option_id_foreign": {
@@ -1510,6 +1474,7 @@
"id"
],
"referencedTableName": "public.fulfillment_address",
"deleteRule": "cascade",
"updateRule": "cascade"
}
}
@@ -1,13 +1,12 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20240305095931_InitialSetupMigration extends Migration {
export class Migration20240311145700_InitialSetupMigration extends Migration {
async up(): Promise<void> {
this.addSql('create table if not exists "fulfillment_address" ("id" text not null, "company" text null, "first_name" text null, "last_name" text null, "address_1" text null, "address_2" text null, "city" text null, "country_code" text null, "province" text null, "postal_code" text null, "phone" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_address_pkey" primary key ("id"));');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_address_deleted_at" ON "fulfillment_address" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('create table if not exists "fulfillment_provider" ("id" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_provider_pkey" primary key ("id"));');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_provider_deleted_at" ON "fulfillment_provider" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('create table if not exists "fulfillment_provider" ("id" text not null, "is_enabled" boolean not null default true, constraint "fulfillment_provider_pkey" primary key ("id"));');
this.addSql('create table if not exists "fulfillment_set" ("id" text not null, "name" text not null, "type" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_set_pkey" primary key ("id"));');
this.addSql('CREATE UNIQUE INDEX IF NOT EXISTS "IDX_fulfillment_set_name_unique" ON "fulfillment_set" (name) WHERE deleted_at IS NULL;');
@@ -32,7 +31,7 @@ export class Migration20240305095931_InitialSetupMigration extends Migration {
this.addSql('CREATE UNIQUE INDEX IF NOT EXISTS "IDX_shipping_profile_name_unique" ON "shipping_profile" (name) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_profile_deleted_at" ON "shipping_profile" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('create table if not exists "shipping_option" ("id" text not null, "name" text not null, "price_type" text check ("price_type" in (\'calculated\', \'flat\')) not null default \'calculated\', "service_zone_id" text not null, "shipping_profile_id" text null, "fulfillment_provider_id" text null, "data" jsonb null, "metadata" jsonb null, "shipping_option_type_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_pkey" primary key ("id"));');
this.addSql('create table if not exists "shipping_option" ("id" text not null, "name" text not null, "price_type" text check ("price_type" in (\'calculated\', \'flat\')) not null default \'calculated\', "service_zone_id" text not null, "shipping_profile_id" text null, "fulfillment_provider_id" text null, "data" jsonb null, "metadata" jsonb null, "shipping_option_type_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_pkey" primary key ("id"));');
this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_shipping_option_type_id_unique" unique ("shipping_option_type_id");');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_service_zone_id" ON "shipping_option" (service_zone_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_shipping_profile_id" ON "shipping_option" (shipping_profile_id) WHERE deleted_at IS NULL;');
@@ -44,7 +43,7 @@ export class Migration20240305095931_InitialSetupMigration extends Migration {
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_rule_shipping_option_id" ON "shipping_option_rule" (shipping_option_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_rule_deleted_at" ON "shipping_option_rule" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('create table if not exists "fulfillment" ("id" text not null, "location_id" text not null, "packed_at" timestamptz null, "shipped_at" timestamptz null, "delivered_at" timestamptz null, "canceled_at" timestamptz null, "data" jsonb null, "provider_id" text not null, "shipping_option_id" text null, "metadata" jsonb null, "delivery_address_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_pkey" primary key ("id"));');
this.addSql('create table if not exists "fulfillment" ("id" text not null, "location_id" text not null, "packed_at" timestamptz null, "shipped_at" timestamptz null, "delivered_at" timestamptz null, "canceled_at" timestamptz null, "data" jsonb null, "provider_id" text null, "shipping_option_id" text null, "metadata" jsonb null, "delivery_address_id" text null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_pkey" primary key ("id"));');
this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_delivery_address_id_unique" unique ("delivery_address_id");');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_location_id" ON "fulfillment" (location_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_provider_id" ON "fulfillment" (provider_id) WHERE deleted_at IS NULL;');
@@ -61,20 +60,20 @@ export class Migration20240305095931_InitialSetupMigration extends Migration {
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_item_fulfillment_id" ON "fulfillment_item" (fulfillment_id) WHERE deleted_at IS NULL;');
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_item_deleted_at" ON "fulfillment_item" (deleted_at) WHERE deleted_at IS NOT NULL;');
this.addSql('alter table if exists "service_zone" add constraint "service_zone_fulfillment_set_id_foreign" foreign key ("fulfillment_set_id") references "fulfillment_set" ("id") on update cascade;');
this.addSql('alter table if exists "service_zone" add constraint "service_zone_fulfillment_set_id_foreign" foreign key ("fulfillment_set_id") references "fulfillment_set" ("id") on update cascade on delete cascade;');
this.addSql('alter table if exists "geo_zone" add constraint "geo_zone_service_zone_id_foreign" foreign key ("service_zone_id") references "service_zone" ("id") on update cascade;');
this.addSql('alter table if exists "geo_zone" add constraint "geo_zone_service_zone_id_foreign" foreign key ("service_zone_id") references "service_zone" ("id") on update cascade on delete cascade;');
this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_service_zone_id_foreign" foreign key ("service_zone_id") references "service_zone" ("id") on update cascade;');
this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_service_zone_id_foreign" foreign key ("service_zone_id") references "service_zone" ("id") on update cascade on delete cascade;');
this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_shipping_profile_id_foreign" foreign key ("shipping_profile_id") references "shipping_profile" ("id") on update cascade on delete set null;');
this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_fulfillment_provider_id_foreign" foreign key ("fulfillment_provider_id") references "fulfillment_provider" ("id") on update cascade on delete set null;');
this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_shipping_option_type_id_foreign" foreign key ("shipping_option_type_id") references "shipping_option_type" ("id") on update cascade on delete cascade;');
this.addSql('alter table if exists "shipping_option_rule" add constraint "shipping_option_rule_shipping_option_id_foreign" foreign key ("shipping_option_id") references "shipping_option" ("id") on update cascade;');
this.addSql('alter table if exists "shipping_option_rule" add constraint "shipping_option_rule_shipping_option_id_foreign" foreign key ("shipping_option_id") references "shipping_option" ("id") on update cascade on delete cascade;');
this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_provider_id_foreign" foreign key ("provider_id") references "fulfillment_provider" ("id") on update cascade;');
this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_provider_id_foreign" foreign key ("provider_id") references "fulfillment_provider" ("id") on update cascade on delete set null;');
this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_shipping_option_id_foreign" foreign key ("shipping_option_id") references "shipping_option" ("id") on update cascade on delete set null;');
this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_delivery_address_id_foreign" foreign key ("delivery_address_id") references "fulfillment_address" ("id") on update cascade;');
this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_delivery_address_id_foreign" foreign key ("delivery_address_id") references "fulfillment_address" ("id") on update cascade on delete cascade;');
this.addSql('alter table if exists "fulfillment_label" add constraint "fulfillment_label_fulfillment_id_foreign" foreign key ("fulfillment_id") references "fulfillment" ("id") on update cascade on delete cascade;');
@@ -1,66 +1,19 @@
import {
createPsqlIndexStatementHelper,
DALUtils,
generateEntityId,
} from "@medusajs/utils"
import { DAL } from "@medusajs/types"
import { generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
Collection,
Entity,
Filter,
OneToMany,
OnInit,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import ShippingOption from "./shipping-option"
type FulfillmentProviderOptionalProps = DAL.SoftDeletableEntityDateColumns
const DeletedAtIndex = createPsqlIndexStatementHelper({
tableName: "fulfillment_provider",
columns: "deleted_at",
where: "deleted_at IS NOT NULL",
})
@Entity()
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
export default class FulfillmentProvider {
[OptionalProps]?: FulfillmentProviderOptionalProps
@PrimaryKey({ columnType: "text" })
id: string
@Property({ columnType: "jsonb", nullable: true })
metadata: Record<string, unknown> | null = null
@OneToMany(
() => ShippingOption,
(shippingOption) => shippingOption.fulfillment_provider
)
shipping_options = new Collection<ShippingOption>(this)
@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
@DeletedAtIndex.MikroORMIndex()
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null = null
@Property({ columnType: "boolean", defaultRaw: "true" })
is_enabled: boolean = true
@BeforeCreate()
onCreate() {
+19 -3
View File
@@ -7,6 +7,7 @@ import {
import { DAL } from "@medusajs/types"
import {
BeforeCreate,
Cascade,
Collection,
Entity,
Filter,
@@ -93,6 +94,8 @@ export default class Fulfillment {
columnType: "text",
fieldName: "provider_id",
mapToPk: true,
nullable: true,
onDelete: "set null",
})
@FulfillmentProviderIdIndex.MikroORMIndex()
provider_id: string
@@ -102,6 +105,7 @@ export default class Fulfillment {
fieldName: "shipping_option_id",
nullable: true,
mapToPk: true,
onDelete: "set null",
})
@FulfillmentShippingOptionIdIndex.MikroORMIndex()
shipping_option_id: string | null = null
@@ -115,13 +119,25 @@ export default class Fulfillment {
@ManyToOne(() => FulfillmentProvider, { persist: false })
provider: FulfillmentProvider
@OneToOne()
@OneToOne({
entity: () => Address,
owner: true,
cascade: [Cascade.PERSIST, "soft-remove"] as any,
nullable: true,
onDelete: "cascade",
})
delivery_address!: Address
@OneToMany(() => FulfillmentItem, (item) => item.fulfillment)
@OneToMany(() => FulfillmentItem, (item) => item.fulfillment, {
cascade: [Cascade.PERSIST, "soft-remove"] as any,
orphanRemoval: true,
})
items = new Collection<FulfillmentItem>(this)
@OneToMany(() => FulfillmentLabel, (label) => label.fulfillment)
@OneToMany(() => FulfillmentLabel, (label) => label.fulfillment, {
cascade: [Cascade.PERSIST, "soft-remove"] as any,
orphanRemoval: true,
})
labels = new Collection<FulfillmentLabel>(this)
@Property({
+1 -1
View File
@@ -78,11 +78,11 @@ export default class GeoZone {
type: "text",
mapToPk: true,
fieldName: "service_zone_id",
onDelete: "cascade",
})
@ServiceZoneIdIndex.MikroORMIndex()
service_zone_id: string
// TODO: Do we have an example or idea of what would be stored in this field? like lat/long for example?
@Property({ columnType: "jsonb", nullable: true })
postal_expression: Record<string, unknown> | null = null
@@ -65,6 +65,7 @@ export default class ServiceZone {
type: "text",
mapToPk: true,
fieldName: "fulfillment_set_id",
onDelete: "cascade",
})
@FulfillmentSetIdIndex.MikroORMIndex()
fulfillment_set_id: string
@@ -80,7 +81,11 @@ export default class ServiceZone {
@OneToMany(
() => ShippingOption,
(shippingOption) => shippingOption.service_zone
(shippingOption) => shippingOption.service_zone,
{
cascade: [Cascade.PERSIST, "soft-remove"] as any,
orphanRemoval: true,
}
)
shipping_options = new Collection<ShippingOption>(this)
@@ -57,6 +57,7 @@ export default class ShippingOptionRule {
type: "text",
mapToPk: true,
fieldName: "shipping_option_id",
onDelete: "cascade",
})
@ShippingOptionIdIndex.MikroORMIndex()
shipping_option_id: string
@@ -50,6 +50,7 @@ export default class ShippingOptionType {
@OneToOne(() => ShippingOption, (so) => so.type, {
type: "text",
onDelete: "cascade",
})
shipping_option: ShippingOption
@@ -81,6 +81,7 @@ export default class ShippingOption {
type: "text",
fieldName: "service_zone_id",
mapToPk: true,
onDelete: "cascade",
})
@ServiceZoneIdIndex.MikroORMIndex()
service_zone_id: string
@@ -90,6 +91,7 @@ export default class ShippingOption {
fieldName: "shipping_profile_id",
mapToPk: true,
nullable: true,
onDelete: "set null",
})
@ShippingProfileIdIndex.MikroORMIndex()
shipping_profile_id: string | null
@@ -128,9 +130,10 @@ export default class ShippingOption {
@OneToOne(() => ShippingOptionType, (so) => so.shipping_option, {
owner: true,
cascade: [Cascade.PERSIST, Cascade.REMOVE, "soft-remove"] as any,
cascade: [Cascade.PERSIST, "soft-remove"] as any,
orphanRemoval: true,
fieldName: "shipping_option_type_id",
onDelete: "cascade",
})
type: ShippingOptionType