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:
committed by
GitHub
parent
5b91a3503a
commit
befc2f1c80
@@ -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."
|
||||
)
|
||||
})
|
||||
})
|
||||
44
packages/utils/src/modules-sdk/build-query.ts
Normal file
44
packages/utils/src/modules-sdk/build-query.ts
Normal 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
|
||||
}
|
||||
}
|
||||
1
packages/utils/src/modules-sdk/decorators/index.ts
Normal file
1
packages/utils/src/modules-sdk/decorators/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./inject-transaction-manager"
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
4
packages/utils/src/modules-sdk/index.ts
Normal file
4
packages/utils/src/modules-sdk/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./load-module-database-config"
|
||||
export * from "./decorators"
|
||||
export * from "./build-query"
|
||||
export * from "./retrieve-entity"
|
||||
@@ -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
|
||||
}
|
||||
47
packages/utils/src/modules-sdk/retrieve-entity.ts
Normal file
47
packages/utils/src/modules-sdk/retrieve-entity.ts
Normal 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]
|
||||
}
|
||||
Reference in New Issue
Block a user