chore: local workflow proxying methods to pass context (#6263)

What:
- When calling a module's method inside a Local Workflow the MedusaContext is passed as the last argument to the method if not provided
- Add `requestId` to req
- A couple of fixes on Remote Joiner and the data fetcher for internal services

Why:
- The context used to initialize the workflow has to be shared with all modules. properties like transactionId will be used to emit events and requestId to trace logs for example.
This commit is contained in:
Carlos R. L. Rodrigues
2024-02-01 10:37:26 -03:00
committed by GitHub
parent a2bf6756ac
commit 45134e4d11
40 changed files with 576 additions and 170 deletions

View File

@@ -144,4 +144,79 @@ describe("toRemoteJoinerQuery", () => {
],
})
})
it("should transform a nested object with arguments and directives to a Remote Joiner Query format only using variables", async () => {
const obj = {
product: {
fields: ["id", "title", "handle"],
variants: {
fields: ["sku"],
__directives: {
directiveName: "value",
},
shipping_profiles: {
profile: {
fields: ["id", "name"],
},
},
},
},
}
const rjQuery = toRemoteJoinerQuery(obj, {
product: {
limit: 10,
offset: 0,
},
"product.variants.shipping_profiles.profile": {
context: {
customer_group: "cg_123",
region_id: "US",
},
},
})
expect(rjQuery).toEqual({
alias: "product",
fields: ["id", "title", "handle"],
expands: [
{
property: "variants",
directives: [
{
name: "directiveName",
value: "value",
},
],
fields: ["sku"],
},
{
property: "variants.shipping_profiles",
},
{
property: "variants.shipping_profiles.profile",
args: [
{
name: "context",
value: {
customer_group: "cg_123",
region_id: "US",
},
},
],
fields: ["id", "name"],
},
],
args: [
{
name: "limit",
value: 10,
},
{
name: "offset",
value: 0,
},
],
})
})
})

View File

@@ -37,10 +37,11 @@ class GraphQLParser {
}
private parseValueNode(valueNode: ValueNode): unknown {
const obj = {}
switch (valueNode.kind) {
case Kind.VARIABLE:
const variableName = valueNode.name.value
return this.variables ? this.variables[variableName] : undefined
return this.variables ? this.variables[valueNode.name.value] : undefined
case Kind.INT:
return parseInt(valueNode.value, 10)
case Kind.FLOAT:
@@ -55,7 +56,6 @@ class GraphQLParser {
case Kind.LIST:
return valueNode.values.map((v) => this.parseValueNode(v))
case Kind.OBJECT:
let obj = {}
for (const field of valueNode.fields) {
obj[field.name.value] = this.parseValueNode(field.value)
}

View File

@@ -1,53 +1,81 @@
import { RemoteJoinerQuery } from "@medusajs/types"
export function toRemoteJoinerQuery(obj: any): RemoteJoinerQuery {
export function toRemoteJoinerQuery(
obj: any,
variables: Record<string, any> = {}
): RemoteJoinerQuery {
const remoteJoinerQuery: RemoteJoinerQuery = {
alias: "",
fields: [],
expands: [],
}
function extractRecursive(obj, parentName = "", isEntryPoint = true) {
for (const key in obj) {
let entryPoint = ""
function extractRecursive(obj: any, parentName = "", isEntryPoint = true) {
for (const key of Object.keys(obj ?? {})) {
const value = obj[key]
const canExpand =
typeof value === "object" &&
!["fields", "__args", "__directives"].includes(key)
if (canExpand) {
const entityName = parentName ? `${parentName}.${key}` : key
const expandObj: any = {
property: entityName,
}
const reference = isEntryPoint ? remoteJoinerQuery : expandObj
if (value.__args) {
reference.args = Object.entries(value.__args).map(
([name, value]) => ({
name,
value,
})
)
}
if (value.__directives) {
reference.directives = Object.entries(value.__directives).map(
([name, value]) => ({ name, value })
)
}
reference.fields = value.fields
if (isEntryPoint) {
remoteJoinerQuery.alias = key
} else {
remoteJoinerQuery.expands!.push(expandObj)
}
extractRecursive(value, isEntryPoint ? "" : entityName, false)
if (!canExpand) {
continue
}
const entityName = parentName ? `${parentName}.${key}` : key
const variablesPath = !isEntryPoint
? `${entryPoint}${parentName ? "." + parentName : parentName}.${key}`
: key
if (isEntryPoint) {
entryPoint = key
}
const currentVariables = variables[variablesPath]
const expandObj: any = {
property: entityName,
}
const reference = isEntryPoint ? remoteJoinerQuery : expandObj
if (currentVariables) {
reference.args = Object.entries(currentVariables).map(
([name, value]) => ({
name,
value,
})
)
}
if (value.__args) {
reference.args = [
...(reference.__args || []),
...Object.entries(value.__args).map(([name, value]) => ({
name,
value,
})),
]
}
if (value.__directives) {
reference.directives = Object.entries(value.__directives).map(
([name, value]) => ({ name, value })
)
}
if (value.fields) {
reference.fields = value.fields
}
if (isEntryPoint) {
remoteJoinerQuery.alias = key
} else {
remoteJoinerQuery.expands!.push(expandObj)
}
extractRecursive(value, isEntryPoint ? "" : entityName, false)
}
return remoteJoinerQuery

View File

@@ -53,7 +53,7 @@ export class RemoteJoiner {
}, {})
if (expands) {
for (const key in expands) {
for (const key of Object.keys(expands ?? {})) {
const expand = expands[key]
if (expand) {
if (Array.isArray(data[key])) {
@@ -146,12 +146,14 @@ export class RemoteJoiner {
const isReadOnlyDefinition =
service.serviceName === undefined || service.isReadOnlyLink
if (!isReadOnlyDefinition) {
if (!service.alias) {
service.alias = [{ name: service.serviceName!.toLowerCase() }]
} else if (!Array.isArray(service.alias)) {
service.alias ??= []
if (!Array.isArray(service.alias)) {
service.alias = [service.alias]
}
service.alias.push({ name: service.serviceName! })
// handle alias.name as array
for (let idx = 0; idx < service.alias.length; idx++) {
const alias = service.alias[idx]
@@ -173,6 +175,11 @@ export class RemoteJoiner {
for (const alias of service.alias) {
if (this.serviceConfigCache.has(`alias_${alias.name}}`)) {
const defined = this.serviceConfigCache.get(`alias_${alias.name}}`)
if (service.serviceName === defined?.serviceName) {
continue
}
throw new Error(
`Cannot add alias "${alias.name}" for "${service.serviceName}". It is already defined for Service "${defined?.serviceName}".`
)
@@ -223,7 +230,9 @@ export class RemoteJoiner {
(rel) => rel.isInternalService === true
)
if (isInternalServicePresent) continue
if (isInternalServicePresent) {
continue
}
throw new Error(`Service "${serviceName}" was not found`)
}
@@ -338,7 +347,9 @@ export class RemoteJoiner {
relationship
)
const isObj = isDefined(response.path)
const resData = isObj ? response.data[response.path!] : response.data
let resData = isObj ? response.data[response.path!] : response.data
resData = Array.isArray(resData) ? resData : [resData]
const filteredDataArray = resData.map((data: any) =>
RemoteJoiner.filterFields(data, expand.fields, expand.expands)

View File

@@ -1,5 +1,5 @@
import { Context, LoadedModule, MedusaContainer } from "@medusajs/types"
import { createContainerLike, createMedusaContainer } from "@medusajs/utils"
import { createMedusaContainer } from "@medusajs/utils"
import { asValue } from "awilix"
import {
@@ -25,7 +25,7 @@ export class GlobalWorkflow extends WorkflowManager {
if (!Array.isArray(modulesLoaded) && modulesLoaded) {
if (!("cradle" in modulesLoaded)) {
container = createContainerLike(modulesLoaded)
container = createMedusaContainer(modulesLoaded)
} else {
container = modulesLoaded
}

View File

@@ -1,5 +1,10 @@
import { Context, LoadedModule, MedusaContainer } from "@medusajs/types"
import { createContainerLike, createMedusaContainer } from "@medusajs/utils"
import {
MedusaContext,
MedusaContextType,
MedusaModuleType,
createMedusaContainer,
} from "@medusajs/utils"
import { asValue } from "awilix"
import {
DistributedTransaction,
@@ -28,6 +33,7 @@ export class LocalWorkflow {
protected customOptions: Partial<TransactionModelOptions> = {}
protected workflow: WorkflowDefinition
protected handlers: Map<string, StepHandler>
protected medusaContext?: Context
constructor(
workflowId: string,
@@ -47,9 +53,9 @@ export class LocalWorkflow {
if (!Array.isArray(modulesLoaded) && modulesLoaded) {
if (!("cradle" in modulesLoaded)) {
container = createContainerLike(modulesLoaded)
container = createMedusaContainer(modulesLoaded)
} else {
container = modulesLoaded
container = createMedusaContainer({}, modulesLoaded) // copy container
}
} else if (Array.isArray(modulesLoaded) && modulesLoaded.length) {
container = createMedusaContainer()
@@ -60,7 +66,49 @@ export class LocalWorkflow {
}
}
this.container = container
this.container = this.contextualizedMedusaModules(container)
}
private contextualizedMedusaModules(container) {
if (!container) {
return createMedusaContainer()
}
// eslint-disable-next-line
const this_ = this
const originalResolver = container.resolve
container.resolve = function (registrationName, opts) {
const resolved = originalResolver(registrationName, opts)
if (resolved?.constructor?.__type !== MedusaModuleType) {
return resolved
}
return new Proxy(resolved, {
get: function (target, prop) {
if (typeof target[prop] !== "function") {
return target[prop]
}
return async (...args) => {
const ctxIndex =
MedusaContext.getIndex(target, prop as string) ?? args.length - 1
const hasContext = args[ctxIndex]?.__type === MedusaContextType
if (!hasContext) {
const context = this_.medusaContext
if (context?.__type === MedusaContextType) {
delete context?.manager
delete context?.transactionManager
args[ctxIndex] = context
}
}
return await target[prop].apply(target, [...args])
}
},
})
}
return container
}
protected commit() {
@@ -265,7 +313,7 @@ export class LocalWorkflow {
if (this.flow.hasChanges) {
this.commit()
}
this.medusaContext = context
const { handler, orchestrator } = this.workflow
const transaction = await orchestrator.beginTransaction(
@@ -288,6 +336,7 @@ export class LocalWorkflow {
}
async getRunningTransaction(uniqueTransactionId: string, context?: Context) {
this.medusaContext = context
const { handler, orchestrator } = this.workflow
const transaction = await orchestrator.retrieveExistingTransaction(
@@ -303,6 +352,7 @@ export class LocalWorkflow {
context?: Context,
subscribe?: DistributedTransactionEvents
) {
this.medusaContext = context
const { orchestrator } = this.workflow
const transaction = await this.getRunningTransaction(
@@ -329,6 +379,7 @@ export class LocalWorkflow {
context?: Context,
subscribe?: DistributedTransactionEvents
): Promise<DistributedTransaction> {
this.medusaContext = context
const { handler, orchestrator } = this.workflow
const { cleanUpEventListeners } = this.registerEventCallbacks({
@@ -355,6 +406,7 @@ export class LocalWorkflow {
context?: Context,
subscribe?: DistributedTransactionEvents
): Promise<DistributedTransaction> {
this.medusaContext = context
const { handler, orchestrator } = this.workflow
const { cleanUpEventListeners } = this.registerEventCallbacks({

View File

@@ -81,7 +81,7 @@ export class WorkflowManager {
const finalFlow = flow instanceof OrchestratorBuilder ? flow.build() : flow
if (WorkflowManager.workflows.has(workflowId)) {
function excludeStepUuid(key, value) {
const excludeStepUuid = (key, value) => {
return key === "uuid" ? undefined : value
}