feat(product): Create (+ workflow), delete, restore (#4459)
* Feat: create product with product module * feat: create product wip * feat: create product wip * feat: update product relation and generate image migration * lint * conitnue implementation * continue implementation and add integration tests for produceService.create * Add integration tests for product creation at the module level for the complete flow * only use persist since write operations are always wrapped in a transaction which will be committed and flushed * simplify the transaction wrapper to make future changes easier * feat: move some utils to the utils package to simplify its usage * tests: fix unit tests * feat: create variants along side the product * Add more integration tests an update migrations * chore: Update actions workflow to include packages integration tests * small types and utils cleanup * chore: Add support for database debug option * chore: Add missing types in package.json from types and util, validate that all the models are sync with medusa * expose retrieve method * fix types issues * fix unit tests and move integration tests workflow with the plugins integration tests * chore: remove migration function export from the definition to prevent them to be ran by the medusa cli just in case * fix package.json script * chore: workflows * feat: start creating the create product workflow * feat: add empty step for prices and sales channel * tests: update scripts and action envs * fix imports * feat: Add proper soft deleted support + add product deletion service public api * chore: update migrations * chore: update migrations * chore: update todo * feat: Add product deletion to the create-product workflow as compensation * chore: cleanup product utils * feat: Add support for cascade soft-remove * feat: refactor repository to take into account withDeleted * fix integration tests * Add support for force delete -> delete, cleanup repositories and improvements * Add support for restoring a product and add integration tests * cleaup + tests * types * fix integration tests * remove unnecessary comments * move specific mikro orm usage to the DAL * Cleanup workflow functions * Make deleted_at optional at the property level and add url index for the images * address feedback + cleanup * fix export * merge migrations into one * feat(product, types): added missing product variant methods (#4475) * chore: added missing product variant methods * chore: address PR feedback * chore: catch undefined case for retrieve + specs for variant service * chore: align TEntity + add changeset * chore: revert changeset, TEntity to ProductVariant * chore: write tests for pagination, unskip the test * Create chilled-mice-deliver.md * update integration fixtuers * update pipeline node version * rename github action * fix pipeline * feat(medusa, types): added missing category tests and service methods (#4499) * chore: added missing category tests and service methods * chore: added type changes to module service * chore: address pr feedback * update repositories manager usage and serialisation from the write public API * move serializisation to the DAL * rename template args * chore: added collection methods for module and collection service (#4505) * chore: added collection methods for module and collection service * Create fresh-islands-teach.md * chore: move retrieve entity to utils package * chore: make products optional in DTO type --------- Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> * feat(product): Apply transaction decorators to the services (#4512) --------- Co-authored-by: Riqwan Thamir <rmthamir@gmail.com> Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
98 files changed
+5444
-688
No files matched your search
@@ -5,18 +5,14 @@ import {
|
||||
MODULE_PACKAGE_NAMES,
|
||||
Modules,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { IProductModuleService, ModulesSdkTypes } from "@medusajs/types"
|
||||
import { moduleDefinition } from "../module-definition"
|
||||
import {
|
||||
InitializeModuleInjectableDependencies,
|
||||
ProductServiceInitializeCustomDataLayerOptions,
|
||||
ProductServiceInitializeOptions,
|
||||
} from "../types"
|
||||
import { InitializeModuleInjectableDependencies } from "../types"
|
||||
|
||||
export const initialize = async (
|
||||
options?:
|
||||
| ProductServiceInitializeOptions
|
||||
| ProductServiceInitializeCustomDataLayerOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
| ExternalModuleDeclaration,
|
||||
injectedDependencies?: InitializeModuleInjectableDependencies
|
||||
): Promise<IProductModuleService> => {
|
||||
|
||||
@@ -6,24 +6,21 @@ import {
|
||||
MODULE_RESOURCE_TYPE,
|
||||
MODULE_SCOPE,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { MedusaError, ModulesSdkUtils } from "@medusajs/utils"
|
||||
|
||||
import { EntitySchema } from "@mikro-orm/core"
|
||||
|
||||
import * as ProductModels from "@models"
|
||||
import {
|
||||
ProductServiceInitializeCustomDataLayerOptions,
|
||||
ProductServiceInitializeOptions,
|
||||
} from "../types"
|
||||
import { createConnection, loadDatabaseConfig } from "../utils"
|
||||
import { createConnection } from "../utils"
|
||||
import { ModulesSdkTypes } from "@medusajs/types"
|
||||
|
||||
export default async (
|
||||
{
|
||||
options,
|
||||
container,
|
||||
}: LoaderOptions<
|
||||
| ProductServiceInitializeOptions
|
||||
| ProductServiceInitializeCustomDataLayerOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
>,
|
||||
moduleDeclaration?: InternalModuleDeclaration
|
||||
): Promise<void> => {
|
||||
@@ -35,11 +32,11 @@ export default async (
|
||||
}
|
||||
|
||||
const customManager = (
|
||||
options as ProductServiceInitializeCustomDataLayerOptions
|
||||
options as ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
)?.manager
|
||||
|
||||
if (!customManager) {
|
||||
const dbData = loadDatabaseConfig(options)
|
||||
const dbData = ModulesSdkUtils.loadDatabaseConfig("product", options)
|
||||
await loadDefault({ database: dbData, container })
|
||||
} else {
|
||||
container.register({
|
||||
|
||||
@@ -3,36 +3,39 @@ import { LoaderOptions } from "@medusajs/modules-sdk"
|
||||
import { asClass } from "awilix"
|
||||
import {
|
||||
ProductCategoryService,
|
||||
ProductCollectionService,
|
||||
ProductImageService,
|
||||
ProductModuleService,
|
||||
ProductOptionService,
|
||||
ProductService,
|
||||
ProductTagService,
|
||||
ProductTypeService,
|
||||
ProductVariantService,
|
||||
ProductCollectionService,
|
||||
} from "@services"
|
||||
import * as DefaultRepositories from "@repositories"
|
||||
import {
|
||||
BaseRepository,
|
||||
ProductCategoryRepository,
|
||||
ProductCollectionRepository,
|
||||
ProductImageRepository,
|
||||
ProductOptionRepository,
|
||||
ProductRepository,
|
||||
ProductTagRepository,
|
||||
ProductTypeRepository,
|
||||
ProductVariantRepository,
|
||||
} from "@repositories"
|
||||
import {
|
||||
ProductServiceInitializeCustomDataLayerOptions,
|
||||
ProductServiceInitializeOptions,
|
||||
} from "../types"
|
||||
import { Constructor, DAL } from "@medusajs/types"
|
||||
import { Constructor, DAL, ModulesSdkTypes } from "@medusajs/types"
|
||||
import { lowerCaseFirst } from "@medusajs/utils"
|
||||
|
||||
export default async ({
|
||||
container,
|
||||
options,
|
||||
}: LoaderOptions<
|
||||
| ProductServiceInitializeOptions
|
||||
| ProductServiceInitializeCustomDataLayerOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
>): Promise<void> => {
|
||||
const customRepositories = (
|
||||
options as ProductServiceInitializeCustomDataLayerOptions
|
||||
options as ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
)?.repositories
|
||||
|
||||
container.register({
|
||||
@@ -42,6 +45,9 @@ export default async ({
|
||||
productVariantService: asClass(ProductVariantService).singleton(),
|
||||
productTagService: asClass(ProductTagService).singleton(),
|
||||
productCollectionService: asClass(ProductCollectionService).singleton(),
|
||||
productImageService: asClass(ProductImageService).singleton(),
|
||||
productTypeService: asClass(ProductTypeService).singleton(),
|
||||
productOptionService: asClass(ProductOptionService).singleton(),
|
||||
})
|
||||
|
||||
if (customRepositories) {
|
||||
@@ -53,13 +59,17 @@ export default async ({
|
||||
|
||||
function loadDefaultRepositories({ container }) {
|
||||
container.register({
|
||||
productRepository: asClass(ProductRepository).singleton(),
|
||||
productVariantRepository: asClass(ProductVariantRepository).singleton(),
|
||||
productTagRepository: asClass(ProductTagRepository).singleton(),
|
||||
baseRepository: asClass(BaseRepository).singleton(),
|
||||
productImageRepository: asClass(ProductImageRepository).singleton(),
|
||||
productCategoryRepository: asClass(ProductCategoryRepository).singleton(),
|
||||
productCollectionRepository: asClass(
|
||||
ProductCollectionRepository
|
||||
).singleton(),
|
||||
productRepository: asClass(ProductRepository).singleton(),
|
||||
productTagRepository: asClass(ProductTagRepository).singleton(),
|
||||
productTypeRepository: asClass(ProductTypeRepository).singleton(),
|
||||
productOptionRepository: asClass(ProductOptionRepository).singleton(),
|
||||
productVariantRepository: asClass(ProductVariantRepository).singleton(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -212,6 +212,15 @@
|
||||
"name": "product_collection",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_collection_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_product_collection_handle_unique",
|
||||
"columnNames": [
|
||||
@@ -234,6 +243,80 @@
|
||||
"checks": [],
|
||||
"foreignKeys": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"metadata": {
|
||||
"name": "metadata",
|
||||
"type": "jsonb",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"mappedType": "json"
|
||||
},
|
||||
"deleted_at": {
|
||||
"name": "deleted_at",
|
||||
"type": "timestamptz",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"length": 6,
|
||||
"mappedType": "datetime"
|
||||
}
|
||||
},
|
||||
"name": "image",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": [
|
||||
"url"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_image_url",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_image_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "image_pkey",
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"id": {
|
||||
@@ -277,6 +360,15 @@
|
||||
"name": "product_tag",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_tag_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "product_tag_pkey",
|
||||
"columnNames": [
|
||||
@@ -333,6 +425,15 @@
|
||||
"name": "product_type",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_type_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "product_type_pkey",
|
||||
"columnNames": [
|
||||
@@ -588,6 +689,15 @@
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_product_handle_unique",
|
||||
"columnNames": [
|
||||
@@ -698,6 +808,15 @@
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_option_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"keyName": "product_option_pkey",
|
||||
"columnNames": [
|
||||
@@ -789,6 +908,71 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"product_id": {
|
||||
"name": "product_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"product_image_id": {
|
||||
"name": "product_image_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
}
|
||||
},
|
||||
"name": "product_images",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "product_images_pkey",
|
||||
"columnNames": [
|
||||
"product_id",
|
||||
"product_image_id"
|
||||
],
|
||||
"composite": true,
|
||||
"primary": true,
|
||||
"unique": true
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"product_images_product_id_foreign": {
|
||||
"constraintName": "product_images_product_id_foreign",
|
||||
"columnNames": [
|
||||
"product_id"
|
||||
],
|
||||
"localTableName": "public.product_images",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.product",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
},
|
||||
"product_images_product_image_id_foreign": {
|
||||
"constraintName": "product_images_product_image_id_foreign",
|
||||
"columnNames": [
|
||||
"product_image_id"
|
||||
],
|
||||
"localTableName": "public.product_images",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.image",
|
||||
"deleteRule": "cascade",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
"product_id": {
|
||||
@@ -917,6 +1101,7 @@
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"default": "100",
|
||||
"mappedType": "decimal"
|
||||
},
|
||||
"allow_backorder": {
|
||||
@@ -1027,6 +1212,7 @@
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": true,
|
||||
"default": "0",
|
||||
"mappedType": "decimal"
|
||||
},
|
||||
"created_at": {
|
||||
@@ -1072,12 +1258,21 @@
|
||||
"name": "product_variant",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_variant_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": [
|
||||
"product_id"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_variant_product_id_index",
|
||||
"keyName": "IDX_product_variant_product_id",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
@@ -1210,7 +1405,25 @@
|
||||
"option_id"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_option_value_product_option",
|
||||
"keyName": "IDX_product_option_value_option_id",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": [
|
||||
"variant_id"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_option_value_variant_id",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
{
|
||||
"columnNames": [
|
||||
"deleted_at"
|
||||
],
|
||||
"composite": false,
|
||||
"keyName": "IDX_product_option_value_deleted_at",
|
||||
"primary": false,
|
||||
"unique": false
|
||||
},
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20230609132805 extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql('create table "product_category" ("id" text not null, "name" text not null, "description" text not null default \'\', "handle" text not null, "mpath" text not null, "is_active" boolean not null default false, "is_internal" boolean not null default false, "rank" numeric not null default 0, "parent_category_id" text null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "product_category_pkey" primary key ("id"));');
|
||||
this.addSql('create index "IDX_product_category_path" on "product_category" ("mpath");');
|
||||
this.addSql('alter table "product_category" add constraint "IDX_product_category_handle" unique ("handle");');
|
||||
|
||||
this.addSql('create table "product_collection" ("id" text not null, "title" text not null, "handle" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_collection_pkey" primary key ("id"));');
|
||||
this.addSql('alter table "product_collection" add constraint "IDX_product_collection_handle_unique" unique ("handle");');
|
||||
|
||||
this.addSql('create table "product_tag" ("id" text not null, "value" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_tag_pkey" primary key ("id"));');
|
||||
|
||||
this.addSql('create table "product_type" ("id" text not null, "value" text not null, "metadata" json null, "deleted_at" timestamptz null, constraint "product_type_pkey" primary key ("id"));');
|
||||
|
||||
this.addSql('create table "product" ("id" text not null, "title" text not null, "handle" text not null, "subtitle" text null, "description" text null, "is_giftcard" boolean not null default false, "status" text check ("status" in (\'draft\', \'proposed\', \'published\', \'rejected\')) not null, "thumbnail" text null, "weight" text null, "length" text null, "height" text null, "width" text null, "origin_country" text null, "hs_code" text null, "mid_code" text null, "material" text null, "collection_id" text null, "type_id" text null, "discountable" boolean not null default true, "external_id" text null, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, "metadata" jsonb null, constraint "product_pkey" primary key ("id"));');
|
||||
this.addSql('create index "IDX_product_type_id" on "product" ("type_id");');
|
||||
this.addSql('alter table "product" add constraint "IDX_product_handle_unique" unique ("handle");');
|
||||
|
||||
this.addSql('create table "product_option" ("id" text not null, "title" text not null, "product_id" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_option_pkey" primary key ("id"));');
|
||||
this.addSql('create index "IDX_product_option_product_id" on "product_option" ("product_id");');
|
||||
|
||||
this.addSql('create table "product_tags" ("product_id" text not null, "product_tag_id" text not null, constraint "product_tags_pkey" primary key ("product_id", "product_tag_id"));');
|
||||
|
||||
this.addSql('create table "product_category_product" ("product_id" text not null, "product_category_id" text not null, constraint "product_category_product_pkey" primary key ("product_id", "product_category_id"));');
|
||||
|
||||
this.addSql('create table "product_variant" ("id" text not null, "title" text not null, "sku" text null, "barcode" text null, "ean" text null, "upc" text null, "inventory_quantity" numeric not null, "allow_backorder" boolean not null default false, "manage_inventory" boolean not null default true, "hs_code" text null, "origin_country" text null, "mid_code" text null, "material" text null, "weight" numeric null, "length" numeric null, "height" numeric null, "width" numeric null, "metadata" jsonb null, "variant_rank" numeric null, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, "product_id" text not null, constraint "product_variant_pkey" primary key ("id"));');
|
||||
this.addSql('create index "IDX_product_variant_product_id_index" on "product_variant" ("product_id");');
|
||||
this.addSql('alter table "product_variant" add constraint "IDX_product_variant_sku_unique" unique ("sku");');
|
||||
this.addSql('alter table "product_variant" add constraint "IDX_product_variant_barcode_unique" unique ("barcode");');
|
||||
this.addSql('alter table "product_variant" add constraint "IDX_product_variant_ean_unique" unique ("ean");');
|
||||
this.addSql('alter table "product_variant" add constraint "IDX_product_variant_upc_unique" unique ("upc");');
|
||||
|
||||
this.addSql('create table "product_option_value" ("id" text not null, "value" text not null, "option_id" text not null, "variant_id" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_option_value_pkey" primary key ("id"));');
|
||||
this.addSql('create index "IDX_product_option_value_product_option" on "product_option_value" ("option_id");');
|
||||
|
||||
this.addSql('alter table "product_category" add constraint "product_category_parent_category_id_foreign" foreign key ("parent_category_id") references "product_category" ("id") on update cascade on delete set null;');
|
||||
|
||||
this.addSql('alter table "product" add constraint "product_collection_id_foreign" foreign key ("collection_id") references "product_collection" ("id") on update cascade on delete set null;');
|
||||
this.addSql('alter table "product" add constraint "product_type_id_foreign" foreign key ("type_id") references "product_type" ("id") on update cascade on delete set null;');
|
||||
|
||||
this.addSql('alter table "product_option" add constraint "product_option_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade;');
|
||||
|
||||
this.addSql('alter table "product_tags" add constraint "product_tags_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;');
|
||||
this.addSql('alter table "product_tags" add constraint "product_tags_product_tag_id_foreign" foreign key ("product_tag_id") references "product_tag" ("id") on update cascade on delete cascade;');
|
||||
|
||||
this.addSql('alter table "product_category_product" add constraint "product_category_product_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;');
|
||||
this.addSql('alter table "product_category_product" add constraint "product_category_product_product_category_id_foreign" foreign key ("product_category_id") references "product_category" ("id") on update cascade on delete cascade;');
|
||||
|
||||
this.addSql('alter table "product_variant" add constraint "product_variant_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;');
|
||||
|
||||
this.addSql('alter table "product_option_value" add constraint "product_option_value_option_id_foreign" foreign key ("option_id") references "product_option" ("id") on update cascade;');
|
||||
this.addSql('alter table "product_option_value" add constraint "product_option_value_variant_id_foreign" foreign key ("variant_id") references "product_variant" ("id") on update cascade on delete cascade;');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { Migration } from "@mikro-orm/migrations"
|
||||
|
||||
export class Migration20230710091208 extends Migration {
|
||||
async up(): Promise<void> {
|
||||
this.addSql(
|
||||
'create table "product_category" ("id" text not null, "name" text not null, "description" text not null default \'\', "handle" text not null, "mpath" text not null, "is_active" boolean not null default false, "is_internal" boolean not null default false, "rank" numeric not null default 0, "parent_category_id" text null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "product_category_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_category_path" on "product_category" ("mpath");'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_category" add constraint "IDX_product_category_handle" unique ("handle");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_collection" ("id" text not null, "title" text not null, "handle" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_collection_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_collection_deleted_at" on "product_collection" ("deleted_at");'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_collection" add constraint "IDX_product_collection_handle_unique" unique ("handle");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "image" ("id" text not null, "url" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "image_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql('create index "IDX_product_image_url" on "image" ("url");')
|
||||
this.addSql(
|
||||
'create index "IDX_product_image_deleted_at" on "image" ("deleted_at");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_tag" ("id" text not null, "value" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_tag_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_tag_deleted_at" on "product_tag" ("deleted_at");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_type" ("id" text not null, "value" text not null, "metadata" json null, "deleted_at" timestamptz null, constraint "product_type_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_type_deleted_at" on "product_type" ("deleted_at");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product" ("id" text not null, "title" text not null, "handle" text not null, "subtitle" text null, "description" text null, "is_giftcard" boolean not null default false, "status" text check ("status" in (\'draft\', \'proposed\', \'published\', \'rejected\')) not null, "thumbnail" text null, "weight" text null, "length" text null, "height" text null, "width" text null, "origin_country" text null, "hs_code" text null, "mid_code" text null, "material" text null, "collection_id" text null, "type_id" text null, "discountable" boolean not null default true, "external_id" text null, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, "metadata" jsonb null, constraint "product_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql('create index "IDX_product_type_id" on "product" ("type_id");')
|
||||
this.addSql(
|
||||
'create index "IDX_product_deleted_at" on "product" ("deleted_at");'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product" add constraint "IDX_product_handle_unique" unique ("handle");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_option" ("id" text not null, "title" text not null, "product_id" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_option_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_option_product_id" on "product_option" ("product_id");'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_option_deleted_at" on "product_option" ("deleted_at");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_tags" ("product_id" text not null, "product_tag_id" text not null, constraint "product_tags_pkey" primary key ("product_id", "product_tag_id"));'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_images" ("product_id" text not null, "product_image_id" text not null, constraint "product_images_pkey" primary key ("product_id", "product_image_id"));'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_category_product" ("product_id" text not null, "product_category_id" text not null, constraint "product_category_product_pkey" primary key ("product_id", "product_category_id"));'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_variant" ("id" text not null, "title" text not null, "sku" text null, "barcode" text null, "ean" text null, "upc" text null, "inventory_quantity" numeric not null default 100, "allow_backorder" boolean not null default false, "manage_inventory" boolean not null default true, "hs_code" text null, "origin_country" text null, "mid_code" text null, "material" text null, "weight" numeric null, "length" numeric null, "height" numeric null, "width" numeric null, "metadata" jsonb null, "variant_rank" numeric null default 0, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, "product_id" text not null, constraint "product_variant_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_variant_deleted_at" on "product_variant" ("deleted_at");'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_variant_product_id" on "product_variant" ("product_id");'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_variant" add constraint "IDX_product_variant_sku_unique" unique ("sku");'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_variant" add constraint "IDX_product_variant_barcode_unique" unique ("barcode");'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_variant" add constraint "IDX_product_variant_ean_unique" unique ("ean");'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_variant" add constraint "IDX_product_variant_upc_unique" unique ("upc");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'create table "product_option_value" ("id" text not null, "value" text not null, "option_id" text not null, "variant_id" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_option_value_pkey" primary key ("id"));'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_option_value_option_id" on "product_option_value" ("option_id");'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_option_value_variant_id" on "product_option_value" ("variant_id");'
|
||||
)
|
||||
this.addSql(
|
||||
'create index "IDX_product_option_value_deleted_at" on "product_option_value" ("deleted_at");'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "product_category" add constraint "product_category_parent_category_id_foreign" foreign key ("parent_category_id") references "product_category" ("id") on update cascade on delete set null;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "product" add constraint "product_collection_id_foreign" foreign key ("collection_id") references "product_collection" ("id") on update cascade on delete set null;'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product" add constraint "product_type_id_foreign" foreign key ("type_id") references "product_type" ("id") on update cascade on delete set null;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "product_option" add constraint "product_option_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "product_tags" add constraint "product_tags_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_tags" add constraint "product_tags_product_tag_id_foreign" foreign key ("product_tag_id") references "product_tag" ("id") on update cascade on delete cascade;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "product_images" add constraint "product_images_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_images" add constraint "product_images_product_image_id_foreign" foreign key ("product_image_id") references "image" ("id") on update cascade on delete cascade;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "product_category_product" add constraint "product_category_product_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_category_product" add constraint "product_category_product_product_category_id_foreign" foreign key ("product_category_id") references "product_category" ("id") on update cascade on delete cascade;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "product_variant" add constraint "product_variant_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'
|
||||
)
|
||||
|
||||
this.addSql(
|
||||
'alter table "product_option_value" add constraint "product_option_value_option_id_foreign" foreign key ("option_id") references "product_option" ("id") on update cascade;'
|
||||
)
|
||||
this.addSql(
|
||||
'alter table "product_option_value" add constraint "product_option_value_variant_id_foreign" foreign key ("variant_id") references "product_variant" ("id") on update cascade on delete cascade;'
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,3 +4,5 @@ export { default as ProductCollection } from "./product-collection"
|
||||
export { default as ProductTag } from "./product-tag"
|
||||
export { default as ProductType } from "./product-type"
|
||||
export { default as ProductVariant } from "./product-variant"
|
||||
export { default as ProductOption } from "./product-option"
|
||||
export { default as Image } from "./product-image"
|
||||
@@ -21,7 +21,7 @@ class ProductCategory {
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text", nullable: false })
|
||||
name: string
|
||||
name?: string
|
||||
|
||||
@Property({ columnType: "text", default: "", nullable: false })
|
||||
description?: string
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Index,
|
||||
OneToMany,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
Unique,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import { generateEntityId, kebabCase } from "@medusajs/utils"
|
||||
import Product from "./product"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalRelations = "products"
|
||||
|
||||
@Entity({ tableName: "product_collection" })
|
||||
@SoftDeletable()
|
||||
class ProductCollection {
|
||||
[OptionalProps]?: OptionalRelations
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@@ -21,13 +32,17 @@ class ProductCollection {
|
||||
name: "IDX_product_collection_handle_unique",
|
||||
properties: ["handle"],
|
||||
})
|
||||
handle: string
|
||||
handle?: string
|
||||
|
||||
@OneToMany(() => Product, (product) => product.collection)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_collection_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToMany,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import Product from "./product"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalRelations = "products"
|
||||
|
||||
@Entity({ tableName: "image" })
|
||||
@SoftDeletable()
|
||||
class ProductImage {
|
||||
[OptionalProps]?: OptionalRelations
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Index({ name: "IDX_product_image_url" })
|
||||
@Property({ columnType: "text" })
|
||||
url: string
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_image_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at?: Date
|
||||
|
||||
@ManyToMany(() => Product, (product) => product.images)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "img")
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductImage
|
||||
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToOne,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
@@ -9,28 +11,53 @@ import { generateEntityId } from "@medusajs/utils"
|
||||
|
||||
import ProductOption from "./product-option"
|
||||
import { ProductVariant } from "./index"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalFields =
|
||||
| "created_at"
|
||||
| "updated_at"
|
||||
| "allow_backorder"
|
||||
| "manage_inventory"
|
||||
| "option_id"
|
||||
| "variant_id"
|
||||
type OptionalRelations = "product" | "option" | "variant"
|
||||
|
||||
@Entity({ tableName: "product_option_value" })
|
||||
@SoftDeletable()
|
||||
class ProductOptionValue {
|
||||
[OptionalProps]?: OptionalFields | OptionalRelations
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
value: string
|
||||
|
||||
@Property({ persist: false })
|
||||
option_id!: string
|
||||
|
||||
@ManyToOne(() => ProductOption, {
|
||||
index: "IDX_product_option_value_product_option",
|
||||
index: "IDX_product_option_value_option_id",
|
||||
fieldName: "option_id",
|
||||
})
|
||||
option: ProductOption
|
||||
|
||||
@ManyToOne(() => ProductVariant, { onDelete: "cascade" })
|
||||
@Property({ persist: false })
|
||||
variant_id!: string
|
||||
|
||||
@ManyToOne(() => ProductVariant, {
|
||||
onDelete: "cascade",
|
||||
index: "IDX_product_option_value_variant_id",
|
||||
fieldName: "variant_id",
|
||||
})
|
||||
variant: ProductVariant
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_option_value_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@BeforeCreate()
|
||||
beforeCreate() {
|
||||
|
||||
@@ -3,17 +3,26 @@ import {
|
||||
Cascade,
|
||||
Collection,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import { Product } from "./index"
|
||||
import ProductOptionValue from "./product-option-value"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalRelations = "values" | "product"
|
||||
type OptionalFields = "product_id"
|
||||
|
||||
@Entity({ tableName: "product_option" })
|
||||
@SoftDeletable()
|
||||
class ProductOption {
|
||||
[OptionalProps]?: OptionalRelations | OptionalFields
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@@ -21,23 +30,25 @@ class ProductOption {
|
||||
title: string
|
||||
|
||||
@Property({ persist: false })
|
||||
product_id!: number
|
||||
product_id!: string
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
index: "IDX_product_option_product_id",
|
||||
fieldName: "product_id",
|
||||
})
|
||||
product: Product
|
||||
|
||||
@OneToMany(() => ProductOptionValue, (value) => value.option, {
|
||||
cascade: [Cascade.REMOVE],
|
||||
cascade: [Cascade.REMOVE, "soft-remove" as any],
|
||||
})
|
||||
values = new Collection<ProductOptionValue>(this)
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_option_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@BeforeCreate()
|
||||
beforeCreate() {
|
||||
|
||||
@@ -2,16 +2,24 @@ import {
|
||||
BeforeCreate,
|
||||
Collection,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToMany,
|
||||
OptionalProps,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import Product from "./product"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalRelations = "products"
|
||||
|
||||
@Entity({ tableName: "product_tag" })
|
||||
@SoftDeletable()
|
||||
class ProductTag {
|
||||
[OptionalProps]?: OptionalRelations
|
||||
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
|
||||
@@ -21,8 +29,9 @@ class ProductTag {
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_tag_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@ManyToMany(() => Product, (product) => product.tags)
|
||||
products = new Collection<Product>(this)
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { BeforeCreate, Entity, PrimaryKey, Property } from "@mikro-orm/core"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
@Entity({ tableName: "product_type" })
|
||||
@SoftDeletable()
|
||||
class ProductType {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id!: string
|
||||
@@ -13,8 +21,9 @@ class ProductType {
|
||||
@Property({ columnType: "json", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Index({ name: "IDX_product_type_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Cascade,
|
||||
Collection,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
OptionalProps,
|
||||
@@ -13,18 +14,18 @@ import {
|
||||
import { generateEntityId } from "@medusajs/utils"
|
||||
import { Product } from "@models"
|
||||
import ProductOptionValue from "./product-option-value"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalFields =
|
||||
| "created_at"
|
||||
| "updated_at"
|
||||
| "updated_at"
|
||||
| "deleted_at"
|
||||
| "allow_backorder"
|
||||
| "manage_inventory"
|
||||
| "product"
|
||||
| "product_id"
|
||||
|
||||
@Entity({ tableName: "product_variant" })
|
||||
@SoftDeletable()
|
||||
class ProductVariant {
|
||||
[OptionalProps]?: OptionalFields
|
||||
|
||||
@@ -65,14 +66,14 @@ class ProductVariant {
|
||||
// Note: Upon serialization, this turns to a string. This is on purpose, because you would loose
|
||||
// precision if you cast numeric to JS number, as JS number is a float.
|
||||
// Ref: https://github.com/mikro-orm/mikro-orm/issues/2295
|
||||
@Property({ columnType: "numeric" })
|
||||
inventory_quantity: number
|
||||
@Property({ columnType: "numeric", default: 100 })
|
||||
inventory_quantity?: number = 100
|
||||
|
||||
@Property({ columnType: "boolean", default: false })
|
||||
allow_backorder: boolean
|
||||
allow_backorder?: boolean = false
|
||||
|
||||
@Property({ columnType: "boolean", default: true })
|
||||
manage_inventory: boolean
|
||||
manage_inventory?: boolean = true
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
hs_code?: string | null
|
||||
@@ -101,7 +102,7 @@ class ProductVariant {
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@Property({ columnType: "numeric", nullable: true })
|
||||
@Property({ columnType: "numeric", nullable: true, default: 0 })
|
||||
variant_rank?: number | null
|
||||
|
||||
@Property({ persist: false })
|
||||
@@ -117,18 +118,19 @@ class ProductVariant {
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_variant_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@ManyToOne(() => Product, {
|
||||
onDelete: "cascade",
|
||||
index: "IDX_product_variant_product_id_index",
|
||||
index: "IDX_product_variant_product_id",
|
||||
fieldName: "product_id",
|
||||
})
|
||||
product!: Product
|
||||
|
||||
@OneToMany(() => ProductOptionValue, (optionValue) => optionValue.variant, {
|
||||
cascade: [Cascade.PERSIST, Cascade.REMOVE],
|
||||
cascade: [Cascade.PERSIST, Cascade.REMOVE, "soft-remove" as any],
|
||||
})
|
||||
options = new Collection<ProductOptionValue>(this)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Collection,
|
||||
Entity,
|
||||
Enum,
|
||||
Index,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
@@ -20,16 +21,20 @@ import ProductOption from "./product-option"
|
||||
import ProductTag from "./product-tag"
|
||||
import ProductType from "./product-type"
|
||||
import ProductVariant from "./product-variant"
|
||||
import ProductImage from "./product-image"
|
||||
import { SoftDeletable } from "../utils"
|
||||
|
||||
type OptionalRelations = "collection" | "type"
|
||||
type OptionalFields =
|
||||
| "collection_id"
|
||||
| "type_id"
|
||||
| "is_giftcard"
|
||||
| "discountable"
|
||||
| "created_at"
|
||||
| "updated_at"
|
||||
| "deleted_at"
|
||||
|
||||
@Entity({ tableName: "product" })
|
||||
@SoftDeletable()
|
||||
class Product {
|
||||
[OptionalProps]?: OptionalRelations | OptionalFields
|
||||
|
||||
@@ -58,16 +63,17 @@ class Product {
|
||||
@Enum(() => ProductTypes.ProductStatus)
|
||||
status!: ProductTypes.ProductStatus
|
||||
|
||||
// TODO: add images model
|
||||
// images: Image[]
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
thumbnail?: string | null
|
||||
|
||||
@OneToMany(() => ProductOption, (o) => o.product)
|
||||
@OneToMany(() => ProductOption, (o) => o.product, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
options = new Collection<ProductOption>(this)
|
||||
|
||||
@OneToMany(() => ProductVariant, (variant) => variant.product)
|
||||
@OneToMany(() => ProductVariant, (variant) => variant.product, {
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
variants = new Collection<ProductVariant>(this)
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
@@ -94,12 +100,22 @@ class Product {
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
material?: string | null
|
||||
|
||||
@ManyToOne(() => ProductCollection, { nullable: true })
|
||||
@Property({ persist: false })
|
||||
collection_id!: string
|
||||
|
||||
@ManyToOne(() => ProductCollection, {
|
||||
nullable: true,
|
||||
fieldName: "collection_id",
|
||||
})
|
||||
collection!: ProductCollection
|
||||
|
||||
@Property({ persist: false })
|
||||
type_id!: string
|
||||
|
||||
@ManyToOne(() => ProductType, {
|
||||
nullable: true,
|
||||
index: "IDX_product_type_id",
|
||||
fieldName: "type_id",
|
||||
})
|
||||
type!: ProductType
|
||||
|
||||
@@ -107,12 +123,22 @@ class Product {
|
||||
owner: true,
|
||||
pivotTable: "product_tags",
|
||||
index: "IDX_product_tag_id",
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
tags = new Collection<ProductTag>(this)
|
||||
|
||||
@ManyToMany(() => ProductImage, "products", {
|
||||
owner: true,
|
||||
pivotTable: "product_images",
|
||||
index: "IDX_product_image_id",
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
images = new Collection<ProductImage>(this)
|
||||
|
||||
@ManyToMany(() => ProductCategory, "products", {
|
||||
owner: true,
|
||||
pivotTable: "product_category_product",
|
||||
cascade: ["soft-remove"] as any,
|
||||
})
|
||||
categories = new Collection<ProductCategory>(this)
|
||||
|
||||
@@ -132,8 +158,9 @@ class Product {
|
||||
})
|
||||
updated_at: Date
|
||||
|
||||
@Index({ name: "IDX_product_deleted_at" })
|
||||
@Property({ columnType: "timestamptz", nullable: true })
|
||||
deleted_at: Date
|
||||
deleted_at?: Date
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata?: Record<string, unknown> | null
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ProductModuleService } from "@services"
|
||||
import loadContainer from "./loaders/container"
|
||||
import loadConnection from "./loaders/connection"
|
||||
import * as ProductModels from "@models"
|
||||
import { revertMigration, runMigrations } from "./scripts"
|
||||
|
||||
const service = ProductModuleService
|
||||
const loaders = [loadContainer, loadConnection] as any
|
||||
@@ -13,6 +12,4 @@ export const moduleDefinition: ModuleExports = {
|
||||
service,
|
||||
loaders,
|
||||
models,
|
||||
runMigrations,
|
||||
revertMigration,
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import { Context, DAL, RepositoryTransformOptions } from "@medusajs/types"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import {
|
||||
buildQuery,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
} from "@medusajs/utils"
|
||||
import { serialize } from "@mikro-orm/core"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
|
||||
// TODO: Should we create a mikro orm specific package for this and the soft deletable decorator util?
|
||||
|
||||
async function transactionWrapper(
|
||||
this: any,
|
||||
task: (transactionManager: unknown) => Promise<any>,
|
||||
{
|
||||
transaction,
|
||||
isolationLevel,
|
||||
enableNestedTransactions = false,
|
||||
}: {
|
||||
isolationLevel?: string
|
||||
transaction?: unknown
|
||||
enableNestedTransactions?: boolean
|
||||
} = {}
|
||||
): Promise<any> {
|
||||
// Reuse the same transaction if it is already provided and nested transactions are disabled
|
||||
if (!enableNestedTransactions && transaction) {
|
||||
return await task(transaction)
|
||||
}
|
||||
|
||||
const forkedManager = this.manager_.fork()
|
||||
|
||||
const options = {}
|
||||
if (isolationLevel) {
|
||||
Object.assign(options, { isolationLevel })
|
||||
}
|
||||
|
||||
if (transaction) {
|
||||
Object.assign(options, { ctx: transaction })
|
||||
await forkedManager.begin(options)
|
||||
} else {
|
||||
await forkedManager.begin(options)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await task(forkedManager)
|
||||
await forkedManager.commit()
|
||||
return result
|
||||
} catch (e) {
|
||||
await forkedManager.rollback()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const updateDeletedAtRecursively = async <T extends object = any>(
|
||||
manager: SqlEntityManager,
|
||||
entities: T[],
|
||||
value: Date | null
|
||||
) => {
|
||||
for await (const entity of entities) {
|
||||
if (!("deleted_at" in entity)) continue
|
||||
;(entity as any).deleted_at = value
|
||||
|
||||
const relations = manager
|
||||
.getDriver()
|
||||
.getMetadata()
|
||||
.get(entities[0].constructor.name).relations
|
||||
|
||||
const relationsToCascade = relations.filter((relation) =>
|
||||
relation.cascade.includes("soft-remove" as any)
|
||||
)
|
||||
|
||||
for (const relation of relationsToCascade) {
|
||||
const relationEntities = (await entity[relation.name].init()).getItems({
|
||||
filters: {
|
||||
[DAL.SoftDeletableFilterKey]: {
|
||||
withDeleted: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await updateDeletedAtRecursively(manager, relationEntities, value)
|
||||
}
|
||||
|
||||
await manager.persist(entities)
|
||||
}
|
||||
}
|
||||
|
||||
const serializer = <
|
||||
T extends object | object[],
|
||||
TResult extends object | object[]
|
||||
>(
|
||||
data: T,
|
||||
options?: any
|
||||
): Promise<TResult> => {
|
||||
options ??= {}
|
||||
const result = serialize(data, options)
|
||||
return Array.isArray(data) ? result : result[0]
|
||||
}
|
||||
|
||||
export abstract class AbstractBaseRepository<T = any>
|
||||
implements DAL.RepositoryService<T>
|
||||
{
|
||||
protected readonly manager_: SqlEntityManager
|
||||
|
||||
protected constructor({ manager }) {
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async transaction(
|
||||
task: (transactionManager: unknown) => Promise<any>,
|
||||
{
|
||||
transaction,
|
||||
isolationLevel,
|
||||
enableNestedTransactions = false,
|
||||
}: {
|
||||
isolationLevel?: string
|
||||
enableNestedTransactions?: boolean
|
||||
transaction?: unknown
|
||||
} = {}
|
||||
): Promise<any> {
|
||||
return await transactionWrapper.apply(this, arguments)
|
||||
}
|
||||
|
||||
serialize<
|
||||
TData extends object | object[] = object[],
|
||||
TResult extends object | object[] = object[]
|
||||
>(data: TData, options?: any): Promise<TResult> {
|
||||
return serializer<TData, TResult>(data, options)
|
||||
}
|
||||
|
||||
abstract find(options?: DAL.FindOptions<T>, context?: Context)
|
||||
|
||||
abstract findAndCount(
|
||||
options?: DAL.FindOptions<T>,
|
||||
context?: Context
|
||||
): Promise<[T[], number]>
|
||||
|
||||
abstract create(data: unknown[], context?: Context): Promise<T[]>
|
||||
|
||||
abstract delete(ids: string[], context?: Context): Promise<void>
|
||||
|
||||
@InjectTransactionManager()
|
||||
async softDelete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<T[]> {
|
||||
const entities = await this.find({ where: { id: { $in: ids } } as any })
|
||||
|
||||
const date = new Date()
|
||||
await updateDeletedAtRecursively(
|
||||
manager as SqlEntityManager,
|
||||
entities,
|
||||
date
|
||||
)
|
||||
|
||||
return entities
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async restore(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context<SqlEntityManager> = {}
|
||||
): Promise<T[]> {
|
||||
const query = buildQuery(
|
||||
{ id: { $in: ids } },
|
||||
{
|
||||
withDeleted: true,
|
||||
}
|
||||
)
|
||||
|
||||
const entities = await this.find(query)
|
||||
|
||||
await updateDeletedAtRecursively(
|
||||
manager as SqlEntityManager,
|
||||
entities,
|
||||
null
|
||||
)
|
||||
|
||||
return entities
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class AbstractTreeRepositoryBase<T = any>
|
||||
extends AbstractBaseRepository<T>
|
||||
implements DAL.TreeRepositoryService<T>
|
||||
{
|
||||
protected constructor({ manager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
}
|
||||
|
||||
abstract find(
|
||||
options?: DAL.FindOptions<T>,
|
||||
transformOptions?: RepositoryTransformOptions,
|
||||
context?: Context
|
||||
)
|
||||
|
||||
abstract findAndCount(
|
||||
options?: DAL.FindOptions<T>,
|
||||
transformOptions?: RepositoryTransformOptions,
|
||||
context?: Context
|
||||
): Promise<[T[], number]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Only used internally in order to be able to wrap in transaction from a
|
||||
* non identified repository
|
||||
*/
|
||||
export class BaseRepository extends AbstractBaseRepository {
|
||||
constructor({ manager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
}
|
||||
|
||||
serialize<
|
||||
TData extends object | object[] = object[],
|
||||
TResult extends object | object[] = object[]
|
||||
>(data: TData, options?: any): Promise<TResult> {
|
||||
return serializer<TData, TResult>(data, options)
|
||||
}
|
||||
|
||||
create(data: unknown[], context?: Context): Promise<any[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
delete(ids: string[], context?: Context): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
find(options?: DAL.FindOptions, context?: Context): Promise<any[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
findAndCount(
|
||||
options?: DAL.FindOptions,
|
||||
context?: Context
|
||||
): Promise<[any[], number]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
export { BaseRepository } from "./base"
|
||||
export { ProductRepository } from "./product"
|
||||
export { ProductTagRepository } from "./product-tag"
|
||||
export { ProductVariantRepository } from "./product-variant"
|
||||
export { ProductCollectionRepository } from "./product-collection"
|
||||
export { ProductCategoryRepository } from "./product-category"
|
||||
export { ProductImageRepository } from "./product-image"
|
||||
export { ProductTypeRepository } from "./product-type"
|
||||
export { ProductOptionRepository } from "./product-option"
|
||||
@@ -1,35 +1,36 @@
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import {
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
LoadStrategy,
|
||||
} from "@mikro-orm/core"
|
||||
import { deduplicateIfNecessary } from "../utils"
|
||||
import { ProductCategory } from "@models"
|
||||
import { DAL, ProductCategoryTransformOptions } from "@medusajs/types"
|
||||
import { Product, ProductCategory } from "@models"
|
||||
import { Context, DAL, ProductCategoryTransformOptions } from "@medusajs/types"
|
||||
import groupBy from "lodash/groupBy"
|
||||
import { AbstractTreeRepositoryBase } from "./base"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
|
||||
export class ProductCategoryRepository
|
||||
implements DAL.RepositoryService<ProductCategory>
|
||||
{
|
||||
export class ProductCategoryRepository extends AbstractTreeRepositoryBase<ProductCategory> {
|
||||
protected readonly manager_: SqlEntityManager
|
||||
|
||||
constructor({ manager }) {
|
||||
this.manager_ = manager.fork()
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
|
||||
transformOptions: ProductCategoryTransformOptions = {},
|
||||
context: { transaction?: any } = {}
|
||||
context: Context = {}
|
||||
): Promise<ProductCategory[]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
const { includeDescendantsTree } = transformOptions
|
||||
|
||||
findOptions_.options ??= {}
|
||||
const fields = (findOptions_.options.fields ??= [])
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
// Ref: Building descendants
|
||||
// mpath and parent_category_id needs to be added to the query for the tree building to be done accurately
|
||||
@@ -39,19 +40,11 @@ export class ProductCategoryRepository
|
||||
fields.push("parent_category_id")
|
||||
}
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
const productCategories = await this.manager_.find(
|
||||
const productCategories = await manager.find(
|
||||
ProductCategory,
|
||||
findOptions_.where as MikroFilterQuery<ProductCategory>,
|
||||
findOptions_.options as MikroOptions<ProductCategory>
|
||||
@@ -69,8 +62,12 @@ export class ProductCategoryRepository
|
||||
|
||||
async buildProductCategoriesWithDescendants(
|
||||
productCategories: ProductCategory[],
|
||||
findOptions: DAL.FindOptions<ProductCategory> = { where: {} }
|
||||
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<ProductCategory[]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
for (let productCategory of productCategories) {
|
||||
const whereOptions = {
|
||||
...findOptions.where,
|
||||
@@ -78,9 +75,11 @@ export class ProductCategoryRepository
|
||||
$like: `${productCategory.mpath}%`,
|
||||
},
|
||||
}
|
||||
delete whereOptions.parent_category_id
|
||||
|
||||
const descendantsForCategory = await this.manager_.find(
|
||||
delete whereOptions.parent_category_id
|
||||
delete whereOptions.id
|
||||
|
||||
const descendantsForCategory = await manager.find(
|
||||
ProductCategory,
|
||||
whereOptions as MikroFilterQuery<ProductCategory>,
|
||||
findOptions.options as MikroOptions<ProductCategory>
|
||||
@@ -111,30 +110,64 @@ export class ProductCategoryRepository
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<ProductCategory> = { where: {} },
|
||||
transformOptions: ProductCategoryTransformOptions = {},
|
||||
context: { transaction?: any } = {}
|
||||
context: Context = {}
|
||||
): Promise<[ProductCategory[], number]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
const { includeDescendantsTree } = transformOptions
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
const fields = (findOptions_.options.fields ??= [])
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
// Ref: Building descendants
|
||||
// mpath and parent_category_id needs to be added to the query for the tree building to be done accurately
|
||||
if (includeDescendantsTree) {
|
||||
fields.indexOf("mpath") === -1 && fields.push("mpath")
|
||||
fields.indexOf("parent_category_id") === -1 &&
|
||||
fields.push("parent_category_id")
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return await this.manager_.findAndCount(
|
||||
const [productCategories, count] = await manager.findAndCount(
|
||||
ProductCategory,
|
||||
findOptions_.where as MikroFilterQuery<ProductCategory>,
|
||||
findOptions_.options as MikroOptions<ProductCategory>
|
||||
)
|
||||
|
||||
if (!includeDescendantsTree) {
|
||||
return [productCategories, count]
|
||||
}
|
||||
|
||||
return [
|
||||
await this.buildProductCategoriesWithDescendants(
|
||||
productCategories,
|
||||
findOptions_
|
||||
),
|
||||
count,
|
||||
]
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<void> {
|
||||
await (manager as SqlEntityManager).nativeDelete(
|
||||
Product,
|
||||
{ id: { $in: ids } },
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
async create(
|
||||
data: unknown[],
|
||||
context: Context = {}
|
||||
): Promise<ProductCategory[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
@@ -1,74 +1,84 @@
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { ProductCollection } from "@models"
|
||||
import { Product, ProductCollection } from "@models"
|
||||
import {
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
LoadStrategy,
|
||||
} from "@mikro-orm/core"
|
||||
import { deduplicateIfNecessary } from "../utils"
|
||||
import { DAL } from "@medusajs/types"
|
||||
import { Context, DAL } from "@medusajs/types"
|
||||
import { AbstractBaseRepository } from "./base"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
|
||||
export class ProductCollectionRepository implements DAL.RepositoryService {
|
||||
export class ProductCollectionRepository extends AbstractBaseRepository<ProductCollection> {
|
||||
protected readonly manager_: SqlEntityManager
|
||||
constructor({ manager }) {
|
||||
this.manager_ = manager.fork()
|
||||
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async find<T = ProductCollection>(
|
||||
findOptions: DAL.FindOptions<T> = { where: {} },
|
||||
context: { transaction?: any } = {}
|
||||
): Promise<T[]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<ProductCollection> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<ProductCollection[]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return (await this.manager_.find(
|
||||
return await manager.find(
|
||||
ProductCollection,
|
||||
findOptions_.where as MikroFilterQuery<ProductCollection>,
|
||||
findOptions_.options as MikroOptions<ProductCollection>
|
||||
)) as unknown as T[]
|
||||
)
|
||||
}
|
||||
|
||||
async findAndCount<T = ProductCollection>(
|
||||
findOptions: DAL.FindOptions<T> = { where: {} },
|
||||
context: { transaction?: any } = {}
|
||||
): Promise<[T[], number]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<ProductCollection> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<[ProductCollection[], number]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return (await this.manager_.findAndCount(
|
||||
return await manager.findAndCount(
|
||||
ProductCollection,
|
||||
findOptions_.where as MikroFilterQuery<ProductCollection>,
|
||||
findOptions_.options as MikroOptions<ProductCollection>
|
||||
)) as unknown as [T[], number]
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<void> {
|
||||
await (manager as SqlEntityManager).nativeDelete(
|
||||
Product,
|
||||
{ id: { $in: ids } },
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async create(
|
||||
data: unknown[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<ProductCollection[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
LoadStrategy,
|
||||
} from "@mikro-orm/core"
|
||||
import { Context, DAL } from "@medusajs/types"
|
||||
import { Image, Product } from "@models"
|
||||
import { AbstractBaseRepository } from "./base"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
|
||||
export class ProductImageRepository extends AbstractBaseRepository<Image> {
|
||||
protected readonly manager_: SqlEntityManager
|
||||
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<Image> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<Image[]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return await manager.find(
|
||||
Image,
|
||||
findOptions_.where as MikroFilterQuery<Image>,
|
||||
findOptions_.options as MikroOptions<Image>
|
||||
)
|
||||
}
|
||||
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<Image> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<[Image[], number]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return await manager.findAndCount(
|
||||
Image,
|
||||
findOptions_.where as MikroFilterQuery<Image>,
|
||||
findOptions_.options as MikroOptions<Image>
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async upsert(
|
||||
urls: string[],
|
||||
@MedusaContext()
|
||||
context: Context = {}
|
||||
): Promise<Image[]> {
|
||||
const { transactionManager: manager } = context
|
||||
|
||||
const existingImages = await this.find(
|
||||
{
|
||||
where: {
|
||||
url: {
|
||||
$in: urls,
|
||||
},
|
||||
},
|
||||
},
|
||||
context
|
||||
)
|
||||
|
||||
const existingImagesMap = new Map(
|
||||
existingImages.map<[string, Image]>((img) => [img.url, img])
|
||||
)
|
||||
|
||||
const upsertedImgs: Image[] = []
|
||||
const imageToCreate: Image[] = []
|
||||
|
||||
urls.forEach((url) => {
|
||||
const aImg = existingImagesMap.get(url)
|
||||
if (aImg) {
|
||||
upsertedImgs.push(aImg)
|
||||
} else {
|
||||
const newImg = (manager as SqlEntityManager).create(Image, { url })
|
||||
imageToCreate.push(newImg)
|
||||
}
|
||||
})
|
||||
|
||||
if (imageToCreate.length) {
|
||||
await (manager as SqlEntityManager).persist(imageToCreate)
|
||||
upsertedImgs.push(...imageToCreate)
|
||||
}
|
||||
|
||||
return upsertedImgs
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<void> {
|
||||
await (manager as SqlEntityManager).nativeDelete(
|
||||
Product,
|
||||
{ id: { $in: ids } },
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async create(
|
||||
data: unknown[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<Image[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
LoadStrategy,
|
||||
} from "@mikro-orm/core"
|
||||
import { Product, ProductOption } from "@models"
|
||||
import { Context, DAL, ProductTypes } from "@medusajs/types"
|
||||
import { AbstractBaseRepository } from "./base"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
|
||||
export class ProductOptionRepository extends AbstractBaseRepository<ProductOption> {
|
||||
protected readonly manager_: SqlEntityManager
|
||||
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<ProductOption> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<ProductOption[]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return await manager.find(
|
||||
ProductOption,
|
||||
findOptions_.where as MikroFilterQuery<ProductOption>,
|
||||
findOptions_.options as MikroOptions<ProductOption>
|
||||
)
|
||||
}
|
||||
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<ProductOption> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<[ProductOption[], number]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return await manager.findAndCount(
|
||||
ProductOption,
|
||||
findOptions_.where as MikroFilterQuery<ProductOption>,
|
||||
findOptions_.options as MikroOptions<ProductOption>
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<void> {
|
||||
await (manager as SqlEntityManager).nativeDelete(
|
||||
Product,
|
||||
{ id: { $in: ids } },
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async create(
|
||||
data: (ProductTypes.CreateProductOptionDTO & { product: { id: string } })[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<ProductOption[]> {
|
||||
const options = data.map((option) => {
|
||||
return (manager as SqlEntityManager).create(ProductOption, option)
|
||||
})
|
||||
|
||||
await (manager as SqlEntityManager).persist(options)
|
||||
|
||||
return options
|
||||
}
|
||||
}
|
||||
@@ -1,74 +1,135 @@
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import {
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
LoadStrategy,
|
||||
RequiredEntityData,
|
||||
} from "@mikro-orm/core"
|
||||
import { deduplicateIfNecessary } from "../utils"
|
||||
import { ProductTag } from "@models"
|
||||
import { DAL } from "@medusajs/types"
|
||||
import { Product, ProductTag } from "@models"
|
||||
import { Context, CreateProductTagDTO, DAL } from "@medusajs/types"
|
||||
import { AbstractBaseRepository } from "./base"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
|
||||
export class ProductTagRepository implements DAL.RepositoryService {
|
||||
export class ProductTagRepository extends AbstractBaseRepository<ProductTag> {
|
||||
protected readonly manager_: SqlEntityManager
|
||||
constructor({ manager }) {
|
||||
this.manager_ = manager.fork()
|
||||
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async find<T = ProductTag>(
|
||||
findOptions: DAL.FindOptions<T> = { where: {} },
|
||||
context: { transaction?: any } = {}
|
||||
): Promise<T[]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<ProductTag> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<ProductTag[]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return (await this.manager_.find(
|
||||
return await manager.find(
|
||||
ProductTag,
|
||||
findOptions_.where as MikroFilterQuery<ProductTag>,
|
||||
findOptions_.options as MikroOptions<ProductTag>
|
||||
)) as unknown as T[]
|
||||
)
|
||||
}
|
||||
|
||||
async findAndCount<T = ProductTag>(
|
||||
findOptions: DAL.FindOptions<T> = { where: {} },
|
||||
context: { transaction?: any } = {}
|
||||
): Promise<[T[], number]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<ProductTag> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<[ProductTag[], number]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return (await this.manager_.findAndCount(
|
||||
return await manager.findAndCount(
|
||||
ProductTag,
|
||||
findOptions_.where as MikroFilterQuery<ProductTag>,
|
||||
findOptions_.options as MikroOptions<ProductTag>
|
||||
)) as unknown as [T[], number]
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async upsert(
|
||||
tags: CreateProductTagDTO[],
|
||||
@MedusaContext()
|
||||
context: Context = {}
|
||||
): Promise<ProductTag[]> {
|
||||
const { transactionManager: manager } = context
|
||||
|
||||
const tagsValues = tags.map((tag) => tag.value)
|
||||
const existingTags = await this.find(
|
||||
{
|
||||
where: {
|
||||
value: {
|
||||
$in: tagsValues,
|
||||
},
|
||||
},
|
||||
},
|
||||
context
|
||||
)
|
||||
|
||||
const existingTagsMap = new Map(
|
||||
existingTags.map<[string, ProductTag]>((tag) => [tag.value, tag])
|
||||
)
|
||||
|
||||
const upsertedTags: ProductTag[] = []
|
||||
const tagsToCreate: RequiredEntityData<ProductTag>[] = []
|
||||
|
||||
tags.forEach((tag) => {
|
||||
const aTag = existingTagsMap.get(tag.value)
|
||||
if (aTag) {
|
||||
upsertedTags.push(aTag)
|
||||
} else {
|
||||
const newTag = (manager as SqlEntityManager).create(ProductTag, tag)
|
||||
tagsToCreate.push(newTag)
|
||||
}
|
||||
})
|
||||
|
||||
if (tagsToCreate.length) {
|
||||
const newTags: ProductTag[] = []
|
||||
tagsToCreate.forEach((tag) => {
|
||||
newTags.push((manager as SqlEntityManager).create(ProductTag, tag))
|
||||
})
|
||||
|
||||
await (manager as SqlEntityManager).persist(newTags)
|
||||
upsertedTags.push(...newTags)
|
||||
}
|
||||
|
||||
return upsertedTags
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<void> {
|
||||
await (manager as SqlEntityManager).nativeDelete(
|
||||
Product,
|
||||
{ id: { $in: ids } },
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async create(
|
||||
data: unknown[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<ProductTag[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
LoadStrategy,
|
||||
RequiredEntityData,
|
||||
} from "@mikro-orm/core"
|
||||
import { Product, ProductType } from "@models"
|
||||
import { Context, CreateProductTypeDTO, DAL } from "@medusajs/types"
|
||||
import { AbstractBaseRepository } from "./base"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
|
||||
export class ProductTypeRepository extends AbstractBaseRepository<ProductType> {
|
||||
protected readonly manager_: SqlEntityManager
|
||||
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<ProductType> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<ProductType[]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return await manager.find(
|
||||
ProductType,
|
||||
findOptions_.where as MikroFilterQuery<ProductType>,
|
||||
findOptions_.options as MikroOptions<ProductType>
|
||||
)
|
||||
}
|
||||
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<ProductType> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<[ProductType[], number]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return await manager.findAndCount(
|
||||
ProductType,
|
||||
findOptions_.where as MikroFilterQuery<ProductType>,
|
||||
findOptions_.options as MikroOptions<ProductType>
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async upsert(
|
||||
types: CreateProductTypeDTO[],
|
||||
@MedusaContext()
|
||||
context: Context = {}
|
||||
): Promise<ProductType[]> {
|
||||
const { transactionManager: manager } = context
|
||||
|
||||
const typesValues = types.map((type) => type.value)
|
||||
const existingTypes = await this.find(
|
||||
{
|
||||
where: {
|
||||
value: {
|
||||
$in: typesValues,
|
||||
},
|
||||
},
|
||||
},
|
||||
context
|
||||
)
|
||||
|
||||
const existingTypesMap = new Map(
|
||||
existingTypes.map<[string, ProductType]>((type) => [type.value, type])
|
||||
)
|
||||
|
||||
const upsertedTypes: ProductType[] = []
|
||||
const typesToCreate: RequiredEntityData<ProductType>[] = []
|
||||
|
||||
types.forEach((type) => {
|
||||
const aType = existingTypesMap.get(type.value)
|
||||
if (aType) {
|
||||
upsertedTypes.push(aType)
|
||||
} else {
|
||||
const newType = (manager as SqlEntityManager).create(ProductType, type)
|
||||
typesToCreate.push(newType)
|
||||
}
|
||||
})
|
||||
|
||||
if (typesToCreate.length) {
|
||||
const newTypes: ProductType[] = []
|
||||
typesToCreate.forEach((type) => {
|
||||
newTypes.push((manager as SqlEntityManager).create(ProductType, type))
|
||||
})
|
||||
|
||||
await (manager as SqlEntityManager).persist(newTypes)
|
||||
upsertedTypes.push(...newTypes)
|
||||
}
|
||||
|
||||
return upsertedTypes
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<void> {
|
||||
await (manager as SqlEntityManager).nativeDelete(
|
||||
Product,
|
||||
{ id: { $in: ids } },
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async create(
|
||||
data: unknown[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<ProductType[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
@@ -1,74 +1,92 @@
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import {
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
LoadStrategy,
|
||||
RequiredEntityData,
|
||||
} from "@mikro-orm/core"
|
||||
import { deduplicateIfNecessary } from "../utils"
|
||||
import { ProductVariant } from "@models"
|
||||
import { DAL } from "@medusajs/types"
|
||||
import { Product, ProductVariant } from "@models"
|
||||
import { Context, DAL } from "@medusajs/types"
|
||||
import { AbstractBaseRepository } from "./base"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
|
||||
export class ProductVariantRepository implements DAL.RepositoryService {
|
||||
export class ProductVariantRepository extends AbstractBaseRepository<ProductVariant> {
|
||||
protected readonly manager_: SqlEntityManager
|
||||
constructor({ manager }) {
|
||||
this.manager_ = manager.fork()
|
||||
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async find<T = ProductVariant>(
|
||||
findOptions: DAL.FindOptions<T> = { where: {} },
|
||||
context: { transaction?: any } = {}
|
||||
): Promise<T[]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<ProductVariant> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<ProductVariant[]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return (await this.manager_.find(
|
||||
return await manager.find(
|
||||
ProductVariant,
|
||||
findOptions_.where as MikroFilterQuery<ProductVariant>,
|
||||
findOptions_.options as MikroOptions<ProductVariant>
|
||||
)) as unknown as T[]
|
||||
)
|
||||
}
|
||||
|
||||
async findAndCount<T = ProductVariant>(
|
||||
findOptions: DAL.FindOptions<T> = { where: {} },
|
||||
context: { transaction?: any } = {}
|
||||
): Promise<[T[], number]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<ProductVariant> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<[ProductVariant[], number]> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return (await this.manager_.findAndCount(
|
||||
return await manager.findAndCount(
|
||||
ProductVariant,
|
||||
findOptions_.where as MikroFilterQuery<ProductVariant>,
|
||||
findOptions_.options as MikroOptions<ProductVariant>
|
||||
)) as unknown as [T[], number]
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<void> {
|
||||
await (manager as SqlEntityManager).nativeDelete(
|
||||
Product,
|
||||
{ id: { $in: ids } },
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async create(
|
||||
data: RequiredEntityData<ProductVariant>[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<ProductVariant[]> {
|
||||
const variants = data.map((variant) => {
|
||||
return (manager as SqlEntityManager).create(ProductVariant, variant)
|
||||
})
|
||||
|
||||
await (manager as SqlEntityManager).persist(variants)
|
||||
|
||||
return variants
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,37 @@
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { Product } from "@models"
|
||||
import {
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
LoadStrategy,
|
||||
} from "@mikro-orm/core"
|
||||
import { deduplicateIfNecessary } from "../utils"
|
||||
import { DAL } from "@medusajs/types"
|
||||
import {
|
||||
Context,
|
||||
DAL,
|
||||
ProductTypes,
|
||||
WithRequiredProperty,
|
||||
} from "@medusajs/types"
|
||||
import { AbstractBaseRepository } from "./base"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
|
||||
export class ProductRepository implements DAL.RepositoryService<Product> {
|
||||
export class ProductRepository extends AbstractBaseRepository<Product> {
|
||||
protected readonly manager_: SqlEntityManager
|
||||
constructor({ manager }) {
|
||||
this.manager_ = manager.fork()
|
||||
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async find(
|
||||
findOptions: DAL.FindOptions<Product> = { where: {} },
|
||||
context: { transaction?: any } = {}
|
||||
context: Context = {}
|
||||
): Promise<Product[]> {
|
||||
// Spread is used to cssopy the options in case of manipulation to prevent side effects
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
@@ -38,7 +39,7 @@ export class ProductRepository implements DAL.RepositoryService<Product> {
|
||||
|
||||
await this.mutateNotInCategoriesConstraints(findOptions_)
|
||||
|
||||
return await this.manager_.find(
|
||||
return await manager.find(
|
||||
Product,
|
||||
findOptions_.where as MikroFilterQuery<Product>,
|
||||
findOptions_.options as MikroOptions<Product>
|
||||
@@ -47,20 +48,13 @@ export class ProductRepository implements DAL.RepositoryService<Product> {
|
||||
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<Product> = { where: {} },
|
||||
context: { transaction?: any } = {}
|
||||
context: Context = {}
|
||||
): Promise<[Product[], number]> {
|
||||
// Spread is used to copy the options in case of manipulation to prevent side effects
|
||||
const findOptions_ = { ...findOptions }
|
||||
|
||||
findOptions_.options ??= {}
|
||||
findOptions_.options.limit ??= 15
|
||||
|
||||
if (findOptions_.options.populate) {
|
||||
deduplicateIfNecessary(findOptions_.options.populate)
|
||||
}
|
||||
|
||||
if (context.transaction) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transaction })
|
||||
if (context.transactionManager) {
|
||||
Object.assign(findOptions_.options, { ctx: context.transactionManager })
|
||||
}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
@@ -75,17 +69,20 @@ export class ProductRepository implements DAL.RepositoryService<Product> {
|
||||
findOptions_.options as MikroOptions<Product>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* In order to be able to have a strict not in categories, and prevent a product
|
||||
* to be return in the case it also belongs to other categories, we need to
|
||||
* first find all products that are in the categories, and then exclude them
|
||||
*/
|
||||
private async mutateNotInCategoriesConstraints(
|
||||
findOptions: DAL.FindOptions<Product> = { where: {} }
|
||||
protected async mutateNotInCategoriesConstraints(
|
||||
findOptions: DAL.FindOptions<Product> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<void> {
|
||||
const manager = (context.transactionManager ??
|
||||
this.manager_) as SqlEntityManager
|
||||
|
||||
if (findOptions.where.categories?.id?.["$nin"]) {
|
||||
const productsInCategories = await this.manager_.find(
|
||||
const productsInCategories = await manager.find(
|
||||
Product,
|
||||
{
|
||||
categories: {
|
||||
@@ -109,4 +106,33 @@ export class ProductRepository implements DAL.RepositoryService<Product> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<void> {
|
||||
await (manager as SqlEntityManager).nativeDelete(
|
||||
Product,
|
||||
{ id: { $in: ids } },
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async create(
|
||||
data: WithRequiredProperty<ProductTypes.CreateProductOnlyDTO, "status">[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<Product[]> {
|
||||
console.log((this as any).prototype)
|
||||
const products = data.map((product) => {
|
||||
return (manager as SqlEntityManager).create(Product, product)
|
||||
})
|
||||
|
||||
await (manager as SqlEntityManager).persist(products)
|
||||
|
||||
return products
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
import { LoaderOptions, Logger } from "@medusajs/types"
|
||||
import {
|
||||
ProductServiceInitializeCustomDataLayerOptions,
|
||||
ProductServiceInitializeOptions,
|
||||
} from "../types"
|
||||
import { createConnection, loadDatabaseConfig } from "../utils"
|
||||
import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types"
|
||||
import { createConnection } from "../utils"
|
||||
import * as ProductModels from "@models"
|
||||
import { EntitySchema } from "@mikro-orm/core"
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
|
||||
/**
|
||||
* This script is only valid for mikro orm managers. If a user provide a custom manager
|
||||
@@ -19,14 +16,14 @@ export async function revertMigration({
|
||||
logger,
|
||||
}: Pick<
|
||||
LoaderOptions<
|
||||
| ProductServiceInitializeOptions
|
||||
| ProductServiceInitializeCustomDataLayerOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
>,
|
||||
"options" | "logger"
|
||||
> = {}) {
|
||||
logger ??= console as unknown as Logger
|
||||
|
||||
const dbData = loadDatabaseConfig(options)
|
||||
const dbData = ModulesSdkUtils.loadDatabaseConfig("product", options)
|
||||
const entities = Object.values(ProductModels) as unknown as EntitySchema[]
|
||||
|
||||
const orm = await createConnection(dbData, entities)
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { LoaderOptions, Logger } from "@medusajs/types"
|
||||
import {
|
||||
ProductServiceInitializeCustomDataLayerOptions,
|
||||
ProductServiceInitializeOptions,
|
||||
} from "../types"
|
||||
import { createConnection, loadDatabaseConfig } from "../utils"
|
||||
import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types"
|
||||
import { createConnection } from "../utils"
|
||||
import * as ProductModels from "@models"
|
||||
import { EntitySchema } from "@mikro-orm/core"
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
|
||||
/**
|
||||
* This script is only valid for mikro orm managers. If a user provide a custom manager
|
||||
@@ -19,14 +16,14 @@ export async function runMigrations({
|
||||
logger,
|
||||
}: Pick<
|
||||
LoaderOptions<
|
||||
| ProductServiceInitializeOptions
|
||||
| ProductServiceInitializeCustomDataLayerOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
>,
|
||||
"options" | "logger"
|
||||
> = {}) {
|
||||
logger ??= console as unknown as Logger
|
||||
|
||||
const dbData = loadDatabaseConfig(options)
|
||||
const dbData = ModulesSdkUtils.loadDatabaseConfig("product", options)
|
||||
const entities = Object.values(ProductModels) as unknown as EntitySchema[]
|
||||
|
||||
const orm = await createConnection(dbData, entities)
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { createConnection, loadDatabaseConfig } from "../utils"
|
||||
import { createConnection } from "../utils"
|
||||
import * as ProductModels from "@models"
|
||||
import { Product, ProductCategory, ProductVariant } from "@models"
|
||||
import { EntitySchema } from "@mikro-orm/core"
|
||||
import { LoaderOptions, Logger } from "@medusajs/types"
|
||||
import {
|
||||
ProductServiceInitializeCustomDataLayerOptions,
|
||||
ProductServiceInitializeOptions,
|
||||
} from "../types"
|
||||
import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types"
|
||||
import { EOL } from "os"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { resolve } from "path"
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
|
||||
export async function run({
|
||||
options,
|
||||
@@ -18,8 +15,8 @@ export async function run({
|
||||
}: Partial<
|
||||
Pick<
|
||||
LoaderOptions<
|
||||
| ProductServiceInitializeOptions
|
||||
| ProductServiceInitializeCustomDataLayerOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
>,
|
||||
"options" | "logger"
|
||||
>
|
||||
@@ -38,7 +35,7 @@ export async function run({
|
||||
|
||||
logger ??= console as unknown as Logger
|
||||
|
||||
const dbData = loadDatabaseConfig(options)
|
||||
const dbData = ModulesSdkUtils.loadDatabaseConfig("product", options)
|
||||
const entities = Object.values(ProductModels) as unknown as EntitySchema[]
|
||||
|
||||
const orm = await createConnection(dbData, entities)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { asClass, asValue, createContainer } from "awilix"
|
||||
import { ProductService } from "@services"
|
||||
|
||||
export const nonExistingProductId = "non-existing-id"
|
||||
|
||||
export const mockContainer = createContainer()
|
||||
mockContainer.register({
|
||||
transaction: asValue(async (task) => await task()),
|
||||
productRepository: asValue({
|
||||
find: jest.fn().mockImplementation(async ({ where: { id } }) => {
|
||||
if (id === nonExistingProductId) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [{}]
|
||||
}),
|
||||
findAndCount: jest.fn().mockResolvedValue([[], 0]),
|
||||
}),
|
||||
productService: asClass(ProductService),
|
||||
})
|
||||
@@ -1,29 +1,65 @@
|
||||
import { asClass, asValue, createContainer } from "awilix"
|
||||
import { ProductService } from "@services"
|
||||
|
||||
const container = createContainer()
|
||||
container.register({
|
||||
productRepository: asValue({
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
findAndCount: jest.fn().mockResolvedValue([[], 0]),
|
||||
}),
|
||||
productVariantService: asValue({
|
||||
list: jest.fn().mockResolvedValue([]),
|
||||
}),
|
||||
productTagService: asValue({
|
||||
list: jest.fn().mockResolvedValue([]),
|
||||
}),
|
||||
productService: asClass(ProductService),
|
||||
})
|
||||
import { mockContainer, nonExistingProductId } from "../__fixtures__/product"
|
||||
|
||||
describe("Product service", function () {
|
||||
beforeEach(function () {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should retrieve a product", async function () {
|
||||
const productService = mockContainer.resolve("productService")
|
||||
const productRepository = mockContainer.resolve("productRepository")
|
||||
|
||||
const productId = "existing-product"
|
||||
await productService.retrieve(productId)
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
id: productId,
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: undefined,
|
||||
populate: [],
|
||||
withDeleted: undefined,
|
||||
},
|
||||
},
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to retrieve a product", async function () {
|
||||
const productService = mockContainer.resolve("productService")
|
||||
const productRepository = mockContainer.resolve("productRepository")
|
||||
|
||||
const err = await productService
|
||||
.retrieve(nonExistingProductId)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
id: nonExistingProductId,
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: undefined,
|
||||
populate: [],
|
||||
withDeleted: undefined,
|
||||
},
|
||||
},
|
||||
undefined
|
||||
)
|
||||
|
||||
expect(err.message).toBe(
|
||||
`Product with id: ${nonExistingProductId} was not found`
|
||||
)
|
||||
})
|
||||
|
||||
it("should list products", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
const productService = mockContainer.resolve("productService")
|
||||
const productRepository = mockContainer.resolve("productRepository")
|
||||
|
||||
const filters = {}
|
||||
const config = {
|
||||
@@ -32,27 +68,31 @@ describe("Product service", function () {
|
||||
|
||||
await productService.list(filters, config)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith({
|
||||
where: {},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: undefined,
|
||||
offset: undefined,
|
||||
populate: [],
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: undefined,
|
||||
populate: [],
|
||||
withDeleted: undefined,
|
||||
},
|
||||
},
|
||||
})
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it("should list products with filters", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
const productService = mockContainer.resolve("productService")
|
||||
const productRepository = mockContainer.resolve("productRepository")
|
||||
|
||||
const filters = {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
const config = {
|
||||
relations: [],
|
||||
@@ -60,33 +100,37 @@ describe("Product service", function () {
|
||||
|
||||
await productService.list(filters, config)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"]
|
||||
}
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: undefined,
|
||||
populate: [],
|
||||
withDeleted: undefined,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: undefined,
|
||||
offset: undefined,
|
||||
populate: [],
|
||||
},
|
||||
})
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it("should list products with filters and relations", async function () {
|
||||
const productService = container.resolve("productService")
|
||||
const productRepository = container.resolve("productRepository")
|
||||
const productService = mockContainer.resolve("productService")
|
||||
const productRepository = mockContainer.resolve("productRepository")
|
||||
|
||||
const filters = {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
const config = {
|
||||
relations: ["tags"],
|
||||
@@ -94,20 +138,62 @@ describe("Product service", function () {
|
||||
|
||||
await productService.list(filters, config)
|
||||
|
||||
expect(productRepository.find).toHaveBeenCalledWith({
|
||||
where: {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"]
|
||||
}
|
||||
expect(productRepository.find).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: undefined,
|
||||
withDeleted: undefined,
|
||||
populate: ["tags"],
|
||||
},
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: undefined,
|
||||
offset: undefined,
|
||||
populate: ["tags"],
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it("should list and count the products with filters and relations", async function () {
|
||||
const productService = mockContainer.resolve("productService")
|
||||
const productRepository = mockContainer.resolve("productRepository")
|
||||
|
||||
const filters = {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
const config = {
|
||||
relations: ["tags"],
|
||||
}
|
||||
|
||||
await productService.listAndCount(filters, config)
|
||||
|
||||
expect(productRepository.findAndCount).toHaveBeenCalledWith(
|
||||
{
|
||||
where: {
|
||||
tags: {
|
||||
value: {
|
||||
$in: ["test"],
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
fields: undefined,
|
||||
limit: 15,
|
||||
offset: undefined,
|
||||
withDeleted: undefined,
|
||||
populate: ["tags"],
|
||||
},
|
||||
},
|
||||
undefined
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -4,3 +4,6 @@ export { default as ProductTagService } from "./product-tag"
|
||||
export { default as ProductVariantService } from "./product-variant"
|
||||
export { default as ProductCollectionService } from "./product-collection"
|
||||
export { default as ProductCategoryService } from "./product-category"
|
||||
export { default as ProductTypeService } from "./product-type"
|
||||
export { default as ProductOptionService } from "./product-option"
|
||||
export { default as ProductImageService } from "./product-image"
|
||||
@@ -1,34 +1,99 @@
|
||||
import { ProductCategory } from "@models"
|
||||
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
|
||||
import { buildQuery } from "../utils"
|
||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
||||
import { ModulesSdkUtils, MedusaError, isDefined } from "@medusajs/utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productCategoryRepository: DAL.RepositoryService
|
||||
productCategoryRepository: DAL.TreeRepositoryService
|
||||
}
|
||||
|
||||
export default class ProductCategoryService<TEntity = ProductCategory> {
|
||||
protected readonly productCategoryRepository_: DAL.RepositoryService
|
||||
export default class ProductCategoryService<
|
||||
TEntity extends ProductCategory = ProductCategory
|
||||
> {
|
||||
protected readonly productCategoryRepository_: DAL.TreeRepositoryService
|
||||
|
||||
constructor({ productCategoryRepository }: InjectedDependencies) {
|
||||
this.productCategoryRepository_ = productCategoryRepository
|
||||
}
|
||||
|
||||
async retrieve(
|
||||
productCategoryId: string,
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity> {
|
||||
if (!isDefined(productCategoryId)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`"productCategoryId" must be defined`
|
||||
)
|
||||
}
|
||||
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>({
|
||||
id: productCategoryId,
|
||||
}, config)
|
||||
|
||||
const transformOptions = {
|
||||
includeDescendantsTree: true,
|
||||
}
|
||||
|
||||
const productCategories = await this.productCategoryRepository_.find(
|
||||
queryOptions,
|
||||
transformOptions,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
if (!productCategories?.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`ProductCategory with id: ${productCategoryId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return productCategories[0] as TEntity
|
||||
}
|
||||
|
||||
async list(
|
||||
filters: ProductTypes.FilterableProductCategoryProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity[]> {
|
||||
const transformOptions = {
|
||||
includeDescendantsTree: filters?.include_descendants_tree || false
|
||||
includeDescendantsTree: filters?.include_descendants_tree || false,
|
||||
}
|
||||
delete filters.include_descendants_tree
|
||||
|
||||
const queryOptions = buildQuery<TEntity>(filters, config)
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
|
||||
filters,
|
||||
config
|
||||
)
|
||||
queryOptions.where ??= {}
|
||||
|
||||
return await this.productCategoryRepository_.find(
|
||||
return (await this.productCategoryRepository_.find(
|
||||
queryOptions,
|
||||
transformOptions,
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
async listAndCount(
|
||||
filters: ProductTypes.FilterableProductCategoryProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<[TEntity[], number]> {
|
||||
const transformOptions = {
|
||||
includeDescendantsTree: filters?.include_descendants_tree || false,
|
||||
}
|
||||
delete filters.include_descendants_tree
|
||||
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCategory>(
|
||||
filters,
|
||||
config
|
||||
)
|
||||
queryOptions.where ??= {}
|
||||
|
||||
return (await this.productCategoryRepository_.findAndCount(
|
||||
queryOptions,
|
||||
transformOptions,
|
||||
sharedContext
|
||||
)) as [TEntity[], number]
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,74 @@
|
||||
import { ProductCollection } from "@models"
|
||||
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
|
||||
import { buildQuery } from "../utils"
|
||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
||||
import { ModulesSdkUtils, retrieveEntity } from "@medusajs/utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productCollectionRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
export default class ProductCollectionService<TEntity = ProductCollection> {
|
||||
protected readonly productCollectionRepository_: DAL.RepositoryService<TEntity>
|
||||
export default class ProductCollectionService<
|
||||
TEntity extends ProductCollection = ProductCollection
|
||||
> {
|
||||
protected readonly productCollectionRepository_: DAL.TreeRepositoryService
|
||||
|
||||
constructor({ productCollectionRepository }: InjectedDependencies) {
|
||||
this.productCollectionRepository_ = productCollectionRepository
|
||||
}
|
||||
|
||||
async retrieve(
|
||||
productCollectionId: string,
|
||||
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity> {
|
||||
return (await retrieveEntity<
|
||||
ProductCollection,
|
||||
ProductTypes.ProductCollectionDTO
|
||||
>({
|
||||
id: productCollectionId,
|
||||
entityName: ProductCollection.name,
|
||||
repository: this.productCollectionRepository_,
|
||||
config,
|
||||
sharedContext,
|
||||
})) as TEntity
|
||||
}
|
||||
|
||||
async list(
|
||||
filters: ProductTypes.FilterableProductCollectionProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity[]> {
|
||||
const queryOptions = buildQuery<TEntity>(filters, config)
|
||||
return (await this.productCollectionRepository_.find(
|
||||
this.buildListQueryOptions(filters, config),
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
async listAndCount(
|
||||
filters: ProductTypes.FilterableProductCollectionProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<[TEntity[], number]> {
|
||||
return (await this.productCollectionRepository_.findAndCount(
|
||||
this.buildListQueryOptions(filters, config),
|
||||
sharedContext
|
||||
)) as [TEntity[], number]
|
||||
}
|
||||
|
||||
protected buildListQueryOptions(
|
||||
filters: ProductTypes.FilterableProductCollectionProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCollectionDTO> = {}
|
||||
) {
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductCollection>(
|
||||
filters,
|
||||
config
|
||||
)
|
||||
|
||||
queryOptions.where ??= {}
|
||||
|
||||
if (filters.title) {
|
||||
queryOptions.where["title"] = { $like: filters.title }
|
||||
}
|
||||
|
||||
return await this.productCollectionRepository_.find(queryOptions)
|
||||
return queryOptions
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Image } from "@models"
|
||||
import { Context, DAL } from "@medusajs/types"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
import { ProductImageRepository } from "@repositories"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productImageRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
export default class ProductImageService<TEntity extends Image = Image> {
|
||||
protected readonly productImageRepository_: DAL.RepositoryService
|
||||
|
||||
constructor({ productImageRepository }: InjectedDependencies) {
|
||||
this.productImageRepository_ = productImageRepository
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productImageRepository_")
|
||||
async upsert(
|
||||
urls: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return (await (this.productImageRepository_ as ProductImageRepository)
|
||||
.upsert!(urls, sharedContext)) as TEntity[]
|
||||
}
|
||||
}
|
||||
@@ -1,66 +1,105 @@
|
||||
import {
|
||||
ProductCategoryService,
|
||||
ProductCollectionService,
|
||||
ProductOptionService,
|
||||
ProductService,
|
||||
ProductTagService,
|
||||
ProductTypeService,
|
||||
ProductVariantService,
|
||||
} from "@services"
|
||||
import {
|
||||
Image,
|
||||
Product,
|
||||
ProductCategory,
|
||||
ProductCollection,
|
||||
ProductOption,
|
||||
ProductTag,
|
||||
ProductType,
|
||||
ProductVariant,
|
||||
} from "@models"
|
||||
import { FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
|
||||
import {
|
||||
Context,
|
||||
CreateProductOnlyDTO,
|
||||
DAL,
|
||||
FindConfig,
|
||||
InternalModuleDeclaration,
|
||||
ProductTypes,
|
||||
} from "@medusajs/types"
|
||||
import ProductImageService from "./product-image"
|
||||
import {
|
||||
InjectTransactionManager,
|
||||
isDefined,
|
||||
isString,
|
||||
kebabCase,
|
||||
MedusaContext,
|
||||
} from "@medusajs/utils"
|
||||
import { shouldForceTransaction } from "../utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
productService: ProductService<any>
|
||||
productVariantService: ProductVariantService<any>
|
||||
productVariantService: ProductVariantService<any, any>
|
||||
productTagService: ProductTagService<any>
|
||||
productCategoryService: ProductCategoryService<any>
|
||||
productCollectionService: ProductCollectionService<any>
|
||||
productImageService: ProductImageService<any>
|
||||
productTypeService: ProductTypeService<any>
|
||||
productOptionService: ProductOptionService<any>
|
||||
}
|
||||
|
||||
export default class ProductModuleService<
|
||||
TProduct = Product,
|
||||
TProductVariant = ProductVariant,
|
||||
TProductTag = ProductTag,
|
||||
TProductCollection = ProductCollection,
|
||||
TProductCategory = ProductCategory
|
||||
> implements
|
||||
ProductTypes.IProductModuleService<
|
||||
TProduct,
|
||||
TProductVariant,
|
||||
TProductTag,
|
||||
TProductCollection,
|
||||
TProductCategory
|
||||
>
|
||||
TProduct extends Product = Product,
|
||||
TProductVariant extends ProductVariant = ProductVariant,
|
||||
TProductTag extends ProductTag = ProductTag,
|
||||
TProductCollection extends ProductCollection = ProductCollection,
|
||||
TProductCategory extends ProductCategory = ProductCategory,
|
||||
TProductImage extends Image = Image,
|
||||
TProductType extends ProductType = ProductType,
|
||||
TProductOption extends ProductOption = ProductOption
|
||||
> implements ProductTypes.IProductModuleService
|
||||
{
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
protected readonly productService_: ProductService<TProduct>
|
||||
protected readonly productVariantService: ProductVariantService<TProductVariant>
|
||||
protected readonly productCategoryService: ProductCategoryService<TProductCategory>
|
||||
protected readonly productTagService: ProductTagService<TProductTag>
|
||||
protected readonly productCollectionService: ProductCollectionService<TProductCollection>
|
||||
protected readonly productVariantService_: ProductVariantService<
|
||||
TProductVariant,
|
||||
TProduct
|
||||
>
|
||||
protected readonly productCategoryService_: ProductCategoryService<TProductCategory>
|
||||
protected readonly productTagService_: ProductTagService<TProductTag>
|
||||
protected readonly productCollectionService_: ProductCollectionService<TProductCollection>
|
||||
protected readonly productImageService_: ProductImageService<TProductImage>
|
||||
protected readonly productTypeService_: ProductTypeService<TProductType>
|
||||
protected readonly productOptionService_: ProductOptionService<TProductOption>
|
||||
|
||||
constructor({
|
||||
productService,
|
||||
productVariantService,
|
||||
productTagService,
|
||||
productCategoryService,
|
||||
productCollectionService,
|
||||
}: InjectedDependencies) {
|
||||
constructor(
|
||||
{
|
||||
baseRepository,
|
||||
productService,
|
||||
productVariantService,
|
||||
productTagService,
|
||||
productCategoryService,
|
||||
productCollectionService,
|
||||
productImageService,
|
||||
productTypeService,
|
||||
productOptionService,
|
||||
}: InjectedDependencies,
|
||||
protected readonly moduleDeclaration: InternalModuleDeclaration
|
||||
) {
|
||||
this.baseRepository_ = baseRepository
|
||||
this.productService_ = productService
|
||||
this.productVariantService = productVariantService
|
||||
this.productTagService = productTagService
|
||||
this.productCategoryService = productCategoryService
|
||||
this.productCollectionService = productCollectionService
|
||||
this.productVariantService_ = productVariantService
|
||||
this.productTagService_ = productTagService
|
||||
this.productCategoryService_ = productCategoryService
|
||||
this.productCollectionService_ = productCollectionService
|
||||
this.productImageService_ = productImageService
|
||||
this.productTypeService_ = productTypeService
|
||||
this.productOptionService_ = productOptionService
|
||||
}
|
||||
|
||||
async list(
|
||||
filters: ProductTypes.FilterableProductProps = {},
|
||||
config: FindConfig<ProductTypes.ProductDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductDTO[]> {
|
||||
const products = await this.productService_.list(
|
||||
filters,
|
||||
@@ -71,10 +110,22 @@ export default class ProductModuleService<
|
||||
return JSON.parse(JSON.stringify(products))
|
||||
}
|
||||
|
||||
async retrieve(
|
||||
productId: string,
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductDTO> {
|
||||
const product = await this.productService_.retrieve(
|
||||
productId,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return JSON.parse(JSON.stringify(product))
|
||||
}
|
||||
|
||||
async listAndCount(
|
||||
filters: ProductTypes.FilterableProductProps = {},
|
||||
config: FindConfig<ProductTypes.ProductDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<[ProductTypes.ProductDTO[], number]> {
|
||||
const [products, count] = await this.productService_.listAndCount(
|
||||
filters,
|
||||
@@ -85,12 +136,26 @@ export default class ProductModuleService<
|
||||
return [JSON.parse(JSON.stringify(products)), count]
|
||||
}
|
||||
|
||||
async retrieveVariant(
|
||||
productVariantId: string,
|
||||
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductVariantDTO> {
|
||||
const productVariant = await this.productVariantService_.retrieve(
|
||||
productVariantId,
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return JSON.parse(JSON.stringify(productVariant))
|
||||
}
|
||||
|
||||
async listVariants(
|
||||
filters: ProductTypes.FilterableProductVariantProps = {},
|
||||
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductVariantDTO[]> {
|
||||
const variants = await this.productVariantService.list(
|
||||
const variants = await this.productVariantService_.list(
|
||||
filters,
|
||||
config,
|
||||
sharedContext
|
||||
@@ -99,12 +164,26 @@ export default class ProductModuleService<
|
||||
return JSON.parse(JSON.stringify(variants))
|
||||
}
|
||||
|
||||
async listAndCountVariants(
|
||||
filters: ProductTypes.FilterableProductVariantProps = {},
|
||||
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<[ProductTypes.ProductVariantDTO[], number]> {
|
||||
const [variants, count] = await this.productVariantService_.listAndCount(
|
||||
filters,
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return [JSON.parse(JSON.stringify(variants)), count]
|
||||
}
|
||||
|
||||
async listTags(
|
||||
filters: ProductTypes.FilterableProductTagProps = {},
|
||||
config: FindConfig<ProductTypes.ProductTagDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductTagDTO[]> {
|
||||
const tags = await this.productTagService.list(
|
||||
const tags = await this.productTagService_.list(
|
||||
filters,
|
||||
config,
|
||||
sharedContext
|
||||
@@ -113,12 +192,26 @@ export default class ProductModuleService<
|
||||
return JSON.parse(JSON.stringify(tags))
|
||||
}
|
||||
|
||||
async retrieveCollection(
|
||||
productCollectionId: string,
|
||||
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductCollectionDTO> {
|
||||
const productCollection = await this.productCollectionService_.retrieve(
|
||||
productCollectionId,
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return JSON.parse(JSON.stringify(productCollection))
|
||||
}
|
||||
|
||||
async listCollections(
|
||||
filters: ProductTypes.FilterableProductCollectionProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductCollectionDTO[]> {
|
||||
const collections = await this.productCollectionService.list(
|
||||
const collections = await this.productCollectionService_.list(
|
||||
filters,
|
||||
config,
|
||||
sharedContext
|
||||
@@ -127,12 +220,40 @@ export default class ProductModuleService<
|
||||
return JSON.parse(JSON.stringify(collections))
|
||||
}
|
||||
|
||||
async listAndCountCollections(
|
||||
filters: ProductTypes.FilterableProductCollectionProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCollectionDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<[ProductTypes.ProductCollectionDTO[], number]> {
|
||||
const collections = await this.productCollectionService_.listAndCount(
|
||||
filters,
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return JSON.parse(JSON.stringify(collections))
|
||||
}
|
||||
|
||||
async retrieveCategory(
|
||||
productCategoryId: string,
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductCategoryDTO> {
|
||||
const productCategory = await this.productCategoryService_.retrieve(
|
||||
productCategoryId,
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return JSON.parse(JSON.stringify(productCategory))
|
||||
}
|
||||
|
||||
async listCategories(
|
||||
filters: ProductTypes.FilterableProductCategoryProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<ProductTypes.ProductCategoryDTO[]> {
|
||||
const categories = await this.productCategoryService.list(
|
||||
const categories = await this.productCategoryService_.list(
|
||||
filters,
|
||||
config,
|
||||
sharedContext
|
||||
@@ -140,4 +261,199 @@ export default class ProductModuleService<
|
||||
|
||||
return JSON.parse(JSON.stringify(categories))
|
||||
}
|
||||
|
||||
async listAndCountCategories(
|
||||
filters: ProductTypes.FilterableProductCategoryProps = {},
|
||||
config: FindConfig<ProductTypes.ProductCategoryDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<[ProductTypes.ProductCategoryDTO[], number]> {
|
||||
const categories = await this.productCategoryService_.listAndCount(
|
||||
filters,
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return JSON.parse(JSON.stringify(categories))
|
||||
}
|
||||
|
||||
async create(data: ProductTypes.CreateProductDTO[], sharedContext?: Context) {
|
||||
const products = await this.create_(data, sharedContext)
|
||||
|
||||
return this.baseRepository_.serialize<
|
||||
TProduct[],
|
||||
ProductTypes.ProductDTO[]
|
||||
>(products, {
|
||||
populate: true,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
protected async create_(
|
||||
data: ProductTypes.CreateProductDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TProduct[]> {
|
||||
const productVariantsMap = new Map<
|
||||
string,
|
||||
ProductTypes.CreateProductVariantDTO[]
|
||||
>()
|
||||
const productOptionsMap = new Map<
|
||||
string,
|
||||
ProductTypes.CreateProductOptionDTO[]
|
||||
>()
|
||||
|
||||
const productsData = await Promise.all(
|
||||
data.map(async (product) => {
|
||||
const productData = { ...product }
|
||||
if (!productData.handle) {
|
||||
productData.handle = kebabCase(product.title)
|
||||
}
|
||||
|
||||
const variants = productData.variants
|
||||
const options = productData.options
|
||||
delete productData.options
|
||||
delete productData.variants
|
||||
|
||||
productVariantsMap.set(productData.handle!, variants ?? [])
|
||||
productOptionsMap.set(productData.handle!, options ?? [])
|
||||
|
||||
if (!productData.thumbnail && productData.images?.length) {
|
||||
productData.thumbnail = isString(productData.images[0])
|
||||
? (productData.images[0] as string)
|
||||
: (productData.images[0] as { url: string }).url
|
||||
}
|
||||
|
||||
if (productData.is_giftcard) {
|
||||
productData.discountable = false
|
||||
}
|
||||
|
||||
if (productData.images?.length) {
|
||||
productData.images = await this.productImageService_.upsert(
|
||||
productData.images.map((image) =>
|
||||
isString(image) ? image : image.url
|
||||
),
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
if (productData.tags?.length) {
|
||||
productData.tags = await this.productTagService_.upsert(
|
||||
productData.tags,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
if (isDefined(productData.type)) {
|
||||
productData.type_id = (
|
||||
await this.productTypeService_.upsert(
|
||||
[productData.type as ProductTypes.CreateProductTypeDTO],
|
||||
sharedContext
|
||||
)
|
||||
)?.[0]!.id
|
||||
}
|
||||
|
||||
return productData as CreateProductOnlyDTO
|
||||
})
|
||||
)
|
||||
|
||||
const products = await this.productService_.create(
|
||||
productsData,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const productByHandleMap = new Map<string, TProduct>(
|
||||
products.map((product) => [product.handle!, product])
|
||||
)
|
||||
|
||||
const productOptionsData = [...productOptionsMap]
|
||||
.map(([handle, options]) => {
|
||||
return options.map((option) => {
|
||||
return {
|
||||
...option,
|
||||
product: productByHandleMap.get(handle)!,
|
||||
}
|
||||
})
|
||||
})
|
||||
.flat()
|
||||
|
||||
const productOptions = await this.productOptionService_.create(
|
||||
productOptionsData,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
for (const variants of productVariantsMap.values()) {
|
||||
variants.forEach((variant) => {
|
||||
variant.options = variant.options?.map((option, index) => {
|
||||
const productOption = productOptions[index]
|
||||
return {
|
||||
option: productOption,
|
||||
value: option.value,
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
[...productVariantsMap].map(async ([handle, variants]) => {
|
||||
return await this.productVariantService_.create(
|
||||
productByHandleMap.get(handle)!,
|
||||
variants as unknown as ProductTypes.CreateProductVariantOnlyDTO[],
|
||||
sharedContext
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
return products
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
async delete(
|
||||
productIds: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
await this.productService_.delete(productIds, sharedContext)
|
||||
}
|
||||
|
||||
async softDelete(
|
||||
productIds: string[],
|
||||
sharedContext: Context = {}
|
||||
): Promise<ProductTypes.ProductDTO[]> {
|
||||
const products = await this.softDelete_(productIds, sharedContext)
|
||||
|
||||
return this.baseRepository_.serialize<
|
||||
TProduct[],
|
||||
ProductTypes.ProductDTO[]
|
||||
>(products, {
|
||||
populate: true,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
protected async softDelete_(
|
||||
productIds: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TProduct[]> {
|
||||
return await this.productService_.softDelete(productIds, sharedContext)
|
||||
}
|
||||
|
||||
async restore(
|
||||
productIds: string[],
|
||||
sharedContext: Context = {}
|
||||
): Promise<ProductTypes.ProductDTO[]> {
|
||||
const products = await this.restore_(productIds, sharedContext)
|
||||
|
||||
return this.baseRepository_.serialize<
|
||||
TProduct[],
|
||||
ProductTypes.ProductDTO[]
|
||||
>(products, {
|
||||
populate: true,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
async restore_(
|
||||
productIds: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TProduct[]> {
|
||||
return await this.productService_.restore(productIds, sharedContext)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ProductOption } from "@models"
|
||||
import { Context, DAL, ProductTypes } from "@medusajs/types"
|
||||
import { ProductOptionRepository } from "@repositories"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productOptionRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
export default class ProductOptionService<
|
||||
TEntity extends ProductOption = ProductOption
|
||||
> {
|
||||
protected readonly productOptionRepository_: DAL.RepositoryService
|
||||
|
||||
constructor({ productOptionRepository }: InjectedDependencies) {
|
||||
this.productOptionRepository_ =
|
||||
productOptionRepository as ProductOptionRepository
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productOptionRepository_")
|
||||
async create(
|
||||
data: ProductTypes.CreateProductOptionOnlyDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return (await (
|
||||
this.productOptionRepository_ as ProductOptionRepository
|
||||
).create(data, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})) as TEntity[]
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,27 @@
|
||||
import { ProductTag } from "@models"
|
||||
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
|
||||
import { buildQuery } from "../utils"
|
||||
import {
|
||||
Context,
|
||||
CreateProductTagDTO,
|
||||
DAL,
|
||||
FindConfig,
|
||||
ProductTypes,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
ModulesSdkUtils,
|
||||
} from "@medusajs/utils"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
import { ProductTagRepository } from "@repositories"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productTagRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
export default class ProductTagService<TEntity = ProductTag> {
|
||||
protected readonly productTagRepository_: DAL.RepositoryService<TEntity>
|
||||
export default class ProductTagService<
|
||||
TEntity extends ProductTag = ProductTag
|
||||
> {
|
||||
protected readonly productTagRepository_: DAL.RepositoryService
|
||||
|
||||
constructor({ productTagRepository }: InjectedDependencies) {
|
||||
this.productTagRepository_ = productTagRepository
|
||||
@@ -16,14 +30,28 @@ export default class ProductTagService<TEntity = ProductTag> {
|
||||
async list(
|
||||
filters: ProductTypes.FilterableProductTagProps = {},
|
||||
config: FindConfig<ProductTypes.ProductTagDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity[]> {
|
||||
const queryOptions = buildQuery<TEntity>(filters, config)
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductTag>(filters, config)
|
||||
|
||||
if (filters.value) {
|
||||
queryOptions.where["value"] = { $ilike: filters.value }
|
||||
}
|
||||
|
||||
return await this.productTagRepository_.find(queryOptions)
|
||||
return (await this.productTagRepository_.find(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productTagRepository_")
|
||||
async upsert(
|
||||
tags: CreateProductTagDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return (await (this.productTagRepository_ as ProductTagRepository).upsert!(
|
||||
tags,
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ProductType } from "@models"
|
||||
import { Context, CreateProductTypeDTO, DAL } from "@medusajs/types"
|
||||
import { InjectTransactionManager, MedusaContext } from "@medusajs/utils"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
import { ProductTypeRepository } from "@repositories"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productTypeRepository: DAL.RepositoryService
|
||||
}
|
||||
|
||||
export default class ProductTypeService<
|
||||
TEntity extends ProductType = ProductType
|
||||
> {
|
||||
protected readonly productTypeRepository_: DAL.RepositoryService
|
||||
|
||||
constructor({ productTypeRepository }: InjectedDependencies) {
|
||||
this.productTypeRepository_ = productTypeRepository
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productTypeRepository_")
|
||||
async upsert(
|
||||
types: CreateProductTypeDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return (await (this.productTypeRepository_ as ProductTypeRepository)
|
||||
.upsert!(types, sharedContext)) as TEntity[]
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,115 @@
|
||||
import { ProductVariant } from "@models"
|
||||
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
|
||||
import { buildQuery } from "../utils"
|
||||
import { Product, ProductVariant } from "@models"
|
||||
import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types"
|
||||
import {
|
||||
InjectTransactionManager,
|
||||
isString,
|
||||
MedusaContext,
|
||||
ModulesSdkUtils,
|
||||
retrieveEntity,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
import ProductService from "./product"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
import { ProductVariantRepository } from "@repositories"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productVariantRepository: DAL.RepositoryService
|
||||
productService: ProductService<any>
|
||||
}
|
||||
|
||||
export default class ProductVariantService<TEntity = ProductVariant> {
|
||||
protected readonly productVariantRepository_: DAL.RepositoryService<TEntity>
|
||||
export default class ProductVariantService<
|
||||
TEntity extends ProductVariant = ProductVariant,
|
||||
TProduct extends Product = Product
|
||||
> {
|
||||
protected readonly productVariantRepository_: DAL.RepositoryService
|
||||
protected readonly productService_: ProductService<TProduct>
|
||||
|
||||
constructor({ productVariantRepository }: InjectedDependencies) {
|
||||
constructor({
|
||||
productVariantRepository,
|
||||
productService,
|
||||
}: InjectedDependencies) {
|
||||
this.productVariantRepository_ = productVariantRepository
|
||||
this.productService_ = productService
|
||||
}
|
||||
|
||||
async retrieve(
|
||||
productVariantId: string,
|
||||
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity> {
|
||||
return (await retrieveEntity<
|
||||
ProductVariant,
|
||||
ProductTypes.ProductVariantDTO
|
||||
>({
|
||||
id: productVariantId,
|
||||
entityName: ProductVariant.name,
|
||||
repository: this.productVariantRepository_,
|
||||
config,
|
||||
sharedContext,
|
||||
})) as TEntity
|
||||
}
|
||||
|
||||
async list(
|
||||
filters: ProductTypes.FilterableProductVariantProps = {},
|
||||
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity[]> {
|
||||
const queryOptions = buildQuery<TEntity>(filters, config)
|
||||
return await this.productVariantRepository_.find(queryOptions)
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductVariant>(
|
||||
filters,
|
||||
config
|
||||
)
|
||||
|
||||
return (await this.productVariantRepository_.find(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
async listAndCount(
|
||||
filters: ProductTypes.FilterableProductVariantProps = {},
|
||||
config: FindConfig<ProductTypes.ProductVariantDTO> = {},
|
||||
sharedContext?: Context
|
||||
): Promise<[TEntity[], number]> {
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<ProductVariant>(
|
||||
filters,
|
||||
config
|
||||
)
|
||||
|
||||
return (await this.productVariantRepository_.findAndCount(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)) as [TEntity[], number]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productVariantRepository_")
|
||||
async create(
|
||||
productOrId: TProduct | string,
|
||||
data: ProductTypes.CreateProductVariantOnlyDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
let product = productOrId as unknown as Product
|
||||
|
||||
if (isString(productOrId)) {
|
||||
product = await this.productService_.retrieve(
|
||||
productOrId as string,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
let computedRank = product.variants.toArray().length
|
||||
|
||||
const data_ = [...data]
|
||||
data_.forEach((variant) => {
|
||||
Object.assign(variant, {
|
||||
variant_rank: computedRank++,
|
||||
product,
|
||||
})
|
||||
})
|
||||
|
||||
return (await (
|
||||
this.productVariantRepository_ as ProductVariantRepository
|
||||
).create(data_, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})) as TEntity[]
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,55 @@
|
||||
import { ProductTagService, ProductVariantService } from "@services"
|
||||
import { Product } from "@models"
|
||||
import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types"
|
||||
import { buildQuery } from "../utils"
|
||||
import {
|
||||
Context,
|
||||
DAL,
|
||||
FindConfig,
|
||||
ProductStatus,
|
||||
ProductTypes,
|
||||
WithRequiredProperty,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
ModulesSdkUtils,
|
||||
} from "@medusajs/utils"
|
||||
import { ProductRepository } from "@repositories"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productRepository: DAL.RepositoryService
|
||||
productVariantService: ProductVariantService
|
||||
productTagService: ProductTagService
|
||||
}
|
||||
|
||||
export default class ProductService<TEntity = Product> {
|
||||
protected readonly productRepository_: DAL.RepositoryService<TEntity>
|
||||
export default class ProductService<TEntity extends Product = Product> {
|
||||
protected readonly productRepository_: DAL.RepositoryService
|
||||
|
||||
constructor({ productRepository }: InjectedDependencies) {
|
||||
this.productRepository_ = productRepository
|
||||
}
|
||||
|
||||
async retrieve(productId: string, sharedContext?: Context): Promise<TEntity> {
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<Product>({
|
||||
id: productId,
|
||||
})
|
||||
const product = await this.productRepository_.find(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
if (!product?.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Product with id: ${productId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return product[0] as TEntity
|
||||
}
|
||||
|
||||
async list(
|
||||
filters: ProductTypes.FilterableProductProps = {},
|
||||
config: FindConfig<ProductTypes.ProductDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity[]> {
|
||||
if (filters.category_ids) {
|
||||
if (Array.isArray(filters.category_ids)) {
|
||||
@@ -34,14 +64,17 @@ export default class ProductService<TEntity = Product> {
|
||||
delete filters.category_ids
|
||||
}
|
||||
|
||||
const queryOptions = buildQuery<TEntity>(filters, config)
|
||||
return await this.productRepository_.find(queryOptions)
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<Product>(filters, config)
|
||||
return (await this.productRepository_.find(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
async listAndCount(
|
||||
filters: ProductTypes.FilterableProductProps = {},
|
||||
config: FindConfig<ProductTypes.ProductDTO> = {},
|
||||
sharedContext?: SharedContext
|
||||
sharedContext?: Context
|
||||
): Promise<[TEntity[], number]> {
|
||||
if (filters.category_ids) {
|
||||
if (Array.isArray(filters.category_ids)) {
|
||||
@@ -56,7 +89,60 @@ export default class ProductService<TEntity = Product> {
|
||||
delete filters.category_ids
|
||||
}
|
||||
|
||||
const queryOptions = buildQuery<TEntity>(filters, config)
|
||||
return await this.productRepository_.findAndCount(queryOptions)
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<Product>(filters, config)
|
||||
return (await this.productRepository_.findAndCount(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)) as [TEntity[], number]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
|
||||
async create(
|
||||
data: ProductTypes.CreateProductOnlyDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
data.forEach((product) => {
|
||||
product.status ??= ProductStatus.DRAFT
|
||||
})
|
||||
|
||||
return (await (this.productRepository_ as ProductRepository).create(
|
||||
data as WithRequiredProperty<
|
||||
ProductTypes.CreateProductOnlyDTO,
|
||||
"status"
|
||||
>[],
|
||||
{
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
}
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
|
||||
async delete(
|
||||
ids: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
await this.productRepository_.delete(ids, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
|
||||
async softDelete(
|
||||
productIds: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return await this.productRepository_.softDelete(productIds, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "productRepository_")
|
||||
async restore(
|
||||
productIds: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return await this.productRepository_.restore(productIds, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,4 @@
|
||||
import { Constructor, DAL, IEventBusService } from "@medusajs/types"
|
||||
|
||||
export type ProductServiceInitializeOptions = {
|
||||
database: {
|
||||
clientUrl: string
|
||||
schema?: string
|
||||
driverOptions?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export type ProductServiceInitializeCustomDataLayerOptions = {
|
||||
manager?: any
|
||||
repositories?: { [key: string]: Constructor<DAL.RepositoryService> }
|
||||
}
|
||||
import { IEventBusService } from "@medusajs/types"
|
||||
|
||||
export type InitializeModuleInjectableDependencies = {
|
||||
eventBusService?: IEventBusService
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import { loadDatabaseConfig } from "../load-database-config"
|
||||
|
||||
describe("loadDatabaseConfig", function () {
|
||||
afterEach(() => {
|
||||
delete process.env.POSTGRES_URL
|
||||
delete process.env.PRODUCT_POSTGRES_URL
|
||||
})
|
||||
|
||||
it("should return the local configuration using the environment variable", function () {
|
||||
process.env.POSTGRES_URL = "postgres://localhost:5432/medusa"
|
||||
let config = loadDatabaseConfig()
|
||||
|
||||
expect(config).toEqual({
|
||||
clientUrl: process.env.POSTGRES_URL,
|
||||
driverOptions: {
|
||||
connection: {
|
||||
ssl: false,
|
||||
},
|
||||
},
|
||||
schema: "",
|
||||
})
|
||||
|
||||
delete process.env.POSTGRES_URL
|
||||
process.env.PRODUCT_POSTGRES_URL = "postgres://localhost:5432/medusa"
|
||||
config = loadDatabaseConfig()
|
||||
|
||||
expect(config).toEqual({
|
||||
clientUrl: process.env.PRODUCT_POSTGRES_URL,
|
||||
driverOptions: {
|
||||
connection: {
|
||||
ssl: false,
|
||||
},
|
||||
},
|
||||
schema: "",
|
||||
})
|
||||
})
|
||||
|
||||
it("should return the remote configuration using the environment variable", function () {
|
||||
process.env.POSTGRES_URL = "postgres://https://test.com:5432/medusa"
|
||||
let config = loadDatabaseConfig()
|
||||
|
||||
expect(config).toEqual({
|
||||
clientUrl: process.env.POSTGRES_URL,
|
||||
driverOptions: {
|
||||
connection: {
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
schema: "",
|
||||
})
|
||||
|
||||
delete process.env.POSTGRES_URL
|
||||
process.env.PRODUCT_POSTGRES_URL = "postgres://https://test.com:5432/medusa"
|
||||
config = loadDatabaseConfig()
|
||||
|
||||
expect(config).toEqual({
|
||||
clientUrl: process.env.PRODUCT_POSTGRES_URL,
|
||||
driverOptions: {
|
||||
connection: {
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
schema: "",
|
||||
})
|
||||
})
|
||||
|
||||
it("should return the local configuration using the options", function () {
|
||||
process.env.POSTGRES_URL = "postgres://localhost:5432/medusa"
|
||||
const options = {
|
||||
database: {
|
||||
clientUrl: "postgres://localhost:5432/medusa-test",
|
||||
},
|
||||
}
|
||||
|
||||
const config = loadDatabaseConfig(options)
|
||||
|
||||
expect(config).toEqual({
|
||||
clientUrl: options.database.clientUrl,
|
||||
driverOptions: {
|
||||
connection: {
|
||||
ssl: false,
|
||||
},
|
||||
},
|
||||
schema: "",
|
||||
})
|
||||
})
|
||||
|
||||
it("should return the remote configuration using the options", function () {
|
||||
process.env.POSTGRES_URL = "postgres://localhost:5432/medusa"
|
||||
const options = {
|
||||
database: {
|
||||
clientUrl: "postgres://https://test.com:5432/medusa-test",
|
||||
},
|
||||
}
|
||||
|
||||
const config = loadDatabaseConfig(options)
|
||||
|
||||
expect(config).toEqual({
|
||||
clientUrl: options.database.clientUrl,
|
||||
driverOptions: {
|
||||
connection: {
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
schema: "",
|
||||
})
|
||||
})
|
||||
|
||||
it("should throw if no clientUrl is provided", function () {
|
||||
let error
|
||||
try {
|
||||
loadDatabaseConfig()
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"No database clientUrl provided. Please provide the clientUrl through the PRODUCT_POSTGRES_URL or POSTGRES_URL environment variable or the options object in the initialize function."
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,15 +1,15 @@
|
||||
import { MikroORM, PostgreSqlDriver } from "@mikro-orm/postgresql"
|
||||
import { ProductServiceInitializeOptions } from "../types"
|
||||
import { ModuleServiceInitializeOptions } from "@medusajs/types"
|
||||
|
||||
export async function createConnection(
|
||||
database: ProductServiceInitializeOptions["database"],
|
||||
database: ModuleServiceInitializeOptions["database"],
|
||||
entities: any[]
|
||||
) {
|
||||
const schema = database.schema || "public"
|
||||
const orm = await MikroORM.init<PostgreSqlDriver>({
|
||||
discovery: { disableDynamicFileAccess: true },
|
||||
entities,
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
debug: database.debug ?? process.env.NODE_ENV?.startsWith("dev") ?? false,
|
||||
baseDir: process.cwd(),
|
||||
clientUrl: database.clientUrl,
|
||||
schema,
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
export * from "./query"
|
||||
import { MODULE_RESOURCE_TYPE } from "@medusajs/types"
|
||||
|
||||
export * from "./create-connection"
|
||||
export * from "./load-database-config"
|
||||
export * from "./soft-deletable"
|
||||
|
||||
export function shouldForceTransaction(target: any): boolean {
|
||||
return target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
}
|
||||
|
||||
export function doNotForceTransaction(): boolean {
|
||||
return false
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import {
|
||||
ProductServiceInitializeCustomDataLayerOptions,
|
||||
ProductServiceInitializeOptions,
|
||||
} from "../types"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
|
||||
function getEnv(key: string): string {
|
||||
const value = process.env[`PRODUCT_${key}`] ?? process.env[`${key}`]
|
||||
return value ?? ""
|
||||
}
|
||||
|
||||
function isProductServiceInitializeOptions(
|
||||
obj: unknown
|
||||
): obj is ProductServiceInitializeOptions {
|
||||
return !!(obj as ProductServiceInitializeOptions)?.database
|
||||
}
|
||||
|
||||
function getDefaultDriverOptions(
|
||||
clientUrl: string
|
||||
): ProductServiceInitializeOptions["database"]["driverOptions"] {
|
||||
const localOptions = {
|
||||
connection: {
|
||||
ssl: false,
|
||||
},
|
||||
}
|
||||
|
||||
const remoteOptions = {
|
||||
connection: {
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if (clientUrl) {
|
||||
return clientUrl.match(/localhost/i) ? localOptions : remoteOptions
|
||||
}
|
||||
|
||||
return process.env.NODE_ENV?.match(/prod/i)
|
||||
? remoteOptions
|
||||
: process.env.NODE_ENV?.match(/dev/i)
|
||||
? localOptions
|
||||
: {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the config for the database connection. The options can be retrieved
|
||||
* through PRODUCT_* (e.g PRODUCT_POSTGRES_URL) or * (e.g POSTGRES_URL) environment variables or the options object.
|
||||
* @param options
|
||||
*/
|
||||
export function loadDatabaseConfig(
|
||||
options?:
|
||||
| ProductServiceInitializeOptions
|
||||
| ProductServiceInitializeCustomDataLayerOptions
|
||||
): ProductServiceInitializeOptions["database"] {
|
||||
const clientUrl = getEnv("POSTGRES_URL")
|
||||
|
||||
const database: ProductServiceInitializeOptions["database"] = {
|
||||
clientUrl: getEnv("POSTGRES_URL"),
|
||||
schema: getEnv("POSTGRES_SCHEMA") ?? "public",
|
||||
driverOptions: JSON.parse(
|
||||
getEnv("POSTGRES_DRIVER_OPTIONS") ||
|
||||
JSON.stringify(getDefaultDriverOptions(clientUrl))
|
||||
),
|
||||
}
|
||||
|
||||
if (isProductServiceInitializeOptions(options)) {
|
||||
database.clientUrl = options.database.clientUrl ?? database.clientUrl
|
||||
database.schema = options.database.schema ?? database.schema
|
||||
database.driverOptions =
|
||||
options.database.driverOptions ??
|
||||
getDefaultDriverOptions(database.clientUrl)
|
||||
}
|
||||
|
||||
if (!database.clientUrl) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"No database clientUrl provided. Please provide the clientUrl through the PRODUCT_POSTGRES_URL or POSTGRES_URL environment variable or the options object in the initialize function."
|
||||
)
|
||||
}
|
||||
return database
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Move to a new build query utils
|
||||
*/
|
||||
import { DAL, FindConfig } from "@medusajs/types"
|
||||
import { isObject } from "@medusajs/utils"
|
||||
|
||||
export function deduplicateIfNecessary<T = any>(collection: T | T[]) {
|
||||
return Array.isArray(collection) ? [...new Set(collection)] : collection
|
||||
}
|
||||
|
||||
export function buildQuery<T = any, TDto = any>(
|
||||
filters: Record<string, any> = {},
|
||||
config: FindConfig<TDto> = {}
|
||||
): DAL.FindOptions<T> {
|
||||
const where: DAL.FilterQuery<T> = {}
|
||||
buildWhere(filters, where)
|
||||
|
||||
const findOptions: DAL.OptionsQuery<T, any> = {
|
||||
populate: config.relations ?? [],
|
||||
fields: config.select,
|
||||
limit: config.take,
|
||||
offset: config.skip,
|
||||
} as any
|
||||
|
||||
return { where, options: findOptions }
|
||||
}
|
||||
|
||||
function buildWhere(filters: Record<string, any> = {}, where = {}) {
|
||||
for (let [prop, value] of Object.entries(filters)) {
|
||||
if (Array.isArray(value)) {
|
||||
value = deduplicateIfNecessary(value)
|
||||
where[prop] = ["$in", "$nin"].includes(prop) ? value : { $in: value }
|
||||
continue
|
||||
}
|
||||
|
||||
if (isObject(value)) {
|
||||
where[prop] = {}
|
||||
buildWhere(value, where[prop])
|
||||
continue
|
||||
}
|
||||
|
||||
where[prop] = value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// TODO: Should we create a mikro orm specific package for this and the base repository?
|
||||
|
||||
import { Filter } from "@mikro-orm/core"
|
||||
import { DAL } from "@medusajs/types"
|
||||
|
||||
interface FilterArguments {
|
||||
withDeleted?: boolean
|
||||
}
|
||||
|
||||
export const SoftDeletable = (): ClassDecorator => {
|
||||
return Filter({
|
||||
name: DAL.SoftDeletableFilterKey,
|
||||
cond: ({ withDeleted }: FilterArguments = {}) => {
|
||||
if (withDeleted) {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
deleted_at: null,
|
||||
}
|
||||
},
|
||||
default: true,
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user