medusajs / medusa

The world's most flexible commerce platform.
https://medusajs.com
MIT License
25.9k stars 2.59k forks source link

Typescript cannot infer type for createWorkflow #6084

Closed stephanboersma closed 10 months ago

stephanboersma commented 10 months ago

Bug report

Describe the bug

Hi Team,

Playing around with the newly announced workflow-sdk to integrate medusa with Shipmondo but I am encountering a Typescript issue and I am not able to determine where or if I screwed up.

The inferred type of 'createShipmondoSalesOrder' cannot be named without a reference to '@medusajs/medusa/node_modules/@medusajs/orchestration'. This is likely not portable. A type annotation is necessary.ts(2742)
The inferred type of 'createShipmondoSalesOrder' cannot be named without a reference to '@medusajs/medusa/node_modules/@medusajs/types'. This is likely not portable. A type annotation is necessary.ts(2742)

System information

node: 18.17 database: postgres latest

{
  "resolutions": {
      "@medusajs/utils": "^1.11.2"
    },
    "dependencies": {
      "@medusajs/admin": "7.1.10",
      "@medusajs/cache-inmemory": "^1.8.9",
      "@medusajs/cache-redis": "^1.8.9",
      "@medusajs/event-bus-local": "^1.9.7",
      "@medusajs/event-bus-redis": "^1.8.10",
      "@medusajs/file-local": "^1.0.2",
      "@medusajs/medusa": "1.20.0",
      "@medusajs/utils": "^1.11.2",
      "@medusajs/workflows-sdk": "^0.1.1",
      "@tanstack/react-query": "4.22.0",
      "body-parser": "^1.19.0",
      "cors": "^2.8.5",
      "dotenv": "16.0.3",
      "express": "^4.17.2",
      "medusa-fulfillment-manual": "^1.1.38",
      "medusa-interfaces": "^1.3.8",
      "medusa-payment-manual": "^1.0.24",
      "medusa-payment-stripe": "^6.0.7",
      "medusa-plugin-meilisearch": "^2.0.10",
      "prism-react-renderer": "^2.0.4",
      "typeorm": "^0.3.16"
    },
    "devDependencies": {
      "@babel/cli": "^7.14.3",
      "@babel/core": "^7.14.3",
      "@babel/preset-typescript": "^7.21.4",
      "@medusajs/medusa-cli": "^1.3.21",
      "@types/express": "^4.17.13",
      "@types/jest": "^27.4.0",
      "@types/node": "^17.0.8",
      "@typescript-eslint/eslint-plugin": "^6.13.1",
      "babel-preset-medusa-package": "^1.1.19",
      "cross-env": "^7.0.3",
      "eslint": "^8.54.0",
      "eslint-plugin-prettier": "^5.0.1",
      "jest": "^27.3.1",
      "prettier": "^3.1.0",
      "rimraf": "^3.0.2",
      "ts-jest": "^27.0.7",
      "ts-loader": "^9.2.6",
      "typescript": "^4.5.2"
    },
}

tsconfig

{
  "compilerOptions": {
    "target": "es2019",
    "allowJs": true,
    "esModuleInterop": true,
    "module": "commonjs",
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "skipLibCheck": true,
    "skipDefaultLibCheck": true,
    "declaration": true,
    "sourceMap": false,
    "outDir": "./dist",
    "rootDir": "./src",
    "baseUrl": ".",
    "jsx": "react-jsx",
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "checkJs": false
  },
  "include": [
    "src/"
  ],
  "exclude": [
    "**/__tests__",
    "**/__fixtures__",
    "node_modules",
    "build",
    ".cache"
  ],
}

The code

index.ts

import {
    createWorkflow,
} from "@medusajs/workflows-sdk"
import { prepareSalesOrder } from "./prepare-sales-order";
import { syncSalesOrder } from "./sync-sales-order";
import { SalesOrder } from "../../services/sdk-generated";

type WorkflowInput = {
    orderId: string;
}

export const createShipmondoSalesOrder = createWorkflow<WorkflowInput, SalesOrder>(
    "create-shipmondo-sales-order",
    function (input) {
        const { preparedSalesOrder } = prepareSalesOrder(input);

        const { salesOrder } = syncSalesOrder({ preparedSalesOrder });

        return salesOrder;
    },
)

prepare-sales-order.ts

import { Logger, Order, OrderService } from "@medusajs/medusa";
import {
    createStep,
    StepResponse,
} from "@medusajs/workflows-sdk"
import { CreateSalesOrderRequest } from "../../services/sdk-generated";
import { compileSalesOrderRequest } from "../../api/shipmondo/utils";

type StepInput = {
    orderId: string;
};

type StepOutput = {
    preparedSalesOrder: CreateSalesOrderRequest;
};

export const prepareSalesOrder = createStep<
    StepInput, StepOutput, string>("prepare-shipmondo-sales-order", async (input, context) => {
        const { orderId } = input;

        const orderService: OrderService = context.container.resolve("orderService");
        const order: Order = await orderService.retrieveWithTotals(orderId, {
            relations: [
                "items",
                "billing_address",
                "discounts",
                "gift_card_transactions",
                "gift_cards",
                "shipping_address",
                "shipping_methods",
                "shipping_methods.shipping_option",
            ],
        });

        const preparedSalesOrder = compileSalesOrderRequest(order);
        return new StepResponse({ preparedSalesOrder });
    }, async (error, context) => {
        const logger: Logger = context.container.resolve("logger");
        logger.error("Failed to prepare sales order", error);
    })

sync-sales-order.ts

import { Logger } from "@medusajs/medusa";
import {
    createStep,
    StepResponse,
} from "@medusajs/workflows-sdk"
import { CreateSalesOrderRequest, SalesOrder } from "src/services/sdk-generated";

type StepInput = {
    preparedSalesOrder: CreateSalesOrderRequest;
};

type StepOutput = {
    salesOrder: SalesOrder;
};

export const syncSalesOrder = createStep<
    StepInput, StepOutput, string>("sync-shipmondo-sales-order", async (input, context) => {
        const shipmondoClient = context.container.resolve("shipmondoService");

        const { preparedSalesOrder } = input;

        const salesOrder = await shipmondoClient.createSalesOrder(preparedSalesOrder);
        return new StepResponse({ salesOrder });
    }, async (error, context) => {
        const logger: Logger = context.container.resolve("logger");
        logger.error("Failed to sync sales order", error);
    })

I am open to share code regarding shipmondo fulfillment when I am further in the process and if you are interested in sharing it as a community package.

olivermrbl commented 10 months ago

Unfortunately, I was unable to reproduce your issue with the information provided.

Can I get you to ensure all your Medusa packages are running the latest version? The issue is likely due to a versioning mismatch between two of our packages.

Also, happy to further debug, if you can provide a repository with a consistent reproduction.

stephanboersma commented 10 months ago

Hi again @olivermrbl ,

Thank you for the quick response!

After some dependency hell, I managed to get rid of the type error. Although, I am still not able to run the workflow according to the example of the createWorkflow documentation.

The logs give this feedback when attempting to run the workflow in the subscriber.

info:    Processing cart.updated which has 0 subscribers
info:    Processing product-variant.updated which has 1 subscribers
info:    Processing payment.updated which has 0 subscribers
info:    Processing order.placed which has 3 subscribers
warn:    An error occurred while processing order.placed: TypeError: create_shipmondo_order_1.createShipmondoSalesOrder.run is not a function
info:    Processing cart.updated which has 0 subscribers
info:    Processing cart.created which has 0 subscribers
import {
    type SubscriberConfig,
    type SubscriberArgs,
    OrderService,
    Logger,
} from "@medusajs/medusa"
import { createShipmondoSalesOrder } from "../workflows/create-shipmondo-order";

export default async function orderPlacedHandler({
    data, eventName, container, pluginOptions,
}: SubscriberArgs<Record<string, any>>) {
    const logger: Logger = container.resolve("logger");
    const { result } = await createShipmondoSalesOrder.run({ input: { orderId: data.id } })

    logger.info("Created Shipmondo sales order: " + result.id);

}

export const config: SubscriberConfig = {
    event: OrderService.Events.PLACED,
    context: {
        subscriberId: "order-placed-shipmondo",
    },
}

I made my repo public - if you have time, please have a look: https://github.com/stephanboersma/metalvinyl/tree/feat/shipmondo.

The workflow is on the feat/shipmondo branch located in apps/backend/src/workflows and I am attempting to run it in a subscriber at apps/backend/src/subscribers/create-shipmondo-order.ts.

See apps/backend/docker-compose.yml as reference for my development environment.

olivermrbl commented 10 months ago

I believe you are missing the workflow initialization step. Can I get you to try the follow:

const { result } = await createShipmondoSalesOrder(container).run({ input: { orderId: data.id } })

Here, our dependency container, container, is passed upon initializing the workflow, so the workflow know what dependencies it can work with. We are looking into abstracting this step away, as we should be able to do it automagically in the average use case (and because it is evidently error-prone).

Let me know how it goes.

stephanboersma commented 10 months ago

Oh god, you are right. The workflow runs and executes the steps correctly. I see that last step actually manages to communicate with third party, however a new typeerror is thrown during the workflow execution.

warn: An error occurred while processing order.placed: TypeError: object is not a function

I was able to console.log just before returning the final StepResult.

export const syncSalesOrder = createStep<
    StepInput, StepOutput, string>("sync-shipmondo-sales-order", async (input) => {
        const shipmondoClient = new ShipmondoClient({
            USERNAME: process.env.SHIPMONDO_USER,
            PASSWORD: process.env.SHIPMONDO_KEY,
        })

        const { preparedSalesOrder } = input;

        const salesOrder = await shipmondoClient.salesOrders.salesOrdersPost(preparedSalesOrder);
        console.log("this works: " + salesOrder.id);
        return new StepResponse({ salesOrder });
    }, async (error, context) => {
        const logger: Logger = context.container.resolve("logger");
        logger.error("Failed to sync sales order", error);
    })
olivermrbl commented 10 months ago

@adrien2p, @carlos-r-l-rodrigues – one of you know what might be wrong here?

stephanboersma commented 10 months ago

@carlos-r-l-rodrigues, the exception is thrown within my event handler/subscriber:

image
import {
    type SubscriberConfig,
    type SubscriberArgs,
    OrderService,
    Logger,
} from "@medusajs/medusa"
import { createShipmondoSalesOrder } from "../workflows/create-shipmondo-order";

export default async function orderPlacedHandler({
    data, eventName, container, pluginOptions,
}: SubscriberArgs<Record<string, any>>) {
    const logger: Logger = container.resolve("logger");
    const { result } = await createShipmondoSalesOrder(container).run({ input: { orderId: data.id } }); // exception thrown here
    logger.info(`Created Shipmondo order ${result.id} for Medusa order ${result.order_id}`); // this never runs
}

export const config: SubscriberConfig = {
    event: OrderService.Events.PLACED,
    context: {
        subscriberId: "order-placed-shipmondo",
    },
}
carlos-r-l-rodrigues commented 10 months ago

I manage to reproduce the issue. Returning a destructured reference is not working. To unblock you while we work in a fix for that, I suggest that in the definition of your workflow, you return the last step and handle the property salesOrder on your side.

function (input) {
    const { preparedSalesOrder } = prepareSalesOrder(input);

    return syncSalesOrder({ preparedSalesOrder });
},
stephanboersma commented 10 months ago

It works, thanks a lot! 🙏

adrien2p commented 10 months ago

Also just to add some information, no need to use the templates for typing, the templates are there to grab and infer all the types based on what you will type the input of the step and what will be returned.

E.g in my playground https://github.com/medusajs/medusa/blob/develop/packages/workflows-sdk/src/utils/_playground.ts

adrien2p commented 10 months ago

Another note, in a step, the second function is the compensation step, the first argument is mot the error, but it is what is passed as the second argument of the step response. For example you create something, you would like to to have the id of the object that have been created to be reverted in the compensation function.

Also, dont forget that the compensation function is called as soon as the step itself fail or a later step. This is to allow full compensation of a workflow