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:
Adrien de Peretti
2023-07-16 20:19:23 +02:00
committed by GitHub
parent 5b91a3503a
commit befc2f1c80
98 changed files with 5444 additions and 688 deletions

View File

@@ -0,0 +1,133 @@
import { loadDatabaseConfig } from "../load-module-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("product")
expect(config).toEqual({
clientUrl: process.env.POSTGRES_URL,
driverOptions: {
connection: {
ssl: false,
},
},
debug: false,
schema: "",
})
delete process.env.POSTGRES_URL
process.env.PRODUCT_POSTGRES_URL = "postgres://localhost:5432/medusa"
config = loadDatabaseConfig("product")
expect(config).toEqual({
clientUrl: process.env.PRODUCT_POSTGRES_URL,
driverOptions: {
connection: {
ssl: false,
},
},
debug: 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("product")
expect(config).toEqual({
clientUrl: process.env.POSTGRES_URL,
driverOptions: {
connection: {
ssl: {
rejectUnauthorized: false,
},
},
},
debug: false,
schema: "",
})
delete process.env.POSTGRES_URL
process.env.PRODUCT_POSTGRES_URL = "postgres://https://test.com:5432/medusa"
config = loadDatabaseConfig("product")
expect(config).toEqual({
clientUrl: process.env.PRODUCT_POSTGRES_URL,
driverOptions: {
connection: {
ssl: {
rejectUnauthorized: false,
},
},
},
debug: 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",
},
}
let config = loadDatabaseConfig("product", options)
expect(config).toEqual({
clientUrl: options.database.clientUrl,
driverOptions: {
connection: {
ssl: false,
},
},
debug: 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",
},
}
let config = loadDatabaseConfig("product", options)
expect(config).toEqual({
clientUrl: options.database.clientUrl,
driverOptions: {
connection: {
ssl: {
rejectUnauthorized: false,
},
},
},
debug: false,
schema: "",
})
})
it("should throw if no clientUrl is provided", function () {
let error
try {
loadDatabaseConfig("product")
} 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."
)
})
})

View File

@@ -0,0 +1,44 @@
import { DAL, FindConfig, SoftDeletableFilterKey } from "@medusajs/types"
import { deduplicate, isObject } from "../common"
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: deduplicate(config.relations ?? []),
fields: config.select as string[],
limit: config.take ?? 15,
offset: config.skip,
}
if (config.withDeleted) {
findOptions.filters ??= {}
findOptions.filters[SoftDeletableFilterKey] = {
withDeleted: true,
}
}
return { where, options: findOptions }
}
function buildWhere(filters: Record<string, any> = {}, where = {}) {
for (let [prop, value] of Object.entries(filters)) {
if (Array.isArray(value)) {
value = deduplicate(value)
where[prop] = ["$in", "$nin"].includes(prop) ? value : { $in: value }
continue
}
if (isObject(value)) {
where[prop] = {}
buildWhere(value, where[prop])
continue
}
where[prop] = value
}
}

View File

@@ -0,0 +1 @@
export * from "./inject-transaction-manager"

View File

@@ -0,0 +1,48 @@
import { Context, SharedContext } from "@medusajs/types"
export function InjectTransactionManager(
shouldForceTransaction: (target: any) => boolean = () => false,
managerProperty?: string
): MethodDecorator {
return function (
target: any,
propertyKey: string | symbol,
descriptor: any
): void {
if (!target.MedusaContextIndex_) {
throw new Error(
`To apply @InjectTransactionManager you have to flag a parameter using @MedusaContext`
)
}
const originalMethod = descriptor.value
const argIndex = target.MedusaContextIndex_[propertyKey]
descriptor.value = async function (...args: any[]) {
const shouldForceTransactionRes = shouldForceTransaction(target)
const context: SharedContext | Context = args[argIndex] ?? {}
if (!shouldForceTransactionRes && context?.transactionManager) {
return await originalMethod.apply(this, args)
}
return await (!managerProperty
? this
: this[managerProperty]
).transaction(
async (transactionManager) => {
args[argIndex] = args[argIndex] ?? {}
args[argIndex].transactionManager = transactionManager
return await originalMethod.apply(this, args)
},
{
transaction: context?.transactionManager,
isolationLevel: (context as Context)?.isolationLevel,
enableNestedTransactions:
(context as Context).enableNestedTransactions ?? false,
}
)
}
}
}

View File

@@ -0,0 +1,4 @@
export * from "./load-module-database-config"
export * from "./decorators"
export * from "./build-query"
export * from "./retrieve-entity"

View File

@@ -0,0 +1,82 @@
import { MedusaError } from "../common"
import { ModulesSdkTypes } from "@medusajs/types"
function getEnv(key: string, moduleName: string): string {
const value =
process.env[`${moduleName.toUpperCase()}_${key}`] ?? process.env[`${key}`]
return value ?? ""
}
function isModuleServiceInitializeOptions(
obj: unknown
): obj is ModulesSdkTypes.ModuleServiceInitializeOptions {
return !!(obj as any)?.database
}
function getDefaultDriverOptions(clientUrl: string) {
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
* e.g through PRODUCT_* (e.g PRODUCT_POSTGRES_URL) or * (e.g POSTGRES_URL) environment variables or the options object.
* @param options
* @param moduleName
*/
export function loadDatabaseConfig(
moduleName: string,
options?:
| ModulesSdkTypes.ModuleServiceInitializeOptions
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
): ModulesSdkTypes.ModuleServiceInitializeOptions["database"] {
const clientUrl = getEnv("POSTGRES_URL", moduleName)
const database = {
clientUrl: getEnv("POSTGRES_URL", moduleName),
schema: getEnv("POSTGRES_SCHEMA", moduleName) ?? "public",
driverOptions: JSON.parse(
getEnv("POSTGRES_DRIVER_OPTIONS", moduleName) ||
JSON.stringify(getDefaultDriverOptions(clientUrl))
),
debug: process.env.NODE_ENV?.startsWith("dev") ?? false,
}
if (isModuleServiceInitializeOptions(options)) {
database.clientUrl = options.database.clientUrl ?? database.clientUrl
database.schema = options.database.schema ?? database.schema
database.driverOptions =
options.database.driverOptions ??
getDefaultDriverOptions(database.clientUrl)
database.debug = options.database.debug ?? database.debug
}
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
}

View File

@@ -0,0 +1,47 @@
import { FindConfig, DAL, Context } from "@medusajs/types"
import { MedusaError, isDefined, lowerCaseFirst } from "../common"
import { buildQuery } from "./build-query"
type RetrieveEntityParams<TDTO> = {
id: string,
entityName: string,
repository: DAL.TreeRepositoryService
config: FindConfig<TDTO>
sharedContext?: Context
}
export async function retrieveEntity<
TEntity,
TDTO,
>({
id,
entityName,
repository,
config = {},
sharedContext,
}: RetrieveEntityParams<TDTO>): Promise<TEntity> {
if (!isDefined(id)) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`"${lowerCaseFirst(entityName)}Id" must be defined`
)
}
const queryOptions = buildQuery<TEntity>({
id,
}, config)
const entities = await repository.find(
queryOptions,
sharedContext
)
if (!entities?.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`${entityName} with id: ${id} was not found`
)
}
return entities[0]
}