diff --git a/.changeset/afraid-otters-train.md b/.changeset/afraid-otters-train.md new file mode 100644 index 0000000000000..26a15ed78971b --- /dev/null +++ b/.changeset/afraid-otters-train.md @@ -0,0 +1,7 @@ +--- +"@medusajs/orchestration": patch +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +feat: Remote Joiner diff --git a/.changeset/fair-kids-tease.md b/.changeset/fair-kids-tease.md new file mode 100644 index 0000000000000..b28eb529279c3 --- /dev/null +++ b/.changeset/fair-kids-tease.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): Improve error messaging in plugin loader diff --git a/.changeset/gentle-cups-crash.md b/.changeset/gentle-cups-crash.md new file mode 100644 index 0000000000000..7cf5a825df285 --- /dev/null +++ b/.changeset/gentle-cups-crash.md @@ -0,0 +1,5 @@ +--- +"gatsby-source-medusa": patch +--- + +chore(gatsby-source-medusa): Cleanup plugin setup diff --git a/.changeset/gentle-teachers-greet.md b/.changeset/gentle-teachers-greet.md new file mode 100644 index 0000000000000..0fe506d5f8e71 --- /dev/null +++ b/.changeset/gentle-teachers-greet.md @@ -0,0 +1,6 @@ +--- +"@medusajs/medusa-js": patch +"medusa-react": patch +--- + +feat(medusa-react,medusa-js): Allow custom headers diff --git a/.changeset/great-panthers-call.md b/.changeset/great-panthers-call.md new file mode 100644 index 0000000000000..66ab384b1f638 --- /dev/null +++ b/.changeset/great-panthers-call.md @@ -0,0 +1,12 @@ +--- +"@medusajs/medusa": patch +"@medusajs/admin-ui": patch +"@medusajs/admin": patch +"medusa-plugin-brightpearl": patch +"medusa-plugin-segment": patch +"medusa-plugin-sendgrid": patch +"medusa-plugin-slack-notification": patch +"@medusajs/utils": patch +--- + +fix(medusa, utils): fix the way selects are consumed alongside the relations diff --git a/.changeset/late-dragons-collect.md b/.changeset/late-dragons-collect.md new file mode 100644 index 0000000000000..8ae2382de99ee --- /dev/null +++ b/.changeset/late-dragons-collect.md @@ -0,0 +1,12 @@ +--- +"@medusajs/medusa": patch +"@medusajs/event-bus-local": patch +"@medusajs/event-bus-redis": patch +"@medusajs/medusa-cli": patch +"medusa-plugin-algolia": patch +"medusa-plugin-meilisearch": patch +"@medusajs/product": patch +"@medusajs/types": patch +--- + +chore(medusa-cli): Cleanup plugin setup + include Logger type update which is used across multiple packages diff --git a/.changeset/late-insects-punch.md b/.changeset/late-insects-punch.md new file mode 100644 index 0000000000000..5ec64f79598e2 --- /dev/null +++ b/.changeset/late-insects-punch.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +fix(medusa): model loader with customizations diff --git a/.changeset/proud-ghosts-speak.md b/.changeset/proud-ghosts-speak.md new file mode 100644 index 0000000000000..6ee2404152d8e --- /dev/null +++ b/.changeset/proud-ghosts-speak.md @@ -0,0 +1,6 @@ +--- +"@medusajs/medusa": patch +"@medusajs/utils": patch +--- + +feat(medusa, utils): improve devx for core entity customizations diff --git a/.changeset/quiet-gifts-kick.md b/.changeset/quiet-gifts-kick.md new file mode 100644 index 0000000000000..acb00e07f2b1e --- /dev/null +++ b/.changeset/quiet-gifts-kick.md @@ -0,0 +1,5 @@ +--- +"medusa-react": patch +--- + +fix(medusa-react): fix wrong admin reservations query key diff --git a/.changeset/twelve-mugs-arrive.md b/.changeset/twelve-mugs-arrive.md new file mode 100644 index 0000000000000..bf4a81db2708c --- /dev/null +++ b/.changeset/twelve-mugs-arrive.md @@ -0,0 +1,5 @@ +--- +"medusa-dev-cli": patch +--- + +chore(medusa-dev-cli): Cleanup plugin setup diff --git a/.github/actions/setup-server/action.yml b/.github/actions/setup-server/action.yml index f18362adb01f4..6a80f2ce9f148 100644 --- a/.github/actions/setup-server/action.yml +++ b/.github/actions/setup-server/action.yml @@ -5,7 +5,7 @@ inputs: node-version: description: "Node version" required: false - default: "14" + default: "16" cache-extension: description: "Extension for fetching cached dependencies" required: true diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 4e5be83357e61..0498d17ee576c 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -17,7 +17,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v2.4.1 with: - node-version: "14" + node-version: "16" cache: "yarn" - name: Assert changed @@ -48,7 +48,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v2.4.1 with: - node-version: "14" + node-version: "16" cache: "yarn" - name: Assert changed @@ -109,7 +109,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v2.4.1 with: - node-version: "14" + node-version: "16" cache: "yarn" - name: Install dependencies @@ -161,7 +161,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v2.4.1 with: - node-version: "14" + node-version: "16" cache: "yarn" - name: Install dependencies @@ -212,7 +212,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v2.4.1 with: - node-version: "14" + node-version: "16" cache: "yarn" - name: Install dependencies diff --git a/.github/workflows/codegen-test.yml b/.github/workflows/codegen-test.yml index 3de782f160e92..8dc98b6f35e51 100644 --- a/.github/workflows/codegen-test.yml +++ b/.github/workflows/codegen-test.yml @@ -25,7 +25,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v2.4.1 with: - node-version: "14" + node-version: "16" cache: "yarn" - name: Install dependencies diff --git a/.github/workflows/docs-freshness-check.yml b/.github/workflows/docs-freshness-check.yml index 09d5cd9beb0b6..e39b8ea4fcfdb 100644 --- a/.github/workflows/docs-freshness-check.yml +++ b/.github/workflows/docs-freshness-check.yml @@ -24,7 +24,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v2.4.1 with: - node-version: "14" + node-version: "16" cache: "yarn" - name: Install dependencies diff --git a/.github/workflows/docs-new-announcement.yml b/.github/workflows/docs-new-announcement.yml index b09047f74f44f..1d2a7b4c447d6 100644 --- a/.github/workflows/docs-new-announcement.yml +++ b/.github/workflows/docs-new-announcement.yml @@ -22,7 +22,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v2.4.1 with: - node-version: "14" + node-version: "16" cache: "yarn" - name: Install dependencies diff --git a/.github/workflows/docs-remove-announcement.yml b/.github/workflows/docs-remove-announcement.yml index 42f9b11719b00..1ae204ac5df2c 100644 --- a/.github/workflows/docs-remove-announcement.yml +++ b/.github/workflows/docs-remove-announcement.yml @@ -23,7 +23,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v2.4.1 with: - node-version: "14" + node-version: "16" cache: "yarn" - name: Install dependencies diff --git a/.github/workflows/generate-api-reference.yml b/.github/workflows/generate-api-reference.yml index 2892f4af8e1cc..d4b63ed5740e9 100644 --- a/.github/workflows/generate-api-reference.yml +++ b/.github/workflows/generate-api-reference.yml @@ -22,7 +22,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v2.4.1 with: - node-version: "14" + node-version: "16" cache: "yarn" - name: Install dependencies diff --git a/.github/workflows/generate-entity-reference.yml b/.github/workflows/generate-entity-reference.yml index 5fe63535463fb..4660110e385a2 100644 --- a/.github/workflows/generate-entity-reference.yml +++ b/.github/workflows/generate-entity-reference.yml @@ -21,7 +21,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v2.4.1 with: - node-version: "14" + node-version: "16" cache: "yarn" - name: Install dependencies diff --git a/.github/workflows/generate-js-reference.yml b/.github/workflows/generate-js-reference.yml index 939ff2faa930d..e6e379521bcf7 100644 --- a/.github/workflows/generate-js-reference.yml +++ b/.github/workflows/generate-js-reference.yml @@ -21,7 +21,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v2.4.1 with: - node-version: "14" + node-version: "16" cache: "yarn" - name: Install dependencies diff --git a/.github/workflows/generate-reference.yml b/.github/workflows/generate-reference.yml index a63c036db57c4..b7c91827d5d96 100644 --- a/.github/workflows/generate-reference.yml +++ b/.github/workflows/generate-reference.yml @@ -21,7 +21,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v2.4.1 with: - node-version: "14" + node-version: "16" cache: "yarn" - name: Install dependencies diff --git a/.github/workflows/oas-test.yml b/.github/workflows/oas-test.yml index 820d812b0fcc6..d01d436665e04 100644 --- a/.github/workflows/oas-test.yml +++ b/.github/workflows/oas-test.yml @@ -22,7 +22,7 @@ jobs: - name: Setup Node.js environment uses: actions/setup-node@v2.4.1 with: - node-version: "14" + node-version: "16" cache: "yarn" - name: Install dependencies diff --git a/integration-tests/api/__tests__/batch-jobs/order/export.js b/integration-tests/api/__tests__/batch-jobs/order/export.js index c52282ba71967..63be7859f6a68 100644 --- a/integration-tests/api/__tests__/batch-jobs/order/export.js +++ b/integration-tests/api/__tests__/batch-jobs/order/export.js @@ -109,8 +109,6 @@ describe("Batchjob with type order-export", () => { expect(batchJob.status).toBe("completed") - expect(batchJob.status).toBe("completed") - exportFilePath = path.resolve(__dirname, batchJob.result.file_key) const isFileExists = (await fs.stat(exportFilePath)).isFile() diff --git a/integration-tests/api/__tests__/batch-jobs/product/export.js b/integration-tests/api/__tests__/batch-jobs/product/export.js index defbed9b1f1b6..69255c34d2de1 100644 --- a/integration-tests/api/__tests__/batch-jobs/product/export.js +++ b/integration-tests/api/__tests__/batch-jobs/product/export.js @@ -60,6 +60,7 @@ describe("Batch job of product-export type", () => { const db = useDb() await db.teardown() + // @ts-ignore try { const isFileExists = (await fs.stat(exportFilePath))?.isFile() @@ -74,8 +75,8 @@ describe("Batch job of product-export type", () => { await fs.unlink(exportFilePath) } - } catch (e) { - console.log(e) + } catch (err) { + // noop } }) diff --git a/integration-tests/api/__tests__/database/index.js b/integration-tests/api/__tests__/database/index.js index 7a86d6f6b701f..27d3ae4b19229 100644 --- a/integration-tests/api/__tests__/database/index.js +++ b/integration-tests/api/__tests__/database/index.js @@ -40,7 +40,7 @@ describe("Database options", () => { // Idle time is 1000 ms so this should timeout await new Promise((resolve) => - setTimeout(() => resolve(console.log("")), 2000) + setTimeout(() => resolve(undefined), 2000) ) // This query should fail with a QueryRunnerAlreadyReleasedError diff --git a/integration-tests/api/__tests__/store/customer.js b/integration-tests/api/__tests__/store/customer.js index 65a5fe2938d86..d93e8284915f7 100644 --- a/integration-tests/api/__tests__/store/customer.js +++ b/integration-tests/api/__tests__/store/customer.js @@ -293,16 +293,19 @@ describe("/store/customers", () => { }) expect(response.status).toEqual(200) - expect(response.data.orders).toEqual([ - expect.objectContaining({ - display_id: 3, - status: "canceled", - }), - expect.objectContaining({ - display_id: 1, - status: "completed", - }), - ]) + expect(response.data.orders.length).toEqual(2) + expect(response.data.orders).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + display_id: 3, + status: "canceled", + }), + expect.objectContaining({ + display_id: 1, + status: "completed", + }), + ]) + ) expect(response.data.orders.length).toEqual(2) }) }) diff --git a/integration-tests/api/__tests__/store/products.js b/integration-tests/api/__tests__/store/products.js index 3d6583791a500..bc765f1ce5476 100644 --- a/integration-tests/api/__tests__/store/products.js +++ b/integration-tests/api/__tests__/store/products.js @@ -10,6 +10,10 @@ const { const productSeeder = require("../../helpers/store-product-seeder") const adminSeeder = require("../../helpers/admin-seeder") +const { + allowedStoreProductsFields, + defaultStoreProductsRelations, +} = require("@medusajs/medusa/dist") jest.setTimeout(30000) @@ -988,23 +992,28 @@ describe("/store/products", () => { it("response contains only fields defined with `fields` param", async () => { const api = useApi() + const fields = allowedStoreProductsFields + const response = await api.get( - "/store/products/test-product?fields=handle" + `/store/products/test-product?fields=${fields.join(",")}` ) expect(response.status).toEqual(200) - expect(Object.keys(response.data.product)).toEqual([ - // fields - "handle", - // relations - "variants", - "options", - "images", - "tags", - "collection", - "type", - ]) + const expectedProperties = [...fields, ...defaultStoreProductsRelations] + const actualProperties = [ + ...Object.keys(response.data.product), + ...Object.keys(response.data.product.variants[0]).map( + (key) => `variants.${key}` + ), + "variants.prices.amount", + "options.values", + ] + + expect(Object.keys(response.data.product).length).toEqual(31) + expect(actualProperties).toEqual( + expect.arrayContaining(expectedProperties) + ) }) }) }) diff --git a/integration-tests/plugins/__tests__/inventory/order/order.js b/integration-tests/plugins/__tests__/inventory/order/order.js index aff714af85704..1d675ceacb6a8 100644 --- a/integration-tests/plugins/__tests__/inventory/order/order.js +++ b/integration-tests/plugins/__tests__/inventory/order/order.js @@ -5,7 +5,6 @@ const { initDb, useDb } = require("../../../../helpers/use-db") const { setPort, useApi } = require("../../../../helpers/use-api") const adminSeeder = require("../../../helpers/admin-seeder") -const cartSeeder = require("../../../helpers/cart-seeder") const { simpleProductFactory, simpleCustomerFactory, diff --git a/packages/admin-ui/ui/src/domain/orders/details/utils/use-admin-expand-paramter.ts b/packages/admin-ui/ui/src/domain/orders/details/utils/use-admin-expand-paramter.ts index 12af7c0bafef5..b39bbc4a3ec31 100644 --- a/packages/admin-ui/ui/src/domain/orders/details/utils/use-admin-expand-paramter.ts +++ b/packages/admin-ui/ui/src/domain/orders/details/utils/use-admin-expand-paramter.ts @@ -7,6 +7,7 @@ const orderRelations = [ "discounts", "discounts.rule", "shipping_methods", + "shipping_methods.shipping_option", "payments", "items", "fulfillments", diff --git a/packages/event-bus-local/src/services/event-bus-local.ts b/packages/event-bus-local/src/services/event-bus-local.ts index 2acd56f7f5ada..e45b3a57a2533 100644 --- a/packages/event-bus-local/src/services/event-bus-local.ts +++ b/packages/event-bus-local/src/services/event-bus-local.ts @@ -1,5 +1,5 @@ -import { Logger, MedusaContainer } from "@medusajs/modules-sdk" -import { EmitData, EventBusTypes, Subscriber } from "@medusajs/types" +import { MedusaContainer } from "@medusajs/modules-sdk" +import { EmitData, EventBusTypes, Logger, Subscriber } from "@medusajs/types" import { AbstractEventBusModuleService } from "@medusajs/utils" import { EventEmitter } from "events" import { ulid } from "ulid" diff --git a/packages/event-bus-redis/src/services/event-bus-redis.ts b/packages/event-bus-redis/src/services/event-bus-redis.ts index ba318988d073b..52712379476b9 100644 --- a/packages/event-bus-redis/src/services/event-bus-redis.ts +++ b/packages/event-bus-redis/src/services/event-bus-redis.ts @@ -1,5 +1,5 @@ -import { InternalModuleDeclaration, Logger } from "@medusajs/modules-sdk" -import { EmitData } from "@medusajs/types" +import { InternalModuleDeclaration } from "@medusajs/modules-sdk" +import { EmitData, Logger } from "@medusajs/types" import { AbstractEventBusModuleService } from "@medusajs/utils" import { BulkJobOptions, JobsOptions, Queue, Worker } from "bullmq" import { Redis } from "ioredis" diff --git a/packages/gatsby-source-medusa/.babelrc b/packages/gatsby-source-medusa/.babelrc deleted file mode 100644 index 5681962c15540..0000000000000 --- a/packages/gatsby-source-medusa/.babelrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "presets": [["babel-preset-gatsby-package"]], - "overrides": [ - { - "test": [], - "presets": [ - ["babel-preset-gatsby-package", { "browser": true, "esm": true }] - ] - } - ] -} diff --git a/packages/gatsby-source-medusa/.gitignore b/packages/gatsby-source-medusa/.gitignore index f930d4acd815d..874c6c69d3341 100644 --- a/packages/gatsby-source-medusa/.gitignore +++ b/packages/gatsby-source-medusa/.gitignore @@ -1,13 +1,6 @@ +/dist node_modules - -.DS_Store - -utils - - -*.js -*.js.map -*.d.ts -!/types/*.d.ts - -!jest.config.js \ No newline at end of file +.DS_store +.env* +.env +*.sql diff --git a/packages/gatsby-source-medusa/.npmignore b/packages/gatsby-source-medusa/.npmignore deleted file mode 100644 index 5645e7cbc9b16..0000000000000 --- a/packages/gatsby-source-medusa/.npmignore +++ /dev/null @@ -1,9 +0,0 @@ -src -.prettierrc -.env -.babelrc.js -.eslintrc -.gitignore - -.yarn -.turbo \ No newline at end of file diff --git a/packages/gatsby-source-medusa/jest.config.js b/packages/gatsby-source-medusa/jest.config.js new file mode 100644 index 0000000000000..e564d67c7053e --- /dev/null +++ b/packages/gatsby-source-medusa/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + globals: { + "ts-jest": { + tsconfig: "tsconfig.spec.json", + isolatedModules: false, + }, + }, + transform: { + "^.+\\.[jt]s?$": "ts-jest", + }, + testEnvironment: `node`, + moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`], +} diff --git a/packages/gatsby-source-medusa/package.json b/packages/gatsby-source-medusa/package.json index 01f3c52f11faa..8036ecfed5652 100644 --- a/packages/gatsby-source-medusa/package.json +++ b/packages/gatsby-source-medusa/package.json @@ -4,9 +4,12 @@ "description": "Gatsby source plugin for building websites using Medusa Commerce as a data source", "scripts": { "test": "jest --passWithNoTests", - "watch": "tsc-watch --outDir .", - "build": "tsc --outDir ." + "watch": "tsc-watch", + "build": "tsc" }, + "files": [ + "dist" + ], "keywords": [ "gatsby", "gatsby-plugin", @@ -27,16 +30,15 @@ }, "dependencies": { "axios": "^0.24.0", - "babel-preset-gatsby-package": "^2.0.0", "gatsby-core-utils": "^3.3.0", "gatsby-plugin-image": "^2.3.0", "gatsby-source-filesystem": "^4.3.0" }, "devDependencies": { "@types/jest": "^27.0.1", - "babel-plugin-polyfill-corejs2": "^0.3.0", "gatsby": "^4.1.0", "jest": "^27.0.6", + "ts-jest": "^27.1.5", "tsc-watch": "^4.5.0", "typescript": "^4.5.2" }, diff --git a/packages/gatsby-source-medusa/__tests__/__snapshots__/process-node.test.ts.snap b/packages/gatsby-source-medusa/src/__tests__/__snapshots__/process-node.test.ts.snap similarity index 100% rename from packages/gatsby-source-medusa/__tests__/__snapshots__/process-node.test.ts.snap rename to packages/gatsby-source-medusa/src/__tests__/__snapshots__/process-node.test.ts.snap diff --git a/packages/gatsby-source-medusa/__tests__/process-node.test.ts b/packages/gatsby-source-medusa/src/__tests__/process-node.test.ts similarity index 51% rename from packages/gatsby-source-medusa/__tests__/process-node.test.ts rename to packages/gatsby-source-medusa/src/__tests__/process-node.test.ts index 583abe21f2500..d21b6b47bddca 100644 --- a/packages/gatsby-source-medusa/__tests__/process-node.test.ts +++ b/packages/gatsby-source-medusa/src/__tests__/process-node.test.ts @@ -1,20 +1,20 @@ -const { processNode } = require("../src/process-node.ts"); +const { processNode } = require("../process-node.ts") describe("helper functions", () => { it("should return return a new node from processNode", () => { - const fieldName = "product"; + const fieldName = "product" const node = { id: "prod_test_1234", title: "Test Shirt", description: "A test product", unit_price: 2500, - }; + } - const createContentDigest = jest.fn(() => "digest_string"); - const processNodeResult = processNode(node, fieldName, createContentDigest); + const createContentDigest = jest.fn(() => "digest_string") + const processNodeResult = processNode(node, fieldName, createContentDigest) - expect(createContentDigest).toBeCalled(); + expect(createContentDigest).toBeCalled() - expect(processNodeResult).toMatchSnapshot(); - }); -}); + expect(processNodeResult).toMatchSnapshot() + }) +}) diff --git a/packages/gatsby-source-medusa/types/interface.d.ts b/packages/gatsby-source-medusa/src/interface.ts similarity index 100% rename from packages/gatsby-source-medusa/types/interface.d.ts rename to packages/gatsby-source-medusa/src/interface.ts diff --git a/packages/gatsby-source-medusa/src/process-node.ts b/packages/gatsby-source-medusa/src/process-node.ts index 0bd729c95a8cf..faaa17ac4de1f 100644 --- a/packages/gatsby-source-medusa/src/process-node.ts +++ b/packages/gatsby-source-medusa/src/process-node.ts @@ -1,5 +1,3 @@ -import { capitalize } from "./utils/capitalize" - export const processNode = ( node: any, fieldName: string, @@ -34,12 +32,14 @@ export const processNode = ( delete node.images } + // TODO: use upperFirstCase from medusajs/utils when it's available + const type = `Medusa${fieldName[0].toUpperCase() + fieldName.slice(1)}` const nodeData = Object.assign({}, node, { id: nodeId, parent: null, children: [], internal: { - type: `Medusa${capitalize(fieldName)}`, + type, content: nodeContent, contentDigest: nodeContentDigest, }, diff --git a/packages/gatsby-source-medusa/src/utils/capitalize.ts b/packages/gatsby-source-medusa/src/utils/capitalize.ts deleted file mode 100644 index 61317e498fa82..0000000000000 --- a/packages/gatsby-source-medusa/src/utils/capitalize.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function capitalize(s: string): string { - return s[0].toUpperCase() + s.slice(1) -} diff --git a/packages/gatsby-source-medusa/tsconfig.json b/packages/gatsby-source-medusa/tsconfig.json index cab823f261c46..731ef8a6e0d46 100644 --- a/packages/gatsby-source-medusa/tsconfig.json +++ b/packages/gatsby-source-medusa/tsconfig.json @@ -1,19 +1,29 @@ { - "include": ["src/**/*.ts", "types"], - "exclude": ["node_modules"], "compilerOptions": { - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "module": "commonjs", - "removeComments": false, - "preserveConstEnums": true, - "skipLibCheck": true, + "lib": ["es2020"], + "target": "es2020", + "outDir": "./dist", "esModuleInterop": true, - "sourceMap": true, - "target": "es2017", "declaration": true, - "lib": ["es2017", "dom", "esnext.asynciterable"] - } + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true, + "downlevelIteration": true // to use ES5 specific tooling + }, + "include": ["src"], + "exclude": [ + "dist", + "./src/**/__tests__", + "./src/**/__mocks__", + "./src/**/__fixtures__", + "node_modules" + ] } diff --git a/packages/gatsby-source-medusa/tsconfig.spec.json b/packages/gatsby-source-medusa/tsconfig.spec.json new file mode 100644 index 0000000000000..9b6240919113c --- /dev/null +++ b/packages/gatsby-source-medusa/tsconfig.spec.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/packages/medusa-cli/.babelrc b/packages/medusa-cli/.babelrc deleted file mode 100644 index b48db12268b7f..0000000000000 --- a/packages/medusa-cli/.babelrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "plugins": ["@babel/plugin-proposal-class-properties"], - "presets": ["@babel/preset-env"], - "env": { - "test": { - "plugins": ["@babel/plugin-transform-runtime"] - } - } -} diff --git a/packages/medusa-cli/jest.config.js b/packages/medusa-cli/jest.config.js new file mode 100644 index 0000000000000..14cd7ed6e7aca --- /dev/null +++ b/packages/medusa-cli/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + globals: { + "ts-jest": { + tsConfig: "tsconfig.spec.json", + isolatedModules: false, + }, + }, + transform: { + "^.+\\.[jt]s?$": "ts-jest", + }, + testEnvironment: `node`, + moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`], +} diff --git a/packages/medusa-cli/package.json b/packages/medusa-cli/package.json index 164f00cd0b430..04dec2b71bf8d 100644 --- a/packages/medusa-cli/package.json +++ b/packages/medusa-cli/package.json @@ -20,20 +20,18 @@ ], "scripts": { "test": "jest --passWithNoTests src", - "build": "babel src --out-dir dist/ --ignore '**/__tests__','**/__mocks__'", - "watch": "babel -w src --out-dir dist/ --ignore '**/__tests__','**/__mocks__'", + "build": "tsc", + "watch": "tsc --watch", "prepare": "cross-env NODE_ENV=production yarn run build" }, "author": "Sebastian Rindom", "license": "MIT", "devDependencies": { - "@babel/cli": "^7.7.5", - "@babel/core": "^7.7.5", - "@babel/plugin-proposal-class-properties": "^7.7.4", - "@babel/plugin-transform-runtime": "^7.7.6", - "@babel/preset-env": "^7.7.5", + "@types/yargs": "^15.0.15", "cross-env": "^5.2.1", - "jest": "^25.5.4" + "jest": "^25.5.4", + "ts-jest": "^25.5.1", + "typescript": "^4.9.5" }, "dependencies": { "@medusajs/utils": "^1.9.1", @@ -61,7 +59,6 @@ "semver": "^7.3.8", "stack-trace": "^0.0.10", "ulid": "^2.3.0", - "url": "^0.11.0", "winston": "^3.8.2", "yargs": "^15.3.1" }, diff --git a/packages/medusa-cli/src/commands/link.js b/packages/medusa-cli/src/commands/link.js deleted file mode 100644 index 1d45220d2ee6a..0000000000000 --- a/packages/medusa-cli/src/commands/link.js +++ /dev/null @@ -1,139 +0,0 @@ -const axios = require("axios").default -const inquirer = require("inquirer") -const open = require("open") -const execa = require("execa") -const resolveCwd = require(`resolve-cwd`) -const { track } = require("medusa-telemetry") - -const { getToken } = require("../util/token-store") -const logger = require("../reporter").default - -const MEDUSA_CLI_DEBUG = process.env.MEDUSA_CLI_DEBUG || false - -module.exports = { - link: async argv => { - track("CLI_LINK", { args: argv }) - const port = process.env.PORT || 9000 - const appHost = - process.env.MEDUSA_APP_HOST || "https://app.medusa-commerce.com" - - const apiHost = - process.env.MEDUSA_API_HOST || "https://api.medusa-commerce.com" - - // Checks if there is already a token from a previous log in; this is - // necessary to redirect the customer to the page where local linking is - // done - const tok = getToken() - if (!tok) { - console.log( - "You must login to Medusa Cloud first. Please run medusa login." - ) - process.exit(1) - } - - // Get the currently logged in user; we will be using the Cloud user id to - // create a user in the local DB with the same user id; allowing you to - // authenticate to the local API. - const { data: auth } = await axios - .get(`${apiHost}/auth`, { - headers: { - authorization: `Bearer ${tok}`, - }, - }) - .catch(err => { - console.log(err) - process.exit(1) - }) - - const linkActivity = logger.activity("Linking local project") - - // Create the user with the user id - if (!argv.skipLocalUser && auth.user) { - let proc - try { - proc = execa( - `./node_modules/@medusajs/medusa/cli.js`, - [`user`, `--id`, auth.user.id, `--email`, auth.user.email], - { - env: { - ...process.env, - NODE_ENV: "command", - }, - } - ) - - if (MEDUSA_CLI_DEBUG) { - proc.stderr.pipe(process.stderr) - proc.stdout.pipe(process.stdout) - } - - const res = await proc - if (res.stderr) { - const err = new Error("stderr error") - err.stderr = res.stderr - throw err - } - } catch (error) { - logger.failure(linkActivity, "Failed to perform local linking") - if (error.stderr) { - console.error(error.stderr) - } else if (error.code === "ENOENT") { - logger.error( - `Couldn't find the Medusa CLI - please make sure that you have installed it globally` - ) - } - process.exit(1) - } - } - - logger.success(linkActivity, "Local project linked") - track("CLI_LINK_COMPLETED") - - console.log() - console.log( - "Link Medusa Cloud to your local server. This will open the browser" - ) - console.log() - - const prompts = [ - { - type: "input", - name: "open", - message: "Press enter key to open browser for linking or n to exit", - }, - ] - - await inquirer.prompt(prompts).then(async a => { - if (a.open === "n") { - process.exit(0) - } - - const params = `lurl=http://localhost:${port}<oken=${auth.user.id}` - - // This step sets the Cloud link by opening a browser - const browserOpen = await open( - `${appHost}/local-link?${encodeURI(params)}`, - { - app: "browser", - wait: false, - } - ) - - browserOpen.on("error", err => { - console.warn(err) - console.log( - `Could not open browser go to: ${appHost}/local-link?lurl=http://localhost:9000<oken=${auth.user.id}` - ) - }) - - track("CLI_LINK_BROWSER_OPENED") - }) - - if (argv.develop) { - const proc = execa(`./node_modules/@medusajs/medusa/cli.js`, [`develop`]) - proc.stdout.pipe(process.stdout) - proc.stderr.pipe(process.stderr) - await proc - } - }, -} diff --git a/packages/medusa-cli/src/commands/login.js b/packages/medusa-cli/src/commands/login.js deleted file mode 100644 index d5890e0ecff04..0000000000000 --- a/packages/medusa-cli/src/commands/login.js +++ /dev/null @@ -1,93 +0,0 @@ -const axios = require("axios").default -const open = require("open") -const inquirer = require("inquirer") -const { track } = require("medusa-telemetry") - -const logger = require("../reporter").default -const { setToken } = require("../util/token-store") - -/** - * The login command allows the CLI to keep track of Cloud users; the command - * makes a cli-login request to the cloud server and keeps an open connection - * until the user has authenticated via the Medusa Cloud website. - */ -module.exports = { - login: async _ => { - track("CLI_LOGIN") - const apiHost = - process.env.MEDUSA_API_HOST || "https://api.medusa-commerce.com" - - const authHost = process.env.MEDUSA_AUTH_HOST || `${apiHost}/cli-auth` - - const loginHost = - process.env.MEDUSA_APP_HOST || "https://app.medusa-commerce.com" - - const { data: urls } = await axios.post(authHost) - - const loginUri = `${loginHost}${urls.browser_url}` - - const prompts = [ - { - type: "input", - name: "open", - message: "Press enter key to open browser for login or n to exit", - }, - ] - - console.log() - console.log("Login to Medusa Cloud") - console.log() - - await inquirer.prompt(prompts).then(async a => { - if (a.open === "n") { - process.exit(0) - } - - const browserOpen = await open(loginUri, { - app: "browser", - wait: false, - }) - browserOpen.on("error", err => { - console.warn(err) - console.log(`Could not open browser go to: ${loginUri}`) - }) - }) - - const spinner = logger.activity(`Waiting for login at ${loginUri}`) - const fetchAuth = async (retries = 3) => { - try { - const { data: auth } = await axios.get(`${authHost}${urls.cli_url}`, { - headers: { authorization: `Bearer ${urls.cli_token}` }, - }) - return auth - } catch (err) { - if (retries > 0 && err.http && err.http.statusCode > 500) - return fetchAuth(retries - 1) - throw err - } - } - const auth = await fetchAuth() - - // This is kept alive for several seconds until the user has authenticated - // in the browser. - const { data: user } = await axios - .get(`${apiHost}/auth`, { - headers: { - authorization: `Bearer ${auth.password}`, - }, - }) - .catch(err => { - console.log(err) - process.exit(1) - }) - - if (user) { - track("CLI_LOGIN_SUCCEEDED") - logger.success(spinner, "Log in succeeded.") - setToken(auth.password) - } else { - track("CLI_LOGIN_FAILED") - logger.failure(spinner, "Log in failed.") - } - }, -} diff --git a/packages/medusa-cli/src/commands/new.js b/packages/medusa-cli/src/commands/new.ts similarity index 97% rename from packages/medusa-cli/src/commands/new.js rename to packages/medusa-cli/src/commands/new.ts index df6cfccebdc34..e54f88e10fd42 100644 --- a/packages/medusa-cli/src/commands/new.js +++ b/packages/medusa-cli/src/commands/new.ts @@ -19,12 +19,13 @@ import inquirer from "inquirer" import reporter from "../reporter" import { getPackageManager, setPackageManager } from "../util/package-manager" import { clearProject } from "@medusajs/utils" +import { PanicId } from "../reporter/panic-handler" const removeUndefined = (obj) => { return Object.fromEntries( Object.entries(obj) .filter(([_, v]) => v != null) - .map(([k, v]) => [k, v === Object(v) ? removeEmpty(v) : v]) + .map(([k, v]) => [k, v === Object(v) ? removeUndefined(v) : v]) ) } @@ -119,10 +120,10 @@ const install = async (rootPath) => { } if (getPackageManager() === `yarn` && checkForYarn()) { await fs.remove(`package-lock.json`) - await spawn(`yarnpkg`) + await spawn(`yarnpkg`, {}) } else { await fs.remove(`yarn.lock`) - await spawn(`npm install`) + await spawn(`npm install`, {}) } } finally { process.chdir(prevDir) @@ -389,6 +390,8 @@ Do you wish to continue with these credentials? console.log("\n\nCould not verify DB credentials - please try again\n\n") } + + return } const setupDB = async (dbName, dbCreds = {}) => { @@ -553,7 +556,7 @@ medusa new ${rootPath} [url-to-starter] if (/medusa-starter/gi.test(rootPath) && isStarterAUrl) { reporter.panic({ - id: `10000`, + id: PanicId.InvalidProjectName, context: { starter, rootPath, @@ -561,8 +564,9 @@ medusa new ${rootPath} [url-to-starter] }) return } + reporter.panic({ - id: `10001`, + id: PanicId.InvalidProjectName, context: { rootPath, }, @@ -572,7 +576,7 @@ medusa new ${rootPath} [url-to-starter] if (!isValid(rootPath)) { reporter.panic({ - id: `10002`, + id: PanicId.InvalidPath, context: { path: sysPath.resolve(rootPath), }, @@ -582,7 +586,7 @@ medusa new ${rootPath} [url-to-starter] if (existsSync(sysPath.join(rootPath, `package.json`))) { reporter.panic({ - id: `10003`, + id: PanicId.AlreadyNodeProject, context: { rootPath, }, diff --git a/packages/medusa-cli/src/commands/whoami.js b/packages/medusa-cli/src/commands/whoami.js deleted file mode 100644 index 54d25059bde67..0000000000000 --- a/packages/medusa-cli/src/commands/whoami.js +++ /dev/null @@ -1,48 +0,0 @@ -const axios = require("axios").default -const { getToken } = require("../util/token-store") -const logger = require("../reporter").default - -/** - * Fetches the locally logged in user. - */ -module.exports = { - whoami: async argv => { - const apiHost = - process.env.MEDUSA_API_HOST || "https://api.medusa-commerce.com" - - const tok = getToken() - - if (!tok) { - console.log( - "You are not logged into Medusa Cloud. Please run medusa login." - ) - process.exit(0) - } - - const activity = logger.activity("checking login details") - - const { data: auth } = await axios - .get(`${apiHost}/auth`, { - headers: { - authorization: `Bearer ${tok}`, - }, - }) - .catch(err => { - logger.failure(activity, "Couldn't gather login details") - logger.error(err) - process.exit(1) - }) - - if (auth.user) { - logger.success( - activity, - `Hi, ${auth.user.first_name}! Here are your details:` - ) - - console.log(`id: ${auth.user.id}`) - console.log(`email: ${auth.user.email}`) - console.log(`first_name: ${auth.user.first_name}`) - console.log(`last_name: ${auth.user.last_name}`) - } - }, -} diff --git a/packages/medusa-cli/src/create-cli.js b/packages/medusa-cli/src/create-cli.ts similarity index 83% rename from packages/medusa-cli/src/create-cli.js rename to packages/medusa-cli/src/create-cli.ts index a124004f513a2..3b1e9ceae336d 100644 --- a/packages/medusa-cli/src/create-cli.js +++ b/packages/medusa-cli/src/create-cli.ts @@ -1,17 +1,15 @@ -const path = require(`path`) -const resolveCwd = require(`resolve-cwd`) -const yargs = require(`yargs`) -const existsSync = require(`fs-exists-cached`).sync -const { setTelemetryEnabled } = require("medusa-telemetry") +import path from "path" +import resolveCwd from "resolve-cwd" +import { sync as existsSync } from "fs-exists-cached" +import { setTelemetryEnabled } from "medusa-telemetry" + +import { getLocalMedusaVersion } from "./util/version" +import { didYouMean } from "./did-you-mean" -const { getLocalMedusaVersion } = require(`./util/version`) -const { didYouMean } = require(`./did-you-mean`) +import reporter from "./reporter" +import { newStarter } from "./commands/new" -const reporter = require("./reporter").default -const { newStarter } = require("./commands/new") -const { whoami } = require("./commands/whoami") -const { login } = require("./commands/login") -const { link } = require("./commands/link") +const yargs = require(`yargs`) const handlerP = (fn) => @@ -31,18 +29,10 @@ function buildLocalCommands(cli, isLocalProject) { const useYarn = existsSync(path.join(directory, `yarn.lock`)) if (isLocalProject) { - const json = require(path.join(directory, `package.json`)) - projectInfo.sitePackageJson = json - } - - function getLocalMedusaMajorVersion() { - let version = getLocalMedusaVersion() - - if (version) { - version = Number(version.split(`.`)[0]) - } - - return version + projectInfo["sitePackageJson"] = require(path.join( + directory, + `package.json` + )) } function resolveLocalCommand(command) { @@ -53,10 +43,10 @@ function buildLocalCommands(cli, isLocalProject) { try { const cmdPath = resolveCwd.silent( `@medusajs/medusa/dist/commands/${command}` - ) + )! return require(cmdPath).default } catch (err) { - if (process.env.NODE_ENV !== "production") { + if (!process.env.NODE_ENV?.startsWith("prod")) { console.log("--------------- ERROR ---------------------") console.log(err) console.log("-------------------------------------------") @@ -166,7 +156,7 @@ function buildLocalCommands(cli, isLocalProject) { }), handler: handlerP( getCommandHandler(`seed`, (args, cmd) => { - process.env.NODE_ENV = process.env.NODE_ENV || `development` + process.env.NODE_ENV ??= `development` return cmd(args) }) ), @@ -187,41 +177,6 @@ function buildLocalCommands(cli, isLocalProject) { }) ), }) - .command({ - command: `whoami`, - desc: `View the details of the currently logged in user.`, - handler: handlerP(whoami), - }) - .command({ - command: `link`, - desc: `Creates your Medusa Cloud user in your local database for local testing.`, - builder: (_) => - _.option(`su`, { - alias: `skip-local-user`, - type: `boolean`, - default: false, - describe: `If set a user will not be created in the database.`, - }).option(`develop`, { - type: `boolean`, - default: false, - describe: `If set medusa develop will be run after successful linking.`, - }), - handler: handlerP((argv) => { - if (!isLocalProject) { - console.log("must be a local project") - cli.showHelp() - } - - const args = { ...argv, ...projectInfo, useYarn } - - return link(args) - }), - }) - .command({ - command: `login`, - desc: `Logs you into Medusa Cloud.`, - handler: handlerP(login), - }) .command({ command: `develop`, desc: `Start development server. Watches file and rebuilds when something changes`, @@ -316,17 +271,20 @@ function buildLocalCommands(cli, isLocalProject) { function isLocalMedusaProject() { let inMedusaProject = false + try { const { dependencies, devDependencies } = require(path.resolve( `./package.json` )) - inMedusaProject = + inMedusaProject = !!( (dependencies && dependencies["@medusajs/medusa"]) || (devDependencies && devDependencies["@medusajs/medusa"]) + ) } catch (err) { - /* ignore */ + // ignore } - return !!inMedusaProject + + return inMedusaProject } function getVersionInfo() { @@ -347,7 +305,7 @@ Medusa version: ${medusaVersion} } } -module.exports = (argv) => { +export default (argv) => { const cli = yargs() const isLocalProject = isLocalMedusaProject() @@ -402,7 +360,7 @@ module.exports = (argv) => { const arg = argv.slice(2)[0] const suggestion = arg ? didYouMean(arg, availableCommands) : `` - if (process.env.NODE_ENV !== "production") { + if (!process.env.NODE_ENV?.startsWith("prod")) { console.log("--------------- ERROR ---------------------") console.log(err) console.log("-------------------------------------------") diff --git a/packages/medusa-cli/src/did-you-mean.js b/packages/medusa-cli/src/did-you-mean.ts similarity index 74% rename from packages/medusa-cli/src/did-you-mean.js rename to packages/medusa-cli/src/did-you-mean.ts index a818bdb46b997..70011d5d40d18 100644 --- a/packages/medusa-cli/src/did-you-mean.js +++ b/packages/medusa-cli/src/did-you-mean.ts @@ -1,7 +1,7 @@ import meant from "meant" -export function didYouMean(scmd, commands) { - const bestSimilarity = meant(scmd, commands).map(str => { +export function didYouMean(scmd, commands): string { + const bestSimilarity = meant(scmd, commands).map((str) => { return ` ${str}` }) diff --git a/packages/medusa-cli/src/index.js b/packages/medusa-cli/src/index.js deleted file mode 100644 index 998df3496e0f7..0000000000000 --- a/packages/medusa-cli/src/index.js +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env node - -import "core-js/stable" -import "regenerator-runtime/runtime" -import os from "os" -import semver from "semver" -import util from "util" -import createCli from "./create-cli" -// import report from "./reporter" -import pkg from "../package.json" -// import updateNotifier from "update-notifier" -// import { ensureWindowsDriveLetterIsUppercase } from "./util/ensure-windows-drive-letter-is-uppercase" - -const useJsonLogger = process.argv.slice(2).some(arg => arg.includes(`json`)) - -if (useJsonLogger) { - process.env.GATSBY_LOGGER = `json` -} - -// Ensure stable runs on Windows when started from different shells (i.e. c:\dir vs C:\dir) -if (os.platform() === `win32`) { - // ensureWindowsDriveLetterIsUppercase() -} - -// Check if update is available -// updateNotifier({ pkg }).notify({ isGlobal: true }) - -const MIN_NODE_VERSION = `10.13.0` -// const NEXT_MIN_NODE_VERSION = `10.13.0` - -if (!semver.satisfies(process.version, `>=${MIN_NODE_VERSION}`)) { - //report.panic( - // report.stripIndent(` - // Gatsby requires Node.js ${MIN_NODE_VERSION} or higher (you have ${process.version}). - // Upgrade Node to the latest stable release: https://gatsby.dev/upgrading-node-js - // `) - //) -} - -// if (!semver.satisfies(process.version, `>=${NEXT_MIN_NODE_VERSION}`)) { -// report.warn( -// report.stripIndent(` -// Node.js ${process.version} has reached End of Life status on 31 December, 2019. -// Gatsby will only actively support ${NEXT_MIN_NODE_VERSION} or higher and drop support for Node 8 soon. -// Please upgrade Node.js to a currently active LTS release: https://gatsby.dev/upgrading-node-js -// `) -// ) -// } - -process.on(`unhandledRejection`, reason => { - // This will exit the process in newer Node anyway so lets be consistent - // across versions and crash - - // reason can be anything, it can be a message, an object, ANYTHING! - // we convert it to an error object so we don't crash on structured error validation - if (!(reason instanceof Error)) { - reason = new Error(util.format(reason)) - } - - console.log(reason) - // report.panic(`UNHANDLED REJECTION`, reason as Error) -}) - -process.on(`uncaughtException`, error => { - console.log(error) - // report.panic(`UNHANDLED EXCEPTION`, error) -}) - -createCli(process.argv) diff --git a/packages/medusa-cli/src/index.ts b/packages/medusa-cli/src/index.ts new file mode 100644 index 0000000000000..d7ee7205008d6 --- /dev/null +++ b/packages/medusa-cli/src/index.ts @@ -0,0 +1,44 @@ +#!/usr/bin/env node + +import "core-js/stable" +import "regenerator-runtime/runtime" +import os from "os" +import util from "util" +import createCli from "./create-cli" + +const useJsonLogger = process.argv.slice(2).some((arg) => arg.includes(`json`)) + +if (useJsonLogger) { + process.env.GATSBY_LOGGER = `json` +} + +// Ensure stable runs on Windows when started from different shells (i.e. c:\dir vs C:\dir) +if (os.platform() === `win32`) { + // ensureWindowsDriveLetterIsUppercase() +} + +// Check if update is available +// updateNotifier({ pkg }).notify({ isGlobal: true }) + +const MIN_NODE_VERSION = `10.13.0` + +process.on(`unhandledRejection`, (reason) => { + // This will exit the process in newer Node anyway so lets be consistent + // across versions and crash + + // reason can be anything, it can be a message, an object, ANYTHING! + // we convert it to an error object, so we don't crash on structured error validation + if (!(reason instanceof Error)) { + reason = new Error(util.format(reason)) + } + + console.log(reason) + // report.panic(`UNHANDLED REJECTION`, reason as Error) +}) + +process.on(`uncaughtException`, (error) => { + console.log(error) + // report.panic(`UNHANDLED EXCEPTION`, error) +}) + +createCli(process.argv) diff --git a/packages/medusa-cli/src/reporter/index.js b/packages/medusa-cli/src/reporter/index.ts similarity index 91% rename from packages/medusa-cli/src/reporter/index.js rename to packages/medusa-cli/src/reporter/index.ts index db87c6b4f9032..7ed1bd2e3c6e0 100644 --- a/packages/medusa-cli/src/reporter/index.js +++ b/packages/medusa-cli/src/reporter/index.ts @@ -5,12 +5,14 @@ import ora from "ora" import { track } from "medusa-telemetry" import { panicHandler } from "./panic-handler" +import * as Transport from "winston-transport" const LOG_LEVEL = process.env.LOG_LEVEL || "silly" const NODE_ENV = process.env.NODE_ENV || "development" -const IS_DEV = NODE_ENV === "development" +const IS_DEV = NODE_ENV.startsWith("dev") + +let transports: Transport[] = [] -const transports = [] if (!IS_DEV) { transports.push(new winston.transports.Console()) } else { @@ -39,8 +41,12 @@ const loggerInstance = winston.createLogger({ }) export class Reporter { + protected activities_: Record + protected loggerInstance_: winston.Logger + protected ora_: typeof ora + constructor({ logger, activityLogger }) { - this.activities_ = [] + this.activities_ = {} this.loggerInstance_ = logger this.ora_ = activityLogger } @@ -92,6 +98,7 @@ export class Reporter { * Begin an activity. In development an activity is displayed as a spinner; * in other environments it will log the activity at the info level. * @param {string} message - the message to log the activity under + * @param {any} config * @returns {string} the id of the activity; this should be passed to do * further operations on the activity such as success, failure, progress. */ @@ -141,7 +148,7 @@ export class Reporter { if (activity.activity) { activity.text = message } else { - toLog.activity_id = activityId + toLog["activity_id"] = activityId this.loggerInstance_.log(toLog) } } else { @@ -156,7 +163,7 @@ export class Reporter { * message to log the error under; or an error object. * @param {Error?} error - an error object to log message with */ - error = (messageOrError, error) => { + error = (messageOrError, error = null) => { let message = messageOrError if (typeof messageOrError === "object") { message = messageOrError.message @@ -169,7 +176,7 @@ export class Reporter { } if (error) { - toLog.stack = stackTrace.parse(error) + toLog["stack"] = stackTrace.parse(error) } this.loggerInstance_.log(toLog) @@ -200,8 +207,8 @@ export class Reporter { if (activity.activity) { activity.activity.fail(`${message} – ${time - activity.start}`) } else { - toLog.duration = time - activity.start - toLog.activity_id = activityId + toLog["duration"] = time - activity.start + toLog["activity_id"] = activityId this.loggerInstance_.log(toLog) } } else { @@ -225,7 +232,7 @@ export class Reporter { * at the info level. * @param {string} activityId - the id of the activity as returned by activity * @param {string} message - the message to log - * @returns {object} data about the activity + * @returns {Record} data about the activity */ success = (activityId, message) => { const time = Date.now() @@ -239,8 +246,8 @@ export class Reporter { if (activity.activity) { activity.activity.succeed(`${message} – ${time - activity.start}ms`) } else { - toLog.duration = time - activity.start - toLog.activity_id = activityId + toLog["duration"] = time - activity.start + toLog["activity_id"] = activityId this.loggerInstance_.log(toLog) } } else { @@ -295,6 +302,7 @@ export class Reporter { * A wrapper around winston's log method. */ log = (...args) => { + // @ts-ignore this.loggerInstance_.log(...args) } } diff --git a/packages/medusa-cli/src/reporter/panic-handler.js b/packages/medusa-cli/src/reporter/panic-handler.ts similarity index 65% rename from packages/medusa-cli/src/reporter/panic-handler.js rename to packages/medusa-cli/src/reporter/panic-handler.ts index 588e96b12ac79..514964985ae7d 100644 --- a/packages/medusa-cli/src/reporter/panic-handler.js +++ b/packages/medusa-cli/src/reporter/panic-handler.ts @@ -1,14 +1,24 @@ -export const panicHandler = (panicData = {}) => { +export type PanicData = { + id: string + context: { + rootPath: string + path: string + } +} + +export enum PanicId { + InvalidProjectName = "10000", + InvalidPath = "10002", + AlreadyNodeProject = "10003", +} + +export const panicHandler = (panicData: PanicData = {} as PanicData) => { const { id, context } = panicData switch (id) { case "10000": return { message: `Looks like you provided a URL as your project name. Try "medusa new my-medusa-store ${context.rootPath}" instead.`, } - case "10001": - return { - message: `Looks like you provided a URL as your project name. Try "medusa new my-medusa-store ${context.rootPath}" instead.`, - } case "10002": return { message: `Could not create project because ${context.path} is not a valid path.`, diff --git a/packages/medusa-cli/src/util/package-manager.js b/packages/medusa-cli/src/util/package-manager.ts similarity index 57% rename from packages/medusa-cli/src/util/package-manager.js rename to packages/medusa-cli/src/util/package-manager.ts index 595e63f33033e..0cb270fe35c68 100644 --- a/packages/medusa-cli/src/util/package-manager.js +++ b/packages/medusa-cli/src/util/package-manager.ts @@ -1,22 +1,15 @@ import ConfigStore from "configstore" import reporter from "../reporter" -let config +const config = new ConfigStore(`medusa`, {}, { globalConfigPath: true }) const packageMangerConfigKey = `cli.packageManager` export const getPackageManager = () => { - if (!config) { - config = new ConfigStore(`medusa`, {}, { globalConfigPath: true }) - } - return config.get(packageMangerConfigKey) } -export const setPackageManager = packageManager => { - if (!config) { - config = new ConfigStore(`medusa`, {}, { globalConfigPath: true }) - } +export const setPackageManager = (packageManager) => { config.set(packageMangerConfigKey, packageManager) reporter.info(`Preferred package manager set to "${packageManager}"`) } diff --git a/packages/medusa-cli/src/util/token-store.js b/packages/medusa-cli/src/util/token-store.js deleted file mode 100644 index acf22ee424aee..0000000000000 --- a/packages/medusa-cli/src/util/token-store.js +++ /dev/null @@ -1,20 +0,0 @@ -const ConfigStore = require("configstore") - -let config - -module.exports = { - getToken: function() { - if (!config) { - config = new ConfigStore(`medusa`, {}, { globalConfigPath: true }) - } - - return config.get("cloud.login_token") - }, - setToken: function(token) { - if (!config) { - config = new ConfigStore(`medusa`, {}, { globalConfigPath: true }) - } - - return config.set("cloud.login_token", token) - }, -} diff --git a/packages/medusa-cli/src/util/version.js b/packages/medusa-cli/src/util/version.js deleted file mode 100644 index 89686f7c1a7c6..0000000000000 --- a/packages/medusa-cli/src/util/version.js +++ /dev/null @@ -1,6 +0,0 @@ -import { getMedusaVersion } from "medusa-core-utils" - -export const getLocalMedusaVersion = () => { - const version = getMedusaVersion() - return version -} diff --git a/packages/medusa-cli/src/util/version.ts b/packages/medusa-cli/src/util/version.ts new file mode 100644 index 0000000000000..088ea17ed8aeb --- /dev/null +++ b/packages/medusa-cli/src/util/version.ts @@ -0,0 +1,5 @@ +import { getMedusaVersion } from "medusa-core-utils" + +export const getLocalMedusaVersion = (): string => { + return getMedusaVersion() +} diff --git a/packages/medusa-cli/tsconfig.json b/packages/medusa-cli/tsconfig.json new file mode 100644 index 0000000000000..731ef8a6e0d46 --- /dev/null +++ b/packages/medusa-cli/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "lib": ["es2020"], + "target": "es2020", + "outDir": "./dist", + "esModuleInterop": true, + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true, + "downlevelIteration": true // to use ES5 specific tooling + }, + "include": ["src"], + "exclude": [ + "dist", + "./src/**/__tests__", + "./src/**/__mocks__", + "./src/**/__fixtures__", + "node_modules" + ] +} diff --git a/packages/medusa-cli/tsconfig.spec.json b/packages/medusa-cli/tsconfig.spec.json new file mode 100644 index 0000000000000..9b6240919113c --- /dev/null +++ b/packages/medusa-cli/tsconfig.spec.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/packages/medusa-core-utils/src/index.ts b/packages/medusa-core-utils/src/index.ts index e2858fbdc005d..6e03eb2e4b0ce 100644 --- a/packages/medusa-core-utils/src/index.ts +++ b/packages/medusa-core-utils/src/index.ts @@ -12,3 +12,4 @@ export * from "./medusa-container" export { parseCorsOrigins } from "./parse-cors-origins" export { transformIdableFields } from "./transform-idable-fields" export { default as zeroDecimalCurrencies } from "./zero-decimal-currencies" +export { getMedusaVersion } from "./get-medusa-version" diff --git a/packages/medusa-dev-cli/.babelrc.js b/packages/medusa-dev-cli/.babelrc.js deleted file mode 100644 index 5611ada92405e..0000000000000 --- a/packages/medusa-dev-cli/.babelrc.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - presets: [["babel-preset-medusa-package"]], -}; diff --git a/packages/medusa-dev-cli/.npmignore b/packages/medusa-dev-cli/.npmignore deleted file mode 100644 index 4c7126f447fd8..0000000000000 --- a/packages/medusa-dev-cli/.npmignore +++ /dev/null @@ -1,3 +0,0 @@ -src -flow-typed -verdaccio diff --git a/packages/medusa-dev-cli/jest.config.js b/packages/medusa-dev-cli/jest.config.js new file mode 100644 index 0000000000000..14cd7ed6e7aca --- /dev/null +++ b/packages/medusa-dev-cli/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + globals: { + "ts-jest": { + tsConfig: "tsconfig.spec.json", + isolatedModules: false, + }, + }, + transform: { + "^.+\\.[jt]s?$": "ts-jest", + }, + testEnvironment: `node`, + moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`], +} diff --git a/packages/medusa-dev-cli/package.json b/packages/medusa-dev-cli/package.json index 3ce690a21a45a..6cfb4c02b292c 100644 --- a/packages/medusa-dev-cli/package.json +++ b/packages/medusa-dev-cli/package.json @@ -10,7 +10,6 @@ "dist" ], "dependencies": { - "@babel/runtime": "^7.12.5", "chokidar": "^3.5.3", "configstore": "^5.0.1", "del": "^6.0.0", @@ -20,18 +19,16 @@ "glob": "^8.1.0", "got": "^11.8.6", "is-absolute": "^1.0.0", - "jest": "^25.5.4", "lodash": "^4.17.21", "signal-exit": "^3.0.7", "verdaccio": "^4.10.0", "yargs": "^15.4.1" }, "devDependencies": { - "@babel/cli": "^7.12.1", - "@babel/core": "^7.12.3", - "babel-preset-medusa-package": "^1.1.19", "cross-env": "^7.0.3", - "jest": "^25.5.4" + "jest": "^25.5.4", + "ts-jest": "^25.5.1", + "typescript": "^4.4.4" }, "homepage": "https://github.com/medusajs/medusa/tree/master/packages/medusa-dev-cli#readme", "keywords": [ @@ -46,9 +43,9 @@ }, "scripts": { "prepare": "cross-env NODE_ENV=production yarn run build", - "test": "jest --passWithNoTests src", - "build": "babel src --out-dir dist/ --ignore '**/__tests__','**/__mocks__'", - "watch": "babel -w src --out-dir dist/ --ignore '**/__tests__','**/__mocks__'" + "test": "jest --passWithNoTests", + "build": "tsc", + "watch": "tsc --watch" }, "engines": { "node": ">=12.13.0" diff --git a/packages/medusa-dev-cli/tsconfig.json b/packages/medusa-dev-cli/tsconfig.json new file mode 100644 index 0000000000000..07b7ad9286ceb --- /dev/null +++ b/packages/medusa-dev-cli/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "lib": ["es5", "es6", "es2019"], + "target": "es6", + "outDir": "./dist", + "esModuleInterop": true, + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true, + "downlevelIteration": true // to use ES5 specific tooling + }, + "include": ["src"], + "exclude": [ + "dist", + "./src/**/__tests__", + "./src/**/__mocks__", + "./src/**/__fixtures__", + "node_modules" + ] +} diff --git a/packages/medusa-dev-cli/tsconfig.spec.json b/packages/medusa-dev-cli/tsconfig.spec.json new file mode 100644 index 0000000000000..9b6240919113c --- /dev/null +++ b/packages/medusa-dev-cli/tsconfig.spec.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/packages/medusa-js/src/request.ts b/packages/medusa-js/src/request.ts index 20194fd5e4897..6c2252bd4c368 100644 --- a/packages/medusa-js/src/request.ts +++ b/packages/medusa-js/src/request.ts @@ -16,6 +16,7 @@ export interface Config { maxRetries: number apiKey?: string publishableApiKey?: string + customHeaders?: Record } export interface RequestOptions { @@ -199,6 +200,9 @@ class Client { options: RequestOptions = {}, customHeaders: Record = {} ): Promise { + + customHeaders = { ...this.config.customHeaders, ...customHeaders } + const reqOpts = { method, withCredentials: true, diff --git a/packages/medusa-plugin-algolia/src/loaders/index.ts b/packages/medusa-plugin-algolia/src/loaders/index.ts index b814f13c62cb6..321513fe383c0 100644 --- a/packages/medusa-plugin-algolia/src/loaders/index.ts +++ b/packages/medusa-plugin-algolia/src/loaders/index.ts @@ -1,4 +1,5 @@ -import { Logger, MedusaContainer } from "@medusajs/modules-sdk" +import { MedusaContainer } from "@medusajs/modules-sdk" +import { Logger } from "@medusajs/types" import AlgoliaService from "../services/algolia" import { AlgoliaPluginOptions } from "../types" diff --git a/packages/medusa-plugin-brightpearl/src/services/brightpearl.js b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js index b3eb85870bf3f..1095ff894f58f 100644 --- a/packages/medusa-plugin-brightpearl/src/services/brightpearl.js +++ b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js @@ -1,8 +1,5 @@ -import { MedusaError, humanizeAmount } from "medusa-core-utils" -import { - ReservationType, - updateInventoryAndReservations, -} from "@medusajs/medusa" +import { humanizeAmount, MedusaError } from "medusa-core-utils" +import { updateInventoryAndReservations } from "@medusajs/medusa" import { BaseService } from "medusa-interfaces" import Brightpearl from "../utils/brightpearl" @@ -111,7 +108,7 @@ class BrightpearlService extends BaseService { httpMethod: "POST", uriTemplate: `${this.options.backend_url}/brightpearl/goods-out`, bodyTemplate: - "{\"account\": \"${account-code}\", \"lifecycle_event\": \"${lifecycle-event}\", \"resource_type\": \"${resource-type}\", \"id\": \"${resource-id}\" }", + '{"account": "${account-code}", "lifecycle_event": "${lifecycle-event}", "resource_type": "${resource-type}", "id": "${resource-id}" }', contentType: "application/json", idSetAccepted: false, }, @@ -1008,6 +1005,7 @@ class BrightpearlService extends BaseService { "shipping_address", "billing_address", "shipping_methods", + "shipping_methods.shipping_option", "payments", "sales_channel", ], diff --git a/packages/medusa-plugin-brightpearl/src/subscribers/order.js b/packages/medusa-plugin-brightpearl/src/subscribers/order.js index 732905a96db15..4e5df3c97f2b8 100644 --- a/packages/medusa-plugin-brightpearl/src/subscribers/order.js +++ b/packages/medusa-plugin-brightpearl/src/subscribers/order.js @@ -102,6 +102,7 @@ class OrderSubscriber { "additional_items.tax_lines", "shipping_address", "shipping_methods", + "shipping_methods.shipping_option", "shipping_methods.tax_lines", ], }) @@ -148,6 +149,7 @@ class OrderSubscriber { "additional_items.variant.product", "additional_items.tax_lines", "shipping_address", + "shipping_methods.shipping_option", "shipping_methods", ], }) diff --git a/packages/medusa-plugin-meilisearch/src/loaders/index.ts b/packages/medusa-plugin-meilisearch/src/loaders/index.ts index f98b65342e147..f45cc98208f16 100644 --- a/packages/medusa-plugin-meilisearch/src/loaders/index.ts +++ b/packages/medusa-plugin-meilisearch/src/loaders/index.ts @@ -1,4 +1,5 @@ -import { Logger, MedusaContainer } from "@medusajs/modules-sdk" +import { MedusaContainer } from "@medusajs/modules-sdk" +import { Logger } from "@medusajs/types" import MeiliSearchService from "../services/meilisearch" import { MeilisearchPluginOptions } from "../types" diff --git a/packages/medusa-plugin-segment/src/subscribers/order.js b/packages/medusa-plugin-segment/src/subscribers/order.js index 49961fc8c51c9..3c13b334a5ce7 100644 --- a/packages/medusa-plugin-segment/src/subscribers/order.js +++ b/packages/medusa-plugin-segment/src/subscribers/order.js @@ -46,6 +46,7 @@ class OrderSubscriber { "discounts", "discounts.rule", "shipping_methods", + "shipping_methods.shipping_option", "payments", "fulfillments", "returns", @@ -56,6 +57,7 @@ class OrderSubscriber { "swaps.return_order", "swaps.payment", "swaps.shipping_methods", + "swaps.shipping_methods.shipping_option", "swaps.shipping_address", "swaps.additional_items", "swaps.fulfillments", @@ -149,6 +151,7 @@ class OrderSubscriber { "discounts", "discounts.rule", "shipping_methods", + "shipping_methods.shipping_option", "payments", "fulfillments", "returns", @@ -159,6 +162,7 @@ class OrderSubscriber { "swaps.return_order", "swaps.payment", "swaps.shipping_methods", + "swaps.shipping_methods.shipping_option", "swaps.shipping_address", "swaps.additional_items", "swaps.fulfillments", @@ -257,6 +261,7 @@ class OrderSubscriber { "discounts", "discounts.rule", "shipping_methods", + "shipping_methods.shipping_option", "payments", "fulfillments", "items", @@ -267,6 +272,7 @@ class OrderSubscriber { "swaps.return_order", "swaps.payment", "swaps.shipping_methods", + "swaps.shipping_methods.shipping_option", "swaps.shipping_address", "swaps.additional_items", "swaps.fulfillments", diff --git a/packages/medusa-plugin-sendgrid/src/services/sendgrid.js b/packages/medusa-plugin-sendgrid/src/services/sendgrid.js index 95dca0344f157..8ac063c6b99b4 100644 --- a/packages/medusa-plugin-sendgrid/src/services/sendgrid.js +++ b/packages/medusa-plugin-sendgrid/src/services/sendgrid.js @@ -850,7 +850,9 @@ class SendGridService extends NotificationService { } async swapCreatedData({ id }) { - const store = await this.storeService_.retrieve({ where: { id: Not(IsNull()) } }) + const store = await this.storeService_.retrieve({ + where: { id: Not(IsNull()) }, + }) const swap = await this.swapService_.retrieve(id, { relations: [ "additional_items", @@ -913,7 +915,7 @@ class SendGridService extends NotificationService { "shipping_total", "subtotal", ], - relations: ["items", "items.variant", "items.variant.product"] + relations: ["items", "items.variant", "items.variant.product"], }) const currencyCode = order.currency_code.toUpperCase() @@ -993,6 +995,7 @@ class SendGridService extends NotificationService { relations: [ "shipping_address", "shipping_methods", + "shipping_methods.shipping_option", "shipping_methods.tax_lines", "additional_items", "additional_items.variant", @@ -1028,11 +1031,7 @@ class SendGridService extends NotificationService { "shipping_total", "subtotal", ], - relations: [ - "items", - "items.variant", - "items.variant.product", - ] + relations: ["items", "items.variant", "items.variant.product"], }) const returnRequest = swap.return_order @@ -1152,7 +1151,7 @@ class SendGridService extends NotificationService { "order.items", "order.items.variant", "order.items.variant.product", - "order.shipping_address" + "order.shipping_address", ], }) diff --git a/packages/medusa-plugin-slack-notification/src/services/slack.js b/packages/medusa-plugin-slack-notification/src/services/slack.js index b7e948d0e10f5..4c7ec8b463730 100644 --- a/packages/medusa-plugin-slack-notification/src/services/slack.js +++ b/packages/medusa-plugin-slack-notification/src/services/slack.js @@ -42,6 +42,7 @@ class SlackService extends BaseService { "discounts", "discounts.rule", "shipping_methods", + "shipping_methods.shipping_option", "payments", "fulfillments", "returns", @@ -51,6 +52,7 @@ class SlackService extends BaseService { "swaps.return_order", "swaps.payment", "swaps.shipping_methods", + "swaps.shipping_methods.shipping_option", "swaps.shipping_address", "swaps.additional_items", "swaps.fulfillments", diff --git a/packages/medusa-react/src/contexts/medusa.tsx b/packages/medusa-react/src/contexts/medusa.tsx index a7ad14c77673b..0cec47ed6f90b 100644 --- a/packages/medusa-react/src/contexts/medusa.tsx +++ b/packages/medusa-react/src/contexts/medusa.tsx @@ -32,6 +32,7 @@ interface MedusaProviderProps { * available within the request */ publishableApiKey?: string + customHeaders?: Record } export const MedusaProvider = ({ @@ -39,6 +40,7 @@ export const MedusaProvider = ({ baseUrl, apiKey, publishableApiKey, + customHeaders, children, }: MedusaProviderProps) => { const medusaClient = new Medusa({ @@ -46,6 +48,7 @@ export const MedusaProvider = ({ maxRetries: 0, apiKey, publishableApiKey, + customHeaders }) return ( diff --git a/packages/medusa-react/src/hooks/admin/reservations/queries.ts b/packages/medusa-react/src/hooks/admin/reservations/queries.ts index df291da927381..da4184b2b3c49 100644 --- a/packages/medusa-react/src/hooks/admin/reservations/queries.ts +++ b/packages/medusa-react/src/hooks/admin/reservations/queries.ts @@ -9,7 +9,7 @@ import { useMedusa } from "../../../contexts" import { UseQueryOptionsWrapper } from "../../../types" import { queryKeysFactory } from "../../utils" -const ADMIN_RESERVATIONS_QUERY_KEY = `admin_stock_locations` as const +const ADMIN_RESERVATIONS_QUERY_KEY = `admin_reservations` as const export const adminReservationsKeys = queryKeysFactory( ADMIN_RESERVATIONS_QUERY_KEY diff --git a/packages/medusa/jest.config.js b/packages/medusa/jest.config.js index adffb81889622..c9b04f47a106d 100644 --- a/packages/medusa/jest.config.js +++ b/packages/medusa/jest.config.js @@ -18,6 +18,7 @@ module.exports = { transform: { "^.+\\.[jt]s?$": "ts-jest", }, + modulePathIgnorePatterns: ["__fixtures__"], testEnvironment: `node`, moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`], setupFilesAfterEnv: ["/setupTests.js"], diff --git a/packages/medusa/src/api/routes/admin/swaps/__tests__/get-swap.js b/packages/medusa/src/api/routes/admin/swaps/__tests__/get-swap.js index cc39fbcc8c2ef..3c554879588e9 100644 --- a/packages/medusa/src/api/routes/admin/swaps/__tests__/get-swap.js +++ b/packages/medusa/src/api/routes/admin/swaps/__tests__/get-swap.js @@ -15,6 +15,7 @@ const defaultRelations = [ "return_order", "shipping_address", "shipping_methods", + "shipping_methods.shipping_option", ] const defaultFields = [ diff --git a/packages/medusa/src/api/routes/admin/swaps/index.ts b/packages/medusa/src/api/routes/admin/swaps/index.ts index 10a9d43f25e2c..ed67ce031f642 100644 --- a/packages/medusa/src/api/routes/admin/swaps/index.ts +++ b/packages/medusa/src/api/routes/admin/swaps/index.ts @@ -34,6 +34,7 @@ export const defaultAdminSwapRelations = [ "return_order", "shipping_address", "shipping_methods", + "shipping_methods.shipping_option", ] export const defaultAdminSwapFields = [ diff --git a/packages/medusa/src/api/routes/store/orders/index.ts b/packages/medusa/src/api/routes/store/orders/index.ts index ab86fb08c130d..eca95e5bdba15 100644 --- a/packages/medusa/src/api/routes/store/orders/index.ts +++ b/packages/medusa/src/api/routes/store/orders/index.ts @@ -83,6 +83,7 @@ export const defaultStoreOrdersRelations = [ "items", "items.variant", "shipping_methods", + "shipping_methods.shipping_option", "discounts", "discounts.rule", "customer", diff --git a/packages/medusa/src/api/routes/store/products/index.ts b/packages/medusa/src/api/routes/store/products/index.ts index 5410cfc35c041..48b63e77c7c37 100644 --- a/packages/medusa/src/api/routes/store/products/index.ts +++ b/packages/medusa/src/api/routes/store/products/index.ts @@ -104,9 +104,7 @@ export const allowedStoreProductsFields = [ export const allowedStoreProductsRelations = [ ...defaultStoreProductsRelations, - "variants.title", "variants.inventory_items", - "variants.prices.amount", "sales_channels", ] diff --git a/packages/medusa/src/api/routes/store/swaps/index.ts b/packages/medusa/src/api/routes/store/swaps/index.ts index 2215ef2fc3872..18f7c3cf749f9 100644 --- a/packages/medusa/src/api/routes/store/swaps/index.ts +++ b/packages/medusa/src/api/routes/store/swaps/index.ts @@ -24,10 +24,12 @@ export const defaultStoreSwapRelations = [ "additional_items.variant", "return_order", "return_order.shipping_method", + "return_order.shipping_method.shipping_option", "fulfillments", "payment", "shipping_address", "shipping_methods", + "shipping_methods.shipping_option", "cart", ] export const defaultStoreSwapFields: FindConfig["select"] = [ diff --git a/packages/medusa/src/commands/utils/get-migrations.js b/packages/medusa/src/commands/utils/get-migrations.js index 38daf0ac52d08..c4dc61e219d17 100644 --- a/packages/medusa/src/commands/utils/get-migrations.js +++ b/packages/medusa/src/commands/utils/get-migrations.js @@ -10,6 +10,7 @@ import { } from "medusa-core-utils" import path from "path" import { handleConfigError } from "../../loaders/config" +import { MEDUSA_PROJECT_NAME } from "../../loaders/plugins" function createFileContentHash(path, files) { return path + files @@ -143,10 +144,11 @@ export default (directory, featureFlagRouter) => { return details }) + // Resolve user's project as a plugin for loading purposes resolved.push({ resolve: `${directory}/dist`, - name: `project-plugin`, - id: createPluginId(`project-plugin`), + name: MEDUSA_PROJECT_NAME, + id: createPluginId(MEDUSA_PROJECT_NAME), options: {}, version: createFileContentHash(process.cwd(), `**`), }) diff --git a/packages/medusa/src/loaders/__tests__/__fixtures__/customizations/medusa-config.js b/packages/medusa/src/loaders/__tests__/__fixtures__/customizations/medusa-config.js new file mode 100644 index 0000000000000..7ec24e6c1e09e --- /dev/null +++ b/packages/medusa/src/loaders/__tests__/__fixtures__/customizations/medusa-config.js @@ -0,0 +1,9 @@ +module.exports = { + featureFlags: {}, + projectConfig: { + database_url: "postgres://localhost/medusa-store", + database_logging: false + }, + plugins: [], + modules: {}, +}; diff --git a/packages/medusa/src/loaders/__tests__/__fixtures__/customizations/models/product.ts b/packages/medusa/src/loaders/__tests__/__fixtures__/customizations/models/product.ts new file mode 100644 index 0000000000000..c08cb0c00865e --- /dev/null +++ b/packages/medusa/src/loaders/__tests__/__fixtures__/customizations/models/product.ts @@ -0,0 +1,11 @@ +import { Column, Entity } from "typeorm" +import { + // alias the core entity to not cause a naming conflict + Product as MedusaProduct, +} from "@medusajs/medusa" + +@Entity() +export class Product extends MedusaProduct { + @Column() + custom_attribute: string = 'test' +} diff --git a/packages/medusa/src/loaders/__tests__/models.spec.ts b/packages/medusa/src/loaders/__tests__/models.spec.ts new file mode 100644 index 0000000000000..7ec8e5af54d57 --- /dev/null +++ b/packages/medusa/src/loaders/__tests__/models.spec.ts @@ -0,0 +1,47 @@ +import { createMedusaContainer } from "medusa-core-utils" +import path from "path" +import { asValue } from "awilix" + +import modelsLoader from "../models" + +describe("models loader", () => { + const container = createMedusaContainer() + container.register("db_entities", asValue([])) + let models + let error + + beforeAll(async () => { + try { + models = modelsLoader({ + container, + isTest: true, + coreTestPathGlob: "../models/{product,product-variant}.ts", + rootDirectory: path.join(__dirname, "__fixtures__/customizations"), + extensionPathGlob: "models/{product,product-variant}.ts", + }) + } catch (e) { + error = e + } + }) + + it("error should be falsy & register 2 models", () => { + expect(error).toBeFalsy() + expect(models).toHaveLength(2) + }) + + it("ensure that the product model is an extended model", () => { + const productModel = container.resolve("productModel") + + expect(productModel.custom_attribute).toEqual("test") + }) + + it("ensure that the extended product model is registered in db_entities", () => { + const entities = container.resolve("db_entities_STORE") + const productModelResolver = entities.find( + (entity) => entity.resolve().name === "Product" + ) + const productModel = productModelResolver.resolve() + + expect(new productModel().custom_attribute).toEqual("test") + }) +}) diff --git a/packages/medusa/src/loaders/__tests__/plugins.spec.ts b/packages/medusa/src/loaders/__tests__/plugins.spec.ts index 06bc62d48a146..9ae20ab702419 100644 --- a/packages/medusa/src/loaders/__tests__/plugins.spec.ts +++ b/packages/medusa/src/loaders/__tests__/plugins.spec.ts @@ -5,11 +5,11 @@ import { Resolver, } from "awilix" import { mkdirSync, rmSync, writeFileSync } from "fs" +import { createMedusaContainer } from "medusa-core-utils" import { resolve } from "path" -import Logger from "../logger" -import { registerServices, registerStrategies } from "../plugins" import { DataSource, EntityManager } from "typeorm" -import { createMedusaContainer } from "medusa-core-utils" +import Logger from "../logger" +import { MEDUSA_PROJECT_NAME, registerServices, registerStrategies } from "../plugins" // ***** TEMPLATES ***** const buildServiceTemplate = (name: string): string => { @@ -116,7 +116,7 @@ describe("plugins loader", () => { const pluginsDetails = { resolve: resolve(__dirname, "__pluginsLoaderTest__"), - name: `project-plugin`, + name: MEDUSA_PROJECT_NAME, id: "fakeId", options: {}, version: '"fakeVersion', diff --git a/packages/medusa/src/loaders/__tests__/register-plugin-models.spec.ts b/packages/medusa/src/loaders/__tests__/register-plugin-models.spec.ts new file mode 100644 index 0000000000000..144ad5f1c6d90 --- /dev/null +++ b/packages/medusa/src/loaders/__tests__/register-plugin-models.spec.ts @@ -0,0 +1,36 @@ +import { createMedusaContainer } from "medusa-core-utils" +import path from "path" +import { asValue } from "awilix" + +import { registerPluginModels } from "../plugins" +import configModule from './__fixtures__/customizations/medusa-config' + +describe("plugin models loader", () => { + const container = createMedusaContainer() + container.register("db_entities", asValue([])) + + let models + let error + + beforeAll(async () => { + try { + await registerPluginModels({ + configModule: configModule, + container, + rootDirectory: path.join(__dirname, '__fixtures__/customizations'), + extensionDirectoryPath: './', + pathGlob: "/models/*.ts", + }) + } catch (e) { + error = e + } + }) + + it("ensure that the product model is registered from the user's respository", () => { + const entities = container.resolve("db_entities_STORE") + const productModelResolver = entities.find(entity => entity.resolve().name === 'Product') + const productModel = productModelResolver.resolve() + + expect((new productModel()).custom_attribute).toEqual("test") + }) +}) diff --git a/packages/medusa/src/loaders/helpers/get-model-extension-map.ts b/packages/medusa/src/loaders/helpers/get-model-extension-map.ts new file mode 100644 index 0000000000000..bc0ac0f6261d2 --- /dev/null +++ b/packages/medusa/src/loaders/helpers/get-model-extension-map.ts @@ -0,0 +1,58 @@ +import glob from "glob" +import path from "path" +import { EntitySchema } from "typeorm" + +import { formatRegistrationName } from "../../utils/format-registration-name" +import { ClassConstructor } from "../../types/global" + +type GetModelExtensionMapParams = { + directory?: string + pathGlob?: string + config: Record +} + +export function getModelExtensionsMap({ + directory, + pathGlob, + config = {}, +}: GetModelExtensionMapParams): Map< + string, + ClassConstructor | EntitySchema +> { + const modelExtensionsMap = new Map< + string, + ClassConstructor | EntitySchema + >() + const fullPathGlob = + directory && pathGlob ? path.join(directory, pathGlob) : null + + const modelExtensions = fullPathGlob + ? glob.sync(fullPathGlob, { + cwd: directory, + ignore: ["index.js", "index.js.map"], + }) + : [] + + modelExtensions.forEach((modelExtensionPath) => { + const extendedModel = require(modelExtensionPath) as + | ClassConstructor + | EntitySchema + | undefined + + if (extendedModel) { + Object.entries(extendedModel).map( + ([_key, val]: [string, ClassConstructor | EntitySchema]) => { + if (typeof val === "function" || val instanceof EntitySchema) { + if (config.register) { + const name = formatRegistrationName(modelExtensionPath) + + modelExtensionsMap.set(name, val) + } + } + } + ) + } + }) + + return modelExtensionsMap +} diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index 9aa98c2e8bc36..63bf8466a763c 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -67,7 +67,7 @@ export default async ({ const modelsActivity = Logger.activity(`Initializing models${EOL}`) track("MODELS_INIT_STARTED") - modelsLoader({ container }) + modelsLoader({ container, rootDirectory }) const mAct = Logger.success(modelsActivity, "Models initialized") || {} track("MODELS_INIT_COMPLETED", { duration: mAct.duration }) diff --git a/packages/medusa/src/loaders/models.ts b/packages/medusa/src/loaders/models.ts index c237e734dc844..aba6c9d22ad78 100644 --- a/packages/medusa/src/loaders/models.ts +++ b/packages/medusa/src/loaders/models.ts @@ -1,34 +1,76 @@ -import formatRegistrationName from "../utils/format-registration-name" +import { + formatRegistrationName, + formatRegistrationNameWithoutNamespace, +} from "../utils/format-registration-name" +import { getModelExtensionsMap } from "./helpers/get-model-extension-map" import glob from "glob" import path from "path" import { ClassConstructor, MedusaContainer } from "../types/global" import { EntitySchema } from "typeorm" import { asClass, asValue } from "awilix" +import { upperCaseFirst } from "@medusajs/utils" +type ModelLoaderParams = { + container: MedusaContainer + isTest?: boolean + rootDirectory?: string + corePathGlob?: string + coreTestPathGlob?: string + extensionPathGlob?: string +} /** * Registers all models in the model directory */ export default ( - { container, isTest }: { container: MedusaContainer; isTest?: boolean }, + { + container, + isTest, + rootDirectory, + corePathGlob = "../models/*.js", + coreTestPathGlob = "../models/*.ts", + extensionPathGlob = "dist/models/*.js", + }: ModelLoaderParams, config = { register: true } ) => { - const corePath = isTest ? "../models/*.ts" : "../models/*.js" - const coreFull = path.join(__dirname, corePath) - + const coreModelsGlob = isTest ? coreTestPathGlob : corePathGlob + const coreModelsFullGlob = path.join(__dirname, coreModelsGlob) const models: (ClassConstructor | EntitySchema)[] = [] - const core = glob.sync(coreFull, { + const coreModels = glob.sync(coreModelsFullGlob, { cwd: __dirname, - ignore: ["index.js", "index.ts"], + ignore: ["index.js", "index.ts", "index.js.map"], }) - core.forEach((fn) => { - const loaded = require(fn) as ClassConstructor | EntitySchema + + const modelExtensionsMap = getModelExtensionsMap({ + directory: rootDirectory, + pathGlob: extensionPathGlob, + config, + }) + + coreModels.forEach((modelPath) => { + const loaded = require(modelPath) as + | ClassConstructor + | EntitySchema + if (loaded) { Object.entries(loaded).map( ([, val]: [string, ClassConstructor | EntitySchema]) => { if (typeof val === "function" || val instanceof EntitySchema) { if (config.register) { - const name = formatRegistrationName(fn) + const name = formatRegistrationName(modelPath) + const mappedExtensionModel = modelExtensionsMap.get(name) + + // If an extension file is found, override it with that instead + if (mappedExtensionModel) { + const coreModel = require(modelPath) + const modelName = upperCaseFirst( + formatRegistrationNameWithoutNamespace(modelPath) + ) + + coreModel[modelName] = mappedExtensionModel + val = mappedExtensionModel + } + container.register({ [name]: asClass(val as ClassConstructor), }) diff --git a/packages/medusa/src/loaders/plugins.ts b/packages/medusa/src/loaders/plugins.ts index 52fcdbd9bbb9f..de6df5f2e3f7d 100644 --- a/packages/medusa/src/loaders/plugins.ts +++ b/packages/medusa/src/loaders/plugins.ts @@ -1,4 +1,4 @@ -import { SearchUtils } from "@medusajs/utils" +import { SearchUtils, upperCaseFirst } from "@medusajs/utils" import { aliasTo, asFunction, asValue, Lifetime } from "awilix" import { Express } from "express" import fs from "fs" @@ -30,7 +30,11 @@ import { Logger, MedusaContainer, } from "../types/global" -import formatRegistrationName from "../utils/format-registration-name" +import { + formatRegistrationName, + formatRegistrationNameWithoutNamespace, +} from "../utils/format-registration-name" +import { getModelExtensionsMap } from "./helpers/get-model-extension-map" import { registerPaymentProcessorFromClass, registerPaymentServiceFromClass, @@ -55,6 +59,8 @@ type PluginDetails = { export const isSearchEngineInstalledResolutionKey = "isSearchEngineInstalled" +export const MEDUSA_PROJECT_NAME = "project-plugin" + /** * Registers all services in the services directory */ @@ -93,7 +99,8 @@ export default async ({ function getResolvedPlugins( rootDirectory: string, - configModule: ConfigModule + configModule: ConfigModule, + extensionDirectoryPath = "dist" ): undefined | PluginDetails[] { const { plugins } = configModule @@ -108,10 +115,12 @@ function getResolvedPlugins( return details }) + const extensionDirectory = path.join(rootDirectory, extensionDirectoryPath) + // Resolve user's project as a plugin for loading purposes resolved.push({ - resolve: `${rootDirectory}/dist`, - name: `project-plugin`, - id: createPluginId(`project-plugin`), + resolve: extensionDirectory, + name: MEDUSA_PROJECT_NAME, + id: createPluginId(MEDUSA_PROJECT_NAME), options: configModule, version: createFileContentHash(process.cwd(), `**`), }) @@ -123,15 +132,22 @@ export async function registerPluginModels({ rootDirectory, container, configModule, + extensionDirectoryPath = "dist", + pathGlob = "/models/*.js", }: { rootDirectory: string container: MedusaContainer configModule: ConfigModule + extensionDirectoryPath?: string + pathGlob?: string }): Promise { - const resolved = getResolvedPlugins(rootDirectory, configModule) || [] + const resolved = + getResolvedPlugins(rootDirectory, configModule, extensionDirectoryPath) || + [] + await Promise.all( resolved.map(async (pluginDetails) => { - registerModels(pluginDetails, container) + registerModels(pluginDetails, container, rootDirectory, pathGlob) }) ) } @@ -325,10 +341,12 @@ function registerApi( activityId: string ): Express { const logger = container.resolve("logger") - logger.progress( - activityId, - `Registering custom endpoints for ${pluginDetails.name}` - ) + const projectName = + pluginDetails.name === MEDUSA_PROJECT_NAME + ? "your Medusa project" + : `${pluginDetails.name}` + + logger.progress(activityId, `Registering custom endpoints for ${projectName}`) try { const routes = require(`${pluginDetails.resolve}/api`).default if (routes) { @@ -336,12 +354,16 @@ function registerApi( } return app } catch (err) { - if (err.message !== `Cannot find module '${pluginDetails.resolve}/api'`) { - logger.progress( - activityId, - `No customer endpoints registered for ${pluginDetails.name}` + if (err.code !== "MODULE_NOT_FOUND") { + logger.warn( + `An error occured while registering endpoints in ${projectName}` ) + + if (err.stack) { + logger.warn(`${err.stack}`) + } } + return app } } @@ -550,28 +572,74 @@ function registerRepositories( * version, id, resolved path, etc. See resolvePlugin * @param {object} container - the container where the services will be * registered + * @param rootDirectory + * @param pathGlob * @return {void} */ function registerModels( pluginDetails: PluginDetails, - container: MedusaContainer + container: MedusaContainer, + rootDirectory: string, + pathGlob = "/models/*.js" ): void { - const files = glob.sync(`${pluginDetails.resolve}/models/*.js`, {}) - files.forEach((fn) => { - const loaded = require(fn) as ClassConstructor | EntitySchema + const pluginFullPathGlob = path.join(pluginDetails.resolve, pathGlob) - Object.entries(loaded).map( - ([, val]: [string, ClassConstructor | EntitySchema]) => { - if (typeof val === "function" || val instanceof EntitySchema) { - const name = formatRegistrationName(fn) - container.register({ - [name]: asValue(val), - }) + const modelExtensionsMap = getModelExtensionsMap({ + directory: pluginDetails.resolve, + pathGlob: pathGlob, + config: { register: true }, + }) - container.registerAdd("db_entities", asValue(val)) - } + const pluginModels = glob.sync(pluginFullPathGlob, { + ignore: ["index.js", "index.js.map"], + }) + + const coreModelsFullGlob = path.join(__dirname, "../models/*.js") + const coreModels = glob.sync(coreModelsFullGlob, { + cwd: __dirname, + ignore: ["index.js", "index.ts", "index.js.map"], + }) + + // Apply the extended models to the core models first to ensure that + // when relationships are created, the extended models are used + coreModels.forEach((modelPath) => { + const loaded = require(modelPath) as + | ClassConstructor + | EntitySchema + + if (loaded) { + const name = formatRegistrationName(modelPath) + const mappedExtensionModel = modelExtensionsMap.get(name) + if (mappedExtensionModel) { + const modelName = upperCaseFirst( + formatRegistrationNameWithoutNamespace(modelPath) + ) + + loaded[modelName] = mappedExtensionModel } - ) + } + }) + + pluginModels.forEach((coreOrPluginModelPath) => { + const loaded = require(coreOrPluginModelPath) as + | ClassConstructor + | EntitySchema + + if (loaded) { + Object.entries(loaded).map( + ([, val]: [string, ClassConstructor | EntitySchema]) => { + if (typeof val === "function" || val instanceof EntitySchema) { + const name = formatRegistrationName(coreOrPluginModelPath) + + container.register({ + [name]: asValue(val), + }) + + container.registerAdd("db_entities", asValue(val)) + } + } + ) + } }) } diff --git a/packages/medusa/src/models/shipping-method.ts b/packages/medusa/src/models/shipping-method.ts index 273b701f1919a..cc2bffe4037c5 100644 --- a/packages/medusa/src/models/shipping-method.ts +++ b/packages/medusa/src/models/shipping-method.ts @@ -76,7 +76,7 @@ export class ShippingMethod { @JoinColumn({ name: "return_id" }) return_order: Return - @ManyToOne(() => ShippingOption, { eager: true }) + @ManyToOne(() => ShippingOption) @JoinColumn({ name: "shipping_option_id" }) shipping_option: ShippingOption diff --git a/packages/medusa/src/repositories/cart.ts b/packages/medusa/src/repositories/cart.ts index c70f9b8f04fa8..d60c3d09301cf 100644 --- a/packages/medusa/src/repositories/cart.ts +++ b/packages/medusa/src/repositories/cart.ts @@ -1,8 +1,12 @@ import { objectToStringPath } from "@medusajs/utils" -import { flatten, groupBy, map, merge } from "lodash" -import { FindManyOptions, FindOptionsRelations, In } from "typeorm" +import { FindManyOptions, FindOptionsRelations } from "typeorm" import { dataSource } from "../loaders/database" import { Cart } from "../models" +import { + getGroupedRelations, + mergeEntitiesWithRelations, + queryEntityWithIds, +} from "../utils/repository" export const CartRepository = dataSource.getRepository(Cart).extend({ async findWithRelations( @@ -12,31 +16,17 @@ export const CartRepository = dataSource.getRepository(Cart).extend({ const entities = await this.find(optionsWithoutRelations) const entitiesIds = entities.map(({ id }) => id) - const groupedRelations = {} - for (const rel of objectToStringPath(relations)) { - const [topLevel] = rel.split(".") - if (groupedRelations[topLevel]) { - groupedRelations[topLevel].push(rel) - } else { - groupedRelations[topLevel] = [rel] - } - } + const groupedRelations = getGroupedRelations(objectToStringPath(relations)) - const entitiesIdsWithRelations = await Promise.all( - Object.entries(groupedRelations).map(async ([_, rels]) => { - return this.find({ - where: { id: In(entitiesIds) }, - select: ["id"], - relations: rels as string[], - }) - }) - ).then(flatten) - const entitiesAndRelations = entitiesIdsWithRelations.concat(entities) + const entitiesIdsWithRelations = await queryEntityWithIds({ + repository: this, + entityIds: entitiesIds, + groupedRelations, + select: ["id"], + }) - const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id") - return map(entitiesAndRelationsById, (entityAndRelations) => - merge({}, ...entityAndRelations) - ) + const entitiesAndRelations = entities.concat(entitiesIdsWithRelations) + return mergeEntitiesWithRelations(entitiesAndRelations) }, async findOneWithRelations( diff --git a/packages/medusa/src/repositories/customer-group.ts b/packages/medusa/src/repositories/customer-group.ts index c9a6635a82bd5..f269037eff88c 100644 --- a/packages/medusa/src/repositories/customer-group.ts +++ b/packages/medusa/src/repositories/customer-group.ts @@ -82,9 +82,9 @@ export const CustomerGroupRepository = dataSource : { ...idsOrOptionsWithoutRelations.order } const originalSelect = isOptionsArray ? undefined - : (objectToStringPath( - idsOrOptionsWithoutRelations.select - ) as (keyof CustomerGroup)[]) + : (objectToStringPath(idsOrOptionsWithoutRelations.select, { + includeParentPropertyFields: false, + }) as (keyof CustomerGroup)[]) const clonedOptions = isOptionsArray ? idsOrOptionsWithoutRelations : cloneDeep(idsOrOptionsWithoutRelations) @@ -169,7 +169,7 @@ export const CustomerGroupRepository = dataSource withDeleted, }) - const entitiesAndRelations = entitiesIdsWithRelations.concat(entities) + const entitiesAndRelations = entities.concat(entitiesIdsWithRelations) const entitiesToReturn = mergeEntitiesWithRelations(entitiesAndRelations) diff --git a/packages/medusa/src/repositories/order.ts b/packages/medusa/src/repositories/order.ts index e0e153b1fe869..0ad6bd905a30b 100644 --- a/packages/medusa/src/repositories/order.ts +++ b/packages/medusa/src/repositories/order.ts @@ -1,8 +1,12 @@ import { objectToStringPath } from "@medusajs/utils" -import { flatten, groupBy, map, merge } from "lodash" +import { flatten } from "lodash" import { FindManyOptions, FindOptionsRelations, In } from "typeorm" import { dataSource } from "../loaders/database" import { Order } from "../models" +import { + getGroupedRelations, + mergeEntitiesWithRelations, +} from "../utils/repository" const ITEMS_REL_NAME = "items" const REGION_REL_NAME = "region" @@ -15,15 +19,7 @@ export const OrderRepository = dataSource.getRepository(Order).extend({ const entities = await this.find(optionsWithoutRelations) const entitiesIds = entities.map(({ id }) => id) - const groupedRelations: { [topLevel: string]: string[] } = {} - for (const rel of objectToStringPath(relations)) { - const [topLevel] = rel.split(".") - if (groupedRelations[topLevel]) { - groupedRelations[topLevel].push(rel) - } else { - groupedRelations[topLevel] = [rel] - } - } + const groupedRelations = getGroupedRelations(objectToStringPath(relations)) const entitiesIdsWithRelations = await Promise.all( Object.entries(groupedRelations).map(async ([topLevel, rels]) => { @@ -39,11 +35,8 @@ export const OrderRepository = dataSource.getRepository(Order).extend({ }) ).then(flatten) - const entitiesAndRelations = entitiesIdsWithRelations.concat(entities) - - const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id") - - return map(entities, (e) => merge({}, ...entitiesAndRelationsById[e.id])) + const entitiesAndRelations = entities.concat(entitiesIdsWithRelations) + return mergeEntitiesWithRelations(entitiesAndRelations) }, async findOneWithRelations( diff --git a/packages/medusa/src/repositories/product-category.ts b/packages/medusa/src/repositories/product-category.ts index 537f8d6dedc22..b680e581ba487 100644 --- a/packages/medusa/src/repositories/product-category.ts +++ b/packages/medusa/src/repositories/product-category.ts @@ -1,10 +1,9 @@ import { - Brackets, + DeleteResult, + FindOneOptions, FindOptionsWhere, ILike, - DeleteResult, In, - FindOneOptions, } from "typeorm" import { ProductCategory } from "../models/product-category" import { ExtendedFindConfig, QuerySelector } from "../types/common" @@ -44,7 +43,9 @@ export const ProductCategoryRepository = dataSource const options_ = { ...options } options_.where = options_.where as FindOptionsWhere - const columnsSelected = objectToStringPath(options_.select) + const columnsSelected = objectToStringPath(options_.select, { + includeParentPropertyFields: false, + }) const relationsSelected = objectToStringPath(options_.relations) const fetchSelectColumns = (relationName: string): string[] => { diff --git a/packages/medusa/src/repositories/product.ts b/packages/medusa/src/repositories/product.ts index d11a49f992b86..d9cab503d43a7 100644 --- a/packages/medusa/src/repositories/product.ts +++ b/packages/medusa/src/repositories/product.ts @@ -18,10 +18,11 @@ import { ExtendedFindConfig } from "@medusajs/types" import { applyOrdering, getGroupedRelations, + mergeEntitiesWithRelations, queryEntityWithIds, queryEntityWithoutRelations, } from "../utils/repository" -import { cloneDeep, groupBy, map, merge } from "lodash" +import { cloneDeep } from "lodash" export type DefaultWithoutRelations = Omit< ExtendedFindConfig, @@ -213,11 +214,13 @@ export const ProductRepository = dataSource.getRepository(Product).extend({ customJoinBuilders: [ (queryBuilder, alias, topLevel) => { if (topLevel === "variants") { - queryBuilder.leftJoinAndSelect( - `${alias}.${topLevel}`, - topLevel, - `${topLevel}.deleted_at IS NULL` - ) + const joinMethod = select.filter( + (key) => !!key.match(/^variants\.\w+$/i) + ).length + ? "leftJoin" + : "leftJoinAndSelect" + + queryBuilder[joinMethod](`${alias}.${topLevel}`, topLevel) if ( !Object.keys(order!).some((key) => key.startsWith("variants")) @@ -228,7 +231,8 @@ export const ProductRepository = dataSource.getRepository(Product).extend({ return false } - return true + + return }, (queryBuilder, alias, topLevel) => { if (topLevel === "categories") { @@ -245,7 +249,7 @@ export const ProductRepository = dataSource.getRepository(Product).extend({ return false } - return true + return }, ], }) @@ -479,9 +483,9 @@ export const ProductRepository = dataSource.getRepository(Product).extend({ : { ...idsOrOptionsWithoutRelations.order } const originalSelect = isOptionsArray ? undefined - : (objectToStringPath( - idsOrOptionsWithoutRelations.select - ) as (keyof Product)[]) + : (objectToStringPath(idsOrOptionsWithoutRelations.select, { + includeParentPropertyFields: false, + }) as (keyof Product)[]) const clonedOptions = isOptionsArray ? idsOrOptionsWithoutRelations : cloneDeep(idsOrOptionsWithoutRelations) @@ -543,10 +547,9 @@ export const ProductRepository = dataSource.getRepository(Product).extend({ withDeleted, }) - const entitiesAndRelations = groupBy(entitiesIdsWithRelations, "id") - const entitiesToReturn = map(entitiesIds, (id) => - merge({}, ...entitiesAndRelations[id]) - ) + const entitiesAndRelations = entities.concat(entitiesIdsWithRelations) + const entitiesToReturn = + mergeEntitiesWithRelations(entitiesAndRelations) return [entitiesToReturn, count] }, diff --git a/packages/medusa/src/repositories/tax-rate.ts b/packages/medusa/src/repositories/tax-rate.ts index 254b1077d4c0d..5a323ebe6536a 100644 --- a/packages/medusa/src/repositories/tax-rate.ts +++ b/packages/medusa/src/repositories/tax-rate.ts @@ -34,7 +34,8 @@ export const TaxRateRepository = dataSource.getRepository(TaxRate).extend({ if (isDefined(findOptions.select)) { const selectableCols: (keyof TaxRate)[] = [] const legacySelect = objectToStringPath( - findOptions.select as FindOptionsSelect + findOptions.select as FindOptionsSelect, + { includeParentPropertyFields: false } ) as (keyof TaxRate)[] for (const k of legacySelect) { if (!resolveableFields.includes(k)) { diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index c26370aad61b8..212f0a45c79d9 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -865,7 +865,9 @@ describe("CartService", () => { payment_sessions: true, region: { countries: true }, shipping_address: true, - shipping_methods: true, + shipping_methods: { + shipping_option: true, + }, }), expect.objectContaining({ select: undefined, diff --git a/packages/medusa/src/services/__tests__/swap.ts b/packages/medusa/src/services/__tests__/swap.ts index 9a2419590fe80..a168eae7a35ed 100644 --- a/packages/medusa/src/services/__tests__/swap.ts +++ b/packages/medusa/src/services/__tests__/swap.ts @@ -12,16 +12,12 @@ import { ProductVariantInventoryService, ReturnService, ShippingOptionService, - TotalsService + TotalsService, } from "../index" import LineItemAdjustmentService from "../line-item-adjustment" import SwapService from "../swap" -import { - LineItemAdjustmentServiceMock -} from "../__mocks__/line-item-adjustment" -import { - ProductVariantInventoryServiceMock -} from "../__mocks__/product-variant-inventory" +import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment" +import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant-inventory" /* ******************** DEFAULT REPOSITORY MOCKS ******************** */ @@ -273,32 +269,33 @@ describe("SwapService", () => { expect(swapRepo.findOne).toHaveBeenCalledWith({ relations: { additional_items: { - variant: true + variant: true, }, order: { claims: { - additional_items: true + additional_items: true, }, discounts: { - rule: true + rule: true, }, items: { variant: { - product: true - } + product: true, + }, }, swaps: { - additional_items: true - } + additional_items: true, + }, }, return_order: { items: true, shipping_method: { - tax_lines: true - } - } + shipping_option: true, + tax_lines: true, + }, + }, }, - where: { id: IdMap.getId("swap-1") } + where: { id: IdMap.getId("swap-1") }, }) expect(lineItemService.createReturnLines).toHaveBeenCalledTimes(1) @@ -413,15 +410,13 @@ describe("SwapService", () => { describe("success", () => { const lineItemService = { - generate: jest - .fn() - .mockImplementation(({ variantId, quantity }) => { - return { - unit_price: 100, - variant_id: variantId, - quantity, - } - }), + generate: jest.fn().mockImplementation(({ variantId, quantity }) => { + return { + unit_price: 100, + variant_id: variantId, + quantity, + } + }), retrieve: () => Promise.resolve({}), list: () => Promise.resolve([]), withTransaction: function () { @@ -457,13 +452,16 @@ describe("SwapService", () => { ) expect(lineItemService.generate).toHaveBeenCalledTimes(1) - expect(lineItemService.generate).toHaveBeenCalledWith({ - quantity: 1, - variantId: IdMap.getId("new-variant") - }, { - "cart": undefined, - region_id: IdMap.getId("region") - }) + expect(lineItemService.generate).toHaveBeenCalledWith( + { + quantity: 1, + variantId: IdMap.getId("new-variant"), + }, + { + cart: undefined, + region_id: IdMap.getId("region"), + } + ) }) it("creates swap", async () => { @@ -605,7 +603,7 @@ describe("SwapService", () => { shipping_methods: existing.shipping_methods, }, [{ item_id: "1234", quantity: 2 }], - { swap_id: IdMap.getId("swap"), metadata: {} }, + { swap_id: IdMap.getId("swap"), metadata: {} } ) }) }) @@ -798,8 +796,7 @@ describe("SwapService", () => { describe("failure", () => { const swapRepo = MockRepository({ - findOne: () => - Promise.resolve({ canceled_at: new Date() }), + findOne: () => Promise.resolve({ canceled_at: new Date() }), }) const swapService = new SwapService({ diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index 2bcdaa4756e97..1abd23d29fd5e 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -1004,7 +1004,7 @@ class CartService extends TransactionBaseService { * shipping discount * If a free shipping is present, we set shipping methods price to 0 * if a free shipping was present, we set shipping methods to original amount - * @param cart - the the cart to adjust free shipping for + * @param cart - the cart to adjust free shipping for * @param shouldAdd - flag to indicate, if we should add or remove * @return void */ @@ -1058,6 +1058,7 @@ class CartService extends TransactionBaseService { "items.variant", "items.variant.product", "shipping_methods", + "shipping_methods.shipping_option", "shipping_address", "billing_address", "gift_cards", @@ -1530,6 +1531,7 @@ class CartService extends TransactionBaseService { "discounts.rule", "payment_sessions", "shipping_methods", + "shipping_methods.shipping_option", ], }) @@ -1829,6 +1831,7 @@ class CartService extends TransactionBaseService { "discounts.rule", "gift_cards", "shipping_methods", + "shipping_methods.shipping_option", "billing_address", "shipping_address", "region", @@ -2170,7 +2173,12 @@ class CartService extends TransactionBaseService { } const updatedCart = await this.retrieve(cart.id, { - relations: ["discounts", "discounts.rule", "shipping_methods"], + relations: [ + "discounts", + "discounts.rule", + "shipping_methods", + "shipping_methods.shipping_option", + ], }) // if cart has freeshipping, adjust price @@ -2537,6 +2545,7 @@ class CartService extends TransactionBaseService { "region.tax_rates", "shipping_address", "shipping_methods", + "shipping_methods.shipping_option", ], }) @@ -2559,6 +2568,7 @@ class CartService extends TransactionBaseService { "items", "items.tax_lines", "shipping_methods", + "shipping_methods.shipping_option", "shipping_methods.tax_lines", ], }) diff --git a/packages/medusa/src/services/claim.ts b/packages/medusa/src/services/claim.ts index dee72aab6b864..978ced59b2aef 100644 --- a/packages/medusa/src/services/claim.ts +++ b/packages/medusa/src/services/claim.ts @@ -530,6 +530,7 @@ export default class ClaimService extends TransactionBaseService { "additional_items.variant", "additional_items.variant.product", "shipping_methods", + "shipping_methods.shipping_option", "shipping_methods.tax_lines", "shipping_address", "order", diff --git a/packages/medusa/src/services/order-edit.ts b/packages/medusa/src/services/order-edit.ts index 8280931ca28a8..af3ae17b43548 100644 --- a/packages/medusa/src/services/order-edit.ts +++ b/packages/medusa/src/services/order-edit.ts @@ -514,6 +514,7 @@ export default class OrderEditService extends TransactionBaseService { "items.variant", "region.tax_rates", "shipping_methods", + "shipping_methods.shipping_option", "shipping_methods.tax_lines", ], }) diff --git a/packages/medusa/src/services/return.ts b/packages/medusa/src/services/return.ts index 985bf07a7d1fc..d9231288e6064 100644 --- a/packages/medusa/src/services/return.ts +++ b/packages/medusa/src/services/return.ts @@ -596,6 +596,7 @@ class ReturnService extends TransactionBaseService { "discounts.rule", "refunds", "shipping_methods", + "shipping_methods.shipping_option", "region", "swaps", "swaps.additional_items", diff --git a/packages/medusa/src/services/swap.ts b/packages/medusa/src/services/swap.ts index f083e16eed1bc..6c01b214950fb 100644 --- a/packages/medusa/src/services/swap.ts +++ b/packages/medusa/src/services/swap.ts @@ -29,7 +29,7 @@ import { } from "./index" import { EntityManager, In } from "typeorm" import { FindConfig, Selector, WithRequiredProperty } from "../types/common" -import { MedusaError, isDefined } from "medusa-core-utils" +import { isDefined, MedusaError } from "medusa-core-utils" import { buildQuery, setMetadata, validateId } from "../utils" import { CreateShipmentConfig } from "../types/fulfillment" @@ -579,6 +579,7 @@ class SwapService extends TransactionBaseService { "return_order", "return_order.items", "return_order.shipping_method", + "return_order.shipping_method.shipping_option", "return_order.shipping_method.tax_lines", ], }) @@ -918,6 +919,7 @@ class SwapService extends TransactionBaseService { "additional_items.variant", "additional_items.variant.product", "shipping_methods", + "shipping_methods.shipping_option", "shipping_methods.tax_lines", "order", "order.region", diff --git a/packages/medusa/src/strategies/cart-completion.ts b/packages/medusa/src/strategies/cart-completion.ts index c14411c5a470d..e4d4bfdbaf73c 100644 --- a/packages/medusa/src/strategies/cart-completion.ts +++ b/packages/medusa/src/strategies/cart-completion.ts @@ -197,6 +197,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy { "region.tax_rates", "shipping_address", "shipping_methods", + "shipping_methods.shipping_option", ], }) diff --git a/packages/medusa/src/types/global.ts b/packages/medusa/src/types/global.ts index 9218ae2234d04..9c4f9e0541132 100644 --- a/packages/medusa/src/types/global.ts +++ b/packages/medusa/src/types/global.ts @@ -1,7 +1,6 @@ import { CommonTypes } from "@medusajs/types" import { Request } from "express" import { MedusaContainer as coreMedusaContainer } from "medusa-core-utils" -import { Logger as _Logger } from "winston" import { Customer, User } from "../models" import { FindConfig, RequestQueryFields } from "./common" @@ -31,10 +30,20 @@ export type ClassConstructor = { export type MedusaContainer = coreMedusaContainer -export type Logger = _Logger & { - progress: (activityId: string, msg: string) => void - info: (msg: string) => void - warn: (msg: string) => void +export type Logger = { + panic: (data) => void + shouldLog: (level: string) => void + setLogLevel: (level: string) => void + unsetLogLevel: () => void + activity: (message: string, config?) => void + progress: (activityId, message) => void + error: (messageOrError, error?) => void + failure: (activityId, message) => void + success: (activityId, message) => void + debug: (message) => void + info: (message) => void + warn: (message) => void + log: (...args) => void } export type Constructor = new (...args: any[]) => T diff --git a/packages/medusa/src/types/orders.ts b/packages/medusa/src/types/orders.ts index de03e475888d2..c3d99240c4f7c 100644 --- a/packages/medusa/src/types/orders.ts +++ b/packages/medusa/src/types/orders.ts @@ -133,9 +133,12 @@ export const defaultAdminOrdersRelations = [ "returns.items", "returns.items.reason", "returns.shipping_method", + "returns.shipping_method.shipping_option", "returns.shipping_method.tax_lines", "shipping_address", "shipping_methods", + "shipping_methods.shipping_option", + "shipping_methods.tax_lines", "swaps", "swaps.additional_items", "swaps.additional_items.variant", @@ -144,9 +147,11 @@ export const defaultAdminOrdersRelations = [ "swaps.payment", "swaps.return_order", "swaps.return_order.shipping_method", + "swaps.return_order.shipping_method.shipping_option", "swaps.return_order.shipping_method.tax_lines", "swaps.shipping_address", "swaps.shipping_methods", + "swaps.shipping_methods.shipping_option", "swaps.shipping_methods.tax_lines", // "claims.claim_items.tags", ] diff --git a/packages/medusa/src/utils/__tests__/format-registration-name.js b/packages/medusa/src/utils/__tests__/format-registration-name.js index 0403c36fb003c..e9ac9d1aeba59 100644 --- a/packages/medusa/src/utils/__tests__/format-registration-name.js +++ b/packages/medusa/src/utils/__tests__/format-registration-name.js @@ -1,5 +1,8 @@ import path from "path" -import formatRegistrationName from "../format-registration-name" +import { + formatRegistrationName, + formatRegistrationNameWithoutNamespace, +} from "../format-registration-name" describe("formatRegistrationName", () => { const tests = [ @@ -32,3 +35,26 @@ describe("formatRegistrationName", () => { expect(res).toEqual(expected) }) }) + +describe("formatRegistrationNameWithoutNamespace", () => { + const tests = [ + [["medusa-test-dir", "dist", "services", "my-test.js"], "myTest"], + [["medusa-test-dir", "dist", "services", "my.js"], "my"], + [["services", "my-quite-long-file.js"], "myQuiteLongFile"], + [["/", "Users", "seb", "com.medusa.js", "services", "dot.js"], "dot"], + [["/", "Users", "seb.rin", "com.medusa.js", "services", "dot.js"], "dot"], + [ + ["/", "Users", "seb.rin", "com.medusa.js", "repositories", "dot.js"], + "dot", + ], + [["/", "Users", "seb.rin", "com.medusa.js", "models", "dot.js"], "dot"], + [["C:", "server", "services", "dot.js"], "dot"], + ] + + test.each( + tests.map(([pathParts, expected]) => [path.join(...pathParts), expected]) + )("Service %s -> %s", (fn, expected) => { + const res = formatRegistrationNameWithoutNamespace(fn) + expect(res).toEqual(expected) + }) +}) diff --git a/packages/medusa/src/utils/format-registration-name.ts b/packages/medusa/src/utils/format-registration-name.ts index 3c65202cfa259..0831ed959a134 100644 --- a/packages/medusa/src/utils/format-registration-name.ts +++ b/packages/medusa/src/utils/format-registration-name.ts @@ -1,4 +1,5 @@ import { parse } from "path" +import { toCamelCase, upperCaseFirst } from "@medusajs/utils" /** * Formats a filename into the correct container resolution name. @@ -7,41 +8,39 @@ import { parse } from "path" * @param path - the full path of the file * @return the formatted name */ -function formatRegistrationName(path: string): string { +export function formatRegistrationName(path: string): string { const parsed = parse(path) const parsedDir = parse(parsed.dir) - const rawname = parsed.name - let namespace = parsedDir.name - if (namespace.startsWith("__")) { + let directoryNamespace = parsedDir.name + + if (directoryNamespace.startsWith("__")) { const parsedCoreDir = parse(parsedDir.dir) - namespace = parsedCoreDir.name + directoryNamespace = parsedCoreDir.name } - switch (namespace) { + switch (directoryNamespace) { // We strip the last character when adding the type of registration // this is a trick for plural "ies" case "repositories": - namespace = "repositorys" + directoryNamespace = "repositorys" break case "strategies": - namespace = "strategys" + directoryNamespace = "strategys" break default: break } - const upperNamespace = - namespace.charAt(0).toUpperCase() + namespace.slice(1, -1) + const upperNamespace = upperCaseFirst(directoryNamespace.slice(0, -1)) - const parts = rawname.split("-").map((n, index) => { - if (index !== 0) { - return n.charAt(0).toUpperCase() + n.slice(1) - } - return n - }) + return formatRegistrationNameWithoutNamespace(path) + upperNamespace +} + +export function formatRegistrationNameWithoutNamespace(path: string): string { + const parsed = parse(path) - return parts.join("") + upperNamespace + return toCamelCase(parsed.name) } export default formatRegistrationName diff --git a/packages/medusa/src/utils/repository.ts b/packages/medusa/src/utils/repository.ts index b20f718a3c596..3f51d0dde7659 100644 --- a/packages/medusa/src/utils/repository.ts +++ b/packages/medusa/src/utils/repository.ts @@ -7,6 +7,11 @@ import { } from "typeorm" import { ExtendedFindConfig } from "../types/common" +// Regex matches all '.' except the rightmost +export const positiveLookaheadDotReplacer = new RegExp(/\.(?=[^.]*\.)/, "g") +// Replace all '.' with '__' to avoid typeorm's automatic aliasing +export const dotReplacer = new RegExp(/\./, "g") + /** * Custom query entity, it is part of the creation of a custom findWithRelationsAndCount needs. * Allow to query the relations for the specified entity ids @@ -35,62 +40,86 @@ export async function queryEntityWithIds({ qb: SelectQueryBuilder, alias: string, toplevel: string - ) => boolean)[] + ) => false | undefined)[] }): Promise { const alias = repository.metadata.name.toLowerCase() return await Promise.all( - Object.entries(groupedRelations).map(async ([toplevel, rels]) => { - let querybuilder = repository.createQueryBuilder(alias) + Object.entries(groupedRelations).map( + async ([toplevel, topLevelRelations]) => { + let querybuilder = repository.createQueryBuilder(alias) - if (select && select.length) { - querybuilder.select(select.map((f) => `${alias}.${f as string}`)) - } + if (select?.length) { + querybuilder.select( + (select as string[]) + .filter(function (s) { + return s.startsWith(toplevel) || !s.includes(".") + }) + .map((column) => { + // In case the column is the toplevel relation, we need to replace the dot with a double underscore if it also contains top level relations + if (column.includes(toplevel)) { + return topLevelRelations.some((rel) => column.includes(rel)) + ? column.replace(positiveLookaheadDotReplacer, "__") + : column + } + return `${alias}.${column}` + }) + ) + } - let shouldAttachDefault = true - for (const customJoinBuilder of customJoinBuilders) { - const result = customJoinBuilder(querybuilder, alias, toplevel) - shouldAttachDefault = shouldAttachDefault && result - } + let shouldAttachDefault: boolean | undefined = true + for (const customJoinBuilder of customJoinBuilders) { + const result = customJoinBuilder(querybuilder, alias, toplevel) + if (result === undefined) { + continue + } - // If the toplevel relation has been attached with a customJoinBuilder and the function return false then - // do not attach the toplevel join bellow. - if (shouldAttachDefault) { - querybuilder = querybuilder.leftJoinAndSelect( - `${alias}.${toplevel}`, - toplevel - ) - } + shouldAttachDefault = shouldAttachDefault && result + } - for (const rel of rels) { - const [_, rest] = rel.split(".") - if (!rest) { - continue + if (shouldAttachDefault) { + const regexp = new RegExp(`^${toplevel}\\.\\w+$`) + const joinMethod = (select as string[]).filter( + (key) => !!key.match(regexp) + ).length + ? "leftJoin" + : "leftJoinAndSelect" + + querybuilder = querybuilder[joinMethod]( + `${alias}.${toplevel}`, + toplevel + ) } - querybuilder = querybuilder.leftJoinAndSelect( - // Regex matches all '.' except the rightmost - rel.replace(/\.(?=[^.]*\.)/g, "__"), - // Replace all '.' with '__' to avoid typeorm's automatic aliasing - rel.replace(/\./g, "__") - ) - } - if (withDeleted) { - querybuilder = querybuilder - .where(`${alias}.id IN (:...entitiesIds)`, { - entitiesIds: entityIds, - }) - .withDeleted() - } else { - querybuilder = querybuilder.where( - `${alias}.deleted_at IS NULL AND ${alias}.id IN (:...entitiesIds)`, - { - entitiesIds: entityIds, + for (const rel of topLevelRelations) { + const [_, rest] = rel.split(".") + if (!rest) { + continue } - ) - } - return querybuilder.getMany() - }) + const regexp = new RegExp(`^${rel}\\.\\w+$`) + const joinMethod = (select as string[]).filter( + (key) => !!key.match(regexp) + ).length + ? "leftJoin" + : "leftJoinAndSelect" + + querybuilder = querybuilder[joinMethod]( + rel.replace(positiveLookaheadDotReplacer, "__"), + rel.replace(dotReplacer, "__") + ) + } + + querybuilder = querybuilder.where(`${alias}.id IN (:...entitiesIds)`, { + entitiesIds: entityIds, + }) + + if (withDeleted) { + querybuilder.withDeleted() + } + + return querybuilder.getMany() + } + ) ).then(flatten) } diff --git a/packages/orchestration/jest.config.js b/packages/orchestration/jest.config.js new file mode 100644 index 0000000000000..7de5bf104a01f --- /dev/null +++ b/packages/orchestration/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + globals: { + "ts-jest": { + tsConfig: "tsconfig.json", + isolatedModules: false, + }, + }, + transform: { + "^.+\\.[jt]s?$": "ts-jest", + }, + testEnvironment: `node`, + moduleFileExtensions: [`js`, `ts`], +} diff --git a/packages/orchestration/package.json b/packages/orchestration/package.json new file mode 100644 index 0000000000000..6c14321aaeb34 --- /dev/null +++ b/packages/orchestration/package.json @@ -0,0 +1,36 @@ +{ + "name": "@medusajs/orchestration", + "version": "0.0.1", + "description": "Medusa utilities to orchestrate modules", + "main": "dist/index.js", + "repository": { + "type": "git", + "url": "https://github.com/medusajs/medusa", + "directory": "packages/orchestration" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "author": "Medusa", + "license": "MIT", + "devDependencies": { + "@medusajs/types": "^1.8.7", + "cross-env": "^5.2.1", + "jest": "^25.5.4", + "ts-jest": "^25.5.1", + "typescript": "^4.4.4" + }, + "dependencies": { + "@medusajs/utils": "^1.9.1", + "graphql": "^16.6.0" + }, + "scripts": { + "prepare": "cross-env NODE_ENV=production yarn run build", + "build": "tsc --build", + "watch": "tsc --build --watch", + "test": "jest" + } +} diff --git a/packages/orchestration/src/__fixtures__/joiner/data.ts b/packages/orchestration/src/__fixtures__/joiner/data.ts new file mode 100644 index 0000000000000..9ce6aadab140a --- /dev/null +++ b/packages/orchestration/src/__fixtures__/joiner/data.ts @@ -0,0 +1,189 @@ +export const remoteJoinerData = { + user: [ + { + id: 1, + email: "johndoe@example.com", + name: "John Doe", + fullname: "John Doe full name", + products: [ + { + id: 1, + product_id: 102, + }, + ], + nested: { + lala: "lala", + multiple: [ + { + abc: 1, + }, + { + abc: 2, + }, + ], + }, + }, + { + id: 2, + email: "janedoe@example.com", + name: "Jane Doe", + products: [ + { + id: 2, + product_id: [101, 102], + }, + ], + nested: { + lala: "lele", + multiple: [ + { + a: 33, + }, + { + a: 44, + }, + ], + }, + }, + { + id: 3, + email: "aaa@example.com", + name: "aaa bbb", + fullname: "3333 Doe full name", + nested: { + lala: "lolo", + multiple: [ + { + a: 555, + }, + { + a: 555, + }, + ], + }, + }, + { + id: 4, + email: "444444@example.com", + name: "a4444 44 44", + fullname: "444 Doe full name", + products: [ + { + id: 4, + product_id: 103, + }, + ], + nested: { + lala: "lulu", + multiple: [ + { + a: 6666, + }, + { + a: 7777, + }, + ], + }, + }, + ], + product: { + rows: [ + { + id: 101, + name: "Product 1", + handler: "product-1-handler", + user_id: 2, + }, + { + id: 102, + name: "Product 2", + handler: "product-2-handler", + user_id: 1, + }, + { + id: 103, + name: "Product 3", + handler: "product-3-handler", + user_id: 3, + }, + ], + limit: 3, + skip: 0, + }, + variant: [ + { + id: 991, + name: "Product variant 1", + product_id: 101, + }, + { + id: 992, + name: "Product variant 2", + product_id: 101, + }, + { + id: 993, + name: "Product variant 33", + product_id: 103, + }, + ], + order_variant: [ + { + order_id: 201, + product_id: 101, + variant_id: 991, + quantity: 1, + }, + { + order_id: 201, + product_id: 101, + variant_id: 992, + quantity: 5, + }, + { + order_id: 205, + product_id: 101, + variant_id: 992, + quantity: 4, + }, + { + order_id: 205, + product_id: 103, + variant_id: 993, + quantity: 1, + }, + ], + order: [ + { + id: 201, + number: "ORD-001", + date: "2023-04-01T12:00:00Z", + products: [ + { + product_id: 101, + variant_id: 991, + quantity: 1, + }, + { + product_id: 101, + variant_id: 992, + quantity: 5, + }, + ], + user_id: 4, + }, + { + id: 205, + number: "ORD-202", + date: "2023-04-01T12:00:00Z", + products: [ + { + product_id: [101, 103], + variant_id: 993, + quantity: 4, + }, + ], + user_id: 1, + }, + ], +} diff --git a/packages/orchestration/src/__mocks__/joiner/mock_data.ts b/packages/orchestration/src/__mocks__/joiner/mock_data.ts new file mode 100644 index 0000000000000..75ccb7a11bbe9 --- /dev/null +++ b/packages/orchestration/src/__mocks__/joiner/mock_data.ts @@ -0,0 +1,118 @@ +import { JoinerServiceConfig } from "@medusajs/types" +import { remoteJoinerData } from "./../../__fixtures__/joiner/data" + +export const serviceConfigs: JoinerServiceConfig[] = [ + { + serviceName: "User", + primaryKeys: ["id"], + relationships: [ + { + foreignKey: "products.product_id", + serviceName: "Product", + primaryKey: "id", + alias: "product", + }, + ], + extends: [ + { + serviceName: "Variant", + resolve: { + foreignKey: "user_id", + serviceName: "User", + primaryKey: "id", + alias: "user", + }, + }, + ], + }, + { + serviceName: "Product", + primaryKeys: ["id", "sku"], + relationships: [ + { + foreignKey: "user_id", + serviceName: "User", + primaryKey: "id", + alias: "user", + }, + ], + }, + { + serviceName: "Variant", + primaryKeys: ["id"], + relationships: [ + { + foreignKey: "product_id", + serviceName: "Product", + primaryKey: "id", + alias: "product", + }, + { + foreignKey: "variant_id", + primaryKey: "id", + serviceName: "Order", + alias: "orders", + inverse: true, // In an inverted relationship the foreign key is on Order and the primary key is on variant + }, + ], + }, + { + serviceName: "Order", + primaryKeys: ["id"], + relationships: [ + { + foreignKey: "product_id", + serviceName: "Product", + primaryKey: "id", + alias: "product", + }, + { + foreignKey: "products.variant_id,product_id", + serviceName: "Variant", + primaryKey: "id,product_id", + alias: "variant", + }, + { + foreignKey: "user_id", + serviceName: "User", + primaryKey: "id", + alias: "user", + }, + ], + }, +] + +export const mockServiceList = (serviceName) => { + return jest.fn().mockImplementation((data) => { + const src = { + userService: remoteJoinerData.user, + productService: remoteJoinerData.product, + variantService: remoteJoinerData.variant, + orderService: remoteJoinerData.order, + } + + let resultset = JSON.parse(JSON.stringify(src[serviceName])) + + if ( + serviceName === "userService" && + !data.fields?.some((field) => field.includes("multiple")) + ) { + resultset = resultset.map((item) => { + delete item.nested.multiple + return item + }) + } + + return { + data: resultset, + path: serviceName === "productService" ? "rows" : undefined, + } + }) +} + +export const serviceMock = { + orderService: mockServiceList("orderService"), + userService: mockServiceList("userService"), + productService: mockServiceList("productService"), + variantService: mockServiceList("variantService"), +} diff --git a/packages/orchestration/src/__tests__/joiner/graphql-ast.ts b/packages/orchestration/src/__tests__/joiner/graphql-ast.ts new file mode 100644 index 0000000000000..8ce56afd0aca6 --- /dev/null +++ b/packages/orchestration/src/__tests__/joiner/graphql-ast.ts @@ -0,0 +1,239 @@ +import GraphQLParser from "../../joiner/graphql-ast" + +describe("RemoteJoiner.parseQuery", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it("Simple query with fields", async () => { + const graphqlQuery = ` + query { + order { + id + number + date + } + } + ` + const parser = new GraphQLParser(graphqlQuery) + const rjQuery = parser.parseQuery() + + expect(rjQuery).toEqual({ + service: "order", + fields: ["id", "number", "date"], + expands: [], + }) + }) + + it("Simple query with fields and arguments", async () => { + const graphqlQuery = ` + query { + order( + id: "ord_123", + another_arg: 987, + complexArg: { + id: "123", + name: "test", + nestedArg: { + nest_id: "abc", + num: 123 + } + } + ) { + id + number + date + } + } + ` + const parser = new GraphQLParser(graphqlQuery) + const rjQuery = parser.parseQuery() + + expect(rjQuery).toEqual({ + service: "order", + fields: ["id", "number", "date"], + expands: [], + args: [ + { + name: "id", + value: "ord_123", + }, + { + name: "another_arg", + value: 987, + }, + { + name: "complexArg", + value: { + id: "123", + name: "test", + nestedArg: { + nest_id: "abc", + num: 123, + }, + }, + }, + ], + }) + }) + + it("Nested query with fields", async () => { + const graphqlQuery = ` + query { + order { + id + number + date + products { + product_id + variant_id + order + variant { + name + sku + } + } + } + } + ` + const parser = new GraphQLParser(graphqlQuery) + const rjQuery = parser.parseQuery() + + expect(rjQuery).toEqual({ + service: "order", + fields: ["id", "number", "date", "products"], + expands: [ + { + property: "products", + fields: ["product_id", "variant_id", "order", "variant"], + }, + { + property: "products.variant", + fields: ["name", "sku"], + }, + ], + }) + }) + + it("Nested query with fields and arguments", async () => { + const graphqlQuery = ` + query { + order (order_id: "ord_123") { + id + number + date + products (limit: 10) { + product_id + variant_id + order + variant (complexArg: { id: "123", name: "test", nestedArg: { nest_id: "abc", num: 123 } }, region_id: "reg_123") { + name + sku + } + } + } + } + ` + const parser = new GraphQLParser(graphqlQuery) + const rjQuery = parser.parseQuery() + + expect(rjQuery).toEqual({ + service: "order", + fields: ["id", "number", "date", "products"], + expands: [ + { + property: "products", + fields: ["product_id", "variant_id", "order", "variant"], + args: [ + { + name: "limit", + value: 10, + }, + ], + }, + { + property: "products.variant", + fields: ["name", "sku"], + args: [ + { + name: "complexArg", + value: { + id: "123", + name: "test", + nestedArg: { + nest_id: "abc", + num: 123, + }, + }, + }, + { + name: "region_id", + value: "reg_123", + }, + ], + }, + ], + args: [ + { + name: "order_id", + value: "ord_123", + }, + ], + }) + }) + + it("Nested query with fields and arguments using variables", async () => { + const graphqlQuery = ` + query($orderId: ID, $anotherArg: String, $randomVariable: nonValidatedType) { + order (order_id: $orderId, anotherArg: $anotherArg) { + id + number + date + products (randomValue: $randomVariable) { + product_id + variant_id + order + } + } + } + ` + const parser = new GraphQLParser(graphqlQuery, { + orderId: 123, + randomVariable: { complex: { num: 12343, str: "str_123" } }, + anotherArg: "any string", + }) + const rjQuery = parser.parseQuery() + + expect(rjQuery).toEqual({ + service: "order", + fields: ["id", "number", "date", "products"], + expands: [ + { + property: "products", + fields: ["product_id", "variant_id", "order"], + args: [ + { + name: "randomValue", + value: { + complex: { + num: 12343, + str: "str_123", + }, + }, + }, + ], + }, + ], + args: [ + { + name: "order_id", + value: 123, + }, + { + name: "anotherArg", + value: "any string", + }, + ], + }) + }) +}) diff --git a/packages/orchestration/src/__tests__/joiner/remote-joiner-data.ts b/packages/orchestration/src/__tests__/joiner/remote-joiner-data.ts new file mode 100644 index 0000000000000..cd2b20ae38d9a --- /dev/null +++ b/packages/orchestration/src/__tests__/joiner/remote-joiner-data.ts @@ -0,0 +1,491 @@ +import { MedusaContainer, RemoteExpandProperty } from "@medusajs/types" +import { lowerCaseFirst, toPascalCase } from "@medusajs/utils" +import { remoteJoinerData } from "../../__fixtures__/joiner/data" +import { serviceConfigs, serviceMock } from "../../__mocks__/joiner/mock_data" +import { RemoteJoiner } from "../../joiner" + +const container = { + resolve: (serviceName) => { + return { + list: (...args) => { + return serviceMock[serviceName].apply(this, args) + }, + getByVariantId: (options) => { + if (serviceName !== "orderService") { + return + } + + let orderVar = JSON.parse( + JSON.stringify(remoteJoinerData.order_variant) + ) + + if (options.expands?.order) { + orderVar = orderVar.map((item) => { + item.order = JSON.parse( + JSON.stringify( + remoteJoinerData.order.find((o) => o.id === item.order_id) + ) + ) + return item + }) + } + + return { + data: orderVar, + } + }, + } + }, +} as MedusaContainer + +const fetchServiceDataCallback = async ( + expand: RemoteExpandProperty, + pkField: string, + ids?: (unknown | unknown[])[], + relationship?: any +) => { + const serviceConfig = expand.serviceConfig + const moduleRegistryName = + lowerCaseFirst(serviceConfig.serviceName) + "Service" + + const service = container.resolve(moduleRegistryName) + const methodName = relationship?.inverse + ? `getBy${toPascalCase(pkField)}` + : "list" + + return await service[methodName]({ + fields: expand.fields, + args: expand.args, + expands: expand.expands, + options: { + [pkField]: ids, + }, + }) +} + +describe("RemoteJoiner", () => { + let joiner: RemoteJoiner + beforeAll(() => { + joiner = new RemoteJoiner(serviceConfigs, fetchServiceDataCallback) + }) + beforeEach(() => { + jest.clearAllMocks() + }) + + it("Simple query of a service, its id and no fields specified", async () => { + const query = { + service: "User", + args: [ + { + name: "id", + value: "1", + }, + ], + fields: ["id", "name", "email"], + } + + const data = await joiner.query(query) + + expect(data).toEqual([ + { + id: 1, + name: "John Doe", + email: "johndoe@example.com", + }, + { + id: 2, + name: "Jane Doe", + email: "janedoe@example.com", + }, + { + id: 3, + name: "aaa bbb", + email: "aaa@example.com", + }, + { + id: 4, + name: "a4444 44 44", + email: "444444@example.com", + }, + ]) + }) + + it("Simple query of a service where the returned data contains multiple properties", async () => { + const query = RemoteJoiner.parseQuery(` + query { + product { + id + name + } + } + `) + const data = await joiner.query(query) + + expect(data).toEqual({ + rows: [ + { + id: 101, + name: "Product 1", + }, + { + id: 102, + name: "Product 2", + }, + { + id: 103, + name: "Product 3", + }, + ], + limit: 3, + skip: 0, + }) + }) + + it("Query of a service, expanding a property and restricting the fields expanded", async () => { + const query = { + service: "User", + args: [ + { + name: "id", + value: "1", + }, + ], + fields: ["username", "email", "products"], + expands: [ + { + property: "products.product", + fields: ["name"], + }, + ], + } + + const data = await joiner.query(query) + + expect(data).toEqual([ + { + email: "johndoe@example.com", + products: [ + { + id: 1, + product_id: 102, + product: { + name: "Product 2", + id: 102, + }, + }, + ], + }, + { + email: "janedoe@example.com", + products: [ + { + id: 2, + product_id: [101, 102], + product: [ + { + name: "Product 1", + id: 101, + }, + { + name: "Product 2", + id: 102, + }, + ], + }, + ], + }, + { + email: "aaa@example.com", + }, + { + email: "444444@example.com", + products: [ + { + id: 4, + product_id: 103, + product: { + name: "Product 3", + id: 103, + }, + }, + ], + }, + ]) + }) + + it("Query a service expanding multiple nested properties", async () => { + const query = { + service: "Order", + fields: ["number", "date", "products"], + expands: [ + { + property: "products", + fields: ["product"], + }, + { + property: "products.product", + fields: ["name"], + }, + { + property: "user", + fields: ["fullname", "email", "products"], + }, + { + property: "user.products.product", + fields: ["name"], + }, + ], + args: [ + { + name: "id", + value: "3", + }, + ], + } + + const data = await joiner.query(query) + + expect(data).toEqual([ + { + number: "ORD-001", + date: "2023-04-01T12:00:00Z", + products: [ + { + product_id: 101, + product: { + name: "Product 1", + id: 101, + }, + }, + { + product_id: 101, + product: { + name: "Product 1", + id: 101, + }, + }, + ], + user_id: 4, + user: { + fullname: "444 Doe full name", + email: "444444@example.com", + products: [ + { + id: 4, + product_id: 103, + product: { + name: "Product 3", + id: 103, + }, + }, + ], + id: 4, + }, + }, + { + number: "ORD-202", + date: "2023-04-01T12:00:00Z", + products: [ + { + product_id: [101, 103], + product: [ + { + name: "Product 1", + id: 101, + }, + { + name: "Product 3", + id: 103, + }, + ], + }, + ], + user_id: 1, + user: { + fullname: "John Doe full name", + email: "johndoe@example.com", + products: [ + { + id: 1, + product_id: 102, + product: { + name: "Product 2", + id: 102, + }, + }, + ], + id: 1, + }, + }, + ]) + }) + + it("Query a service expanding an inverse relation", async () => { + const query = RemoteJoiner.parseQuery(` + query { + variant { + id + name + orders { + order { + number + products { + quantity + product { + name + } + variant { + name + } + } + } + } + } + } + `) + const data = await joiner.query(query) + + expect(data).toEqual([ + { + id: 991, + name: "Product variant 1", + orders: { + order: { + number: "ORD-001", + products: [ + { + product_id: 101, + variant_id: 991, + quantity: 1, + product: { + name: "Product 1", + id: 101, + }, + variant: { + name: "Product variant 1", + id: 991, + product_id: 101, + }, + }, + { + product_id: 101, + variant_id: 992, + quantity: 5, + product: { + name: "Product 1", + id: 101, + }, + variant: { + name: "Product variant 2", + id: 992, + product_id: 101, + }, + }, + ], + id: 201, + }, + variant_id: 991, + order_id: 201, + }, + }, + { + id: 992, + name: "Product variant 2", + orders: [ + { + order: { + number: "ORD-001", + products: [ + { + product_id: 101, + variant_id: 991, + quantity: 1, + product: { + name: "Product 1", + id: 101, + }, + variant: { + name: "Product variant 1", + id: 991, + product_id: 101, + }, + }, + { + product_id: 101, + variant_id: 992, + quantity: 5, + product: { + name: "Product 1", + id: 101, + }, + variant: { + name: "Product variant 2", + id: 992, + product_id: 101, + }, + }, + ], + id: 201, + }, + variant_id: 992, + order_id: 201, + }, + { + order: { + number: "ORD-202", + products: [ + { + product_id: [101, 103], + variant_id: 993, + quantity: 4, + product: [ + { + name: "Product 1", + id: 101, + }, + { + name: "Product 3", + id: 103, + }, + ], + }, + ], + id: 205, + }, + variant_id: 992, + order_id: 205, + }, + ], + }, + { + id: 993, + name: "Product variant 33", + orders: { + order: { + number: "ORD-202", + products: [ + { + product_id: [101, 103], + variant_id: 993, + quantity: 4, + product: [ + { + name: "Product 1", + id: 101, + }, + { + name: "Product 3", + id: 103, + }, + ], + }, + ], + id: 205, + }, + variant_id: 993, + order_id: 205, + }, + }, + ]) + }) +}) diff --git a/packages/orchestration/src/__tests__/joiner/remote-joiner.ts b/packages/orchestration/src/__tests__/joiner/remote-joiner.ts new file mode 100644 index 0000000000000..5034c859312a7 --- /dev/null +++ b/packages/orchestration/src/__tests__/joiner/remote-joiner.ts @@ -0,0 +1,287 @@ +import { MedusaContainer, RemoteExpandProperty } from "@medusajs/types" +import { lowerCaseFirst, toPascalCase } from "@medusajs/utils" +import { serviceConfigs, serviceMock } from "../../__mocks__/joiner/mock_data" +import { RemoteJoiner } from "./../../joiner" + +const container = { + resolve: (serviceName) => { + return { + list: (...args) => { + return serviceMock[serviceName].apply(this, args) + }, + } + }, +} as MedusaContainer + +const fetchServiceDataCallback = async ( + expand: RemoteExpandProperty, + pkField: string, + ids?: (unknown | unknown[])[], + relationship?: any +) => { + const serviceConfig = expand.serviceConfig + const moduleRegistryName = + lowerCaseFirst(serviceConfig.serviceName) + "Service" + + const service = container.resolve(moduleRegistryName) + const methodName = relationship?.inverse + ? `getBy${toPascalCase(pkField)}` + : "list" + + return await service[methodName]({ + fields: expand.fields, + args: expand.args, + expands: expand.expands, + options: { + [pkField]: ids, + }, + }) +} + +describe("RemoteJoiner", () => { + let joiner: RemoteJoiner + beforeAll(() => { + joiner = new RemoteJoiner(serviceConfigs, fetchServiceDataCallback) + }) + beforeEach(() => { + jest.clearAllMocks() + }) + + it("Simple query of a service, its id and no fields specified", async () => { + const query = { + service: "User", + args: [ + { + name: "id", + value: "1", + }, + ], + fields: ["id", "name", "email"], + } + + await joiner.query(query) + + expect(serviceMock.userService).toHaveBeenCalledTimes(1) + expect(serviceMock.userService).toHaveBeenCalledWith({ + args: [], + fields: ["id", "name", "email"], + options: { id: ["1"] }, + }) + }) + + it("Transforms main service name into PascalCase", async () => { + const query = { + service: "user", + fields: ["id"], + } + + await joiner.query(query) + + expect(serviceMock.userService).toHaveBeenCalledTimes(1) + }) + + it("Simple query of a service, its id and a few fields specified", async () => { + const query = { + service: "User", + args: [ + { + name: "id", + value: "1", + }, + ], + fields: ["username", "email"], + } + + await joiner.query(query) + + expect(serviceMock.userService).toHaveBeenCalledTimes(1) + expect(serviceMock.userService).toHaveBeenCalledWith({ + args: [], + fields: ["username", "email"], + options: { id: ["1"] }, + }) + }) + + it("Query of a service, expanding a property and restricting the fields expanded", async () => { + const query = { + service: "user", + fields: ["username", "email", "products"], + args: [ + { + name: "id", + value: "1", + }, + ], + expands: [ + { + property: "products", + fields: ["product"], + }, + { + property: "products.product", + fields: ["name"], + }, + ], + } + + await joiner.query(query) + + expect(serviceMock.userService).toHaveBeenCalledTimes(1) + expect(serviceMock.userService).toHaveBeenCalledWith({ + args: [], + fields: ["username", "email", "products"], + expands: { + products: { + args: undefined, + fields: ["product_id"], + }, + }, + options: { id: ["1"] }, + }) + + expect(serviceMock.productService).toHaveBeenCalledTimes(1) + expect(serviceMock.productService).toHaveBeenCalledWith({ + fields: ["name", "id"], + options: { id: expect.arrayContaining([101, 102, 103]) }, + }) + }) + + it("Query a service using more than 1 argument, expanding a property with another argument", async () => { + const query = { + service: "User", + args: [ + { + name: "id", + value: "1", + }, + { + name: "role", + value: "admin", + }, + ], + fields: ["username", "email", "products"], + expands: [ + { + property: "products", + fields: ["product"], + }, + { + property: "products.product", + fields: ["name"], + args: [ + { + name: "limit", + value: "5", + }, + ], + }, + ], + } + + await joiner.query(query) + + expect(serviceMock.userService).toHaveBeenCalledTimes(1) + expect(serviceMock.userService).toHaveBeenCalledWith({ + args: [ + { + name: "role", + value: "admin", + }, + ], + fields: ["username", "email", "products"], + expands: { + products: { + args: undefined, + fields: ["product_id"], + }, + }, + options: { id: ["1"] }, + }) + + expect(serviceMock.productService).toHaveBeenCalledTimes(1) + expect(serviceMock.productService).toHaveBeenCalledWith({ + fields: ["name", "id"], + options: { id: expect.arrayContaining([101, 102, 103]) }, + args: [ + { + name: "limit", + value: "5", + }, + ], + }) + }) + + it("Query a service expanding multiple nested properties", async () => { + const query = { + service: "Order", + fields: ["number", "date", "products"], + expands: [ + { + property: "products", + fields: ["product"], + }, + { + property: "products.product", + fields: ["handler"], + }, + { + property: "user", + fields: ["fullname", "email", "products"], + }, + { + property: "user.products", + fields: ["product"], + }, + { + property: "user.products.product", + fields: ["name"], + }, + ], + args: [ + { + name: "id", + value: "3", + }, + ], + } + + await joiner.query(query) + + expect(serviceMock.orderService).toHaveBeenCalledTimes(1) + expect(serviceMock.orderService).toHaveBeenCalledWith({ + args: [], + fields: ["number", "date", "products", "user_id"], + expands: { + products: { + args: undefined, + fields: ["product_id"], + }, + }, + options: { id: ["3"] }, + }) + + expect(serviceMock.userService).toHaveBeenCalledTimes(1) + expect(serviceMock.userService).toHaveBeenCalledWith({ + fields: ["fullname", "email", "products", "id"], + args: undefined, + expands: { + products: { + args: undefined, + fields: ["product_id"], + }, + }, + options: { id: [4, 1] }, + }) + + expect(serviceMock.productService).toHaveBeenCalledTimes(2) + expect(serviceMock.productService).toHaveBeenNthCalledWith(1, { + fields: ["name", "id"], + options: { id: expect.arrayContaining([103, 102]) }, + }) + + expect(serviceMock.productService).toHaveBeenNthCalledWith(2, { + fields: ["handler", "id"], + options: { id: expect.arrayContaining([101, 103]) }, + }) + }) +}) diff --git a/packages/orchestration/src/index.ts b/packages/orchestration/src/index.ts new file mode 100644 index 0000000000000..3a2ffb55e4078 --- /dev/null +++ b/packages/orchestration/src/index.ts @@ -0,0 +1 @@ +export * from "./joiner" diff --git a/packages/orchestration/src/joiner/graphql-ast.ts b/packages/orchestration/src/joiner/graphql-ast.ts new file mode 100644 index 0000000000000..c33e9fdb60cc9 --- /dev/null +++ b/packages/orchestration/src/joiner/graphql-ast.ts @@ -0,0 +1,154 @@ +import { RemoteJoinerQuery } from "@medusajs/types" +import { + ArgumentNode, + DocumentNode, + FieldNode, + Kind, + OperationDefinitionNode, + SelectionSetNode, + ValueNode, + parse, +} from "graphql" + +interface Argument { + name: string + value?: unknown +} + +interface Entity { + property: string + fields: string[] + args?: Argument[] +} + +class GraphQLParser { + private ast: DocumentNode + + constructor(input: string, private variables?: { [key: string]: unknown }) { + this.ast = parse(input) + this.variables = variables || {} + } + + private parseValueNode(valueNode: ValueNode): unknown { + switch (valueNode.kind) { + case Kind.VARIABLE: + const variableName = valueNode.name.value + return this.variables ? this.variables[variableName] : undefined + case Kind.INT: + return parseInt(valueNode.value, 10) + case Kind.FLOAT: + return parseFloat(valueNode.value) + case Kind.BOOLEAN: + return Boolean(valueNode.value) + case Kind.STRING: + case Kind.ENUM: + return valueNode.value + case Kind.NULL: + return null + 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) + } + return obj + default: + return undefined + } + } + + private parseArguments( + args: readonly ArgumentNode[] + ): Argument[] | undefined { + if (!args.length) { + return + } + + return args.map((arg) => { + const value = this.parseValueNode(arg.value) + + return { + name: arg.name.value, + value: value, + } + }) + } + + private extractEntities( + node: SelectionSetNode, + parentName = "", + mainService = "" + ): Entity[] { + const entities: Entity[] = [] + + node.selections.forEach((selection) => { + if (selection.kind === "Field") { + const fieldNode = selection as FieldNode + + if (fieldNode.selectionSet) { + const entityName = parentName + ? `${parentName}.${fieldNode.name.value}` + : fieldNode.name.value + + const nestedEntity: Entity = { + property: entityName.replace(`${mainService}.`, ""), + fields: fieldNode.selectionSet.selections.map( + (field) => (field as FieldNode).name.value + ), + args: this.parseArguments(fieldNode.arguments!), + } + + entities.push(nestedEntity) + entities.push( + ...this.extractEntities( + fieldNode.selectionSet, + entityName, + mainService + ) + ) + } + } + }) + + return entities + } + + public parseQuery(): RemoteJoinerQuery { + const queryDefinition = this.ast.definitions.find( + (definition) => definition.kind === "OperationDefinition" + ) as OperationDefinitionNode + + if (!queryDefinition) { + throw new Error("No query found") + } + + const rootFieldNode = queryDefinition.selectionSet + .selections[0] as FieldNode + + const remoteJoinConfig: RemoteJoinerQuery = { + service: rootFieldNode.name.value, + fields: [], + expands: [], + } + + if (rootFieldNode.arguments) { + remoteJoinConfig.args = this.parseArguments(rootFieldNode.arguments) + } + + if (rootFieldNode.selectionSet) { + remoteJoinConfig.fields = rootFieldNode.selectionSet.selections.map( + (field) => (field as FieldNode).name.value + ) + remoteJoinConfig.expands = this.extractEntities( + rootFieldNode.selectionSet, + rootFieldNode.name.value, + rootFieldNode.name.value + ) + } + + return remoteJoinConfig + } +} + +export default GraphQLParser diff --git a/packages/orchestration/src/joiner/index.ts b/packages/orchestration/src/joiner/index.ts new file mode 100644 index 0000000000000..ba55313b31f34 --- /dev/null +++ b/packages/orchestration/src/joiner/index.ts @@ -0,0 +1 @@ +export * from "./remote-joiner" diff --git a/packages/orchestration/src/joiner/remote-joiner.ts b/packages/orchestration/src/joiner/remote-joiner.ts new file mode 100644 index 0000000000000..799fece99b19a --- /dev/null +++ b/packages/orchestration/src/joiner/remote-joiner.ts @@ -0,0 +1,575 @@ +import { + JoinerRelationship, + JoinerServiceConfig, + RemoteExpandProperty, + RemoteJoinerQuery, + RemoteNestedExpands, +} from "@medusajs/types" +import { isDefined, toPascalCase } from "@medusajs/utils" +import GraphQLParser from "./graphql-ast" + +const BASE_PATH = "_root" +export class RemoteJoiner { + private serviceConfigs: JoinerServiceConfig[] + private serviceConfigCache: Map = new Map() + + private static filterFields( + data: any, + fields: string[], + expands?: RemoteNestedExpands + ): Record { + if (!fields) { + return data + } + + const filteredData = fields.reduce((acc: any, field: string) => { + acc[field] = data?.[field] + return acc + }, {}) + + if (expands) { + for (const key in expands) { + const expand = expands[key] + if (expand) { + if (Array.isArray(data[key])) { + filteredData[key] = data[key].map((item: any) => + RemoteJoiner.filterFields(item, expand.fields, expand.expands) + ) + } else { + filteredData[key] = RemoteJoiner.filterFields( + data[key], + expand.fields, + expand.expands + ) + } + } + } + } + + return filteredData + } + + private static getNestedItems(items: any[], property: string): any[] { + return items + .flatMap((item) => item[property]) + .filter((item) => item !== undefined) + } + + private static createRelatedDataMap( + relatedDataArray: any[], + joinFields: string[] + ): Map { + return relatedDataArray.reduce((acc, data) => { + const joinValues = joinFields.map((field) => data[field]) + const key = joinValues.length === 1 ? joinValues[0] : joinValues.join(",") + + let isArray = Array.isArray(acc[key]) + if (isDefined(acc[key]) && !isArray) { + acc[key] = [acc[key]] + isArray = true + } + + if (isArray) { + acc[key].push(data) + } else { + acc[key] = data + } + return acc + }, {}) + } + + static parseQuery(graphqlQuery: string, variables?: any): RemoteJoinerQuery { + const parser = new GraphQLParser(graphqlQuery, variables) + return parser.parseQuery() + } + + constructor( + serviceConfigs: JoinerServiceConfig[], + private remoteFetchData: ( + expand: RemoteExpandProperty, + pkField: string, + ids?: (unknown | unknown[])[], + relationship?: any + ) => Promise<{ + data: unknown[] | { [path: string]: unknown[] } + path?: string + }> + ) { + this.serviceConfigs = this.buildReferences(serviceConfigs) + } + + private buildReferences(serviceConfigs: JoinerServiceConfig[]) { + const expandedRelationships: Map = new Map() + for (const service of serviceConfigs) { + // self-reference + const propName = service.serviceName.toLowerCase() + if (!service.relationships) { + service.relationships = [] + } + + service.relationships?.push({ + alias: propName, + foreignKey: propName + "_id", + primaryKey: "id", + serviceName: service.serviceName, + }) + + this.serviceConfigCache.set(service.serviceName, service) + + if (!service.extends) { + continue + } + + for (const extend of service.extends) { + if (!expandedRelationships.has(extend.serviceName)) { + expandedRelationships.set(extend.serviceName, []) + } + + expandedRelationships.get(extend.serviceName)!.push(extend.resolve) + } + } + + for (const [serviceName, relationships] of expandedRelationships) { + if (!this.serviceConfigCache.has(serviceName)) { + throw new Error(`Service ${serviceName} not found`) + } + + const service = this.serviceConfigCache.get(serviceName) + service!.relationships?.push(...relationships) + } + + return serviceConfigs + } + + private findServiceConfig( + serviceName: string + ): JoinerServiceConfig | undefined { + if (!this.serviceConfigCache.has(serviceName)) { + const config = this.serviceConfigs.find( + (config) => config.serviceName === serviceName + ) + this.serviceConfigCache.set(serviceName, config!) + } + return this.serviceConfigCache.get(serviceName) + } + + private async fetchData( + expand: RemoteExpandProperty, + pkField: string, + ids?: (unknown | unknown[])[], + relationship?: any + ): Promise<{ + data: unknown[] | { [path: string]: unknown[] } + path?: string + }> { + let uniqueIds = Array.isArray(ids) ? ids : ids ? [ids] : undefined + + if (uniqueIds) { + const isCompositeKey = Array.isArray(uniqueIds[0]) + if (isCompositeKey) { + const seen = new Set() + uniqueIds = uniqueIds.filter((idArray) => { + const key = JSON.stringify(idArray) + const isNew = !seen.has(key) + seen.add(key) + return isNew + }) + } else { + uniqueIds = Array.from(new Set(uniqueIds.flat())) + } + } + + if (relationship) { + pkField = relationship.inverse + ? relationship.foreignKey.split(".").pop()! + : relationship.primaryKey + } + + const response = await this.remoteFetchData( + expand, + pkField, + uniqueIds, + relationship + ) + const isObj = isDefined(response.path) + const resData = isObj ? response.data[response.path!] : response.data + + const filteredDataArray = resData.map((data: any) => + RemoteJoiner.filterFields(data, expand.fields, expand.expands) + ) + + if (isObj) { + response.data[response.path!] = filteredDataArray + } else { + response.data = filteredDataArray + } + + return response + } + + private async handleExpands( + items: any[], + query: RemoteJoinerQuery, + parsedExpands: Map + ): Promise { + if (!parsedExpands) { + return + } + + const stack: [ + any[], + RemoteJoinerQuery, + Map, + string, + Set + ][] = [[items, query, parsedExpands, "", new Set()]] + + while (stack.length > 0) { + const [ + currentItems, + currentQuery, + currentParsedExpands, + basePath, + resolvedPaths, + ] = stack.pop()! + + for (const [expandedPath, expand] of currentParsedExpands.entries()) { + const isImmediateChildPath = + expandedPath.startsWith(basePath) && + expandedPath.split(".").length === basePath.split(".").length + 1 + + if (!isImmediateChildPath || resolvedPaths.has(expandedPath)) { + continue + } + + resolvedPaths.add(expandedPath) + + const property = expand.property || "" + const parentServiceConfig = this.findServiceConfig(currentQuery.service) + + await this.expandProperty(currentItems, parentServiceConfig!, expand) + + const relationship = parentServiceConfig?.relationships?.find( + (relation) => relation.alias === property + ) + + const nestedItems = RemoteJoiner.getNestedItems(currentItems, property) + + if (nestedItems.length > 0) { + const nextProp = relationship + ? { + ...currentQuery, + service: relationship.serviceName, + } + : currentQuery + + stack.push([ + nestedItems, + nextProp, + currentParsedExpands, + expandedPath, + new Set(), + ]) + } + } + } + } + + private async expandProperty( + items: any[], + parentServiceConfig: JoinerServiceConfig, + expand?: RemoteExpandProperty + ): Promise { + if (!expand) { + return + } + + const relationship = parentServiceConfig?.relationships?.find( + (relation) => relation.alias === expand.property + ) + + if (relationship) { + await this.expandRelationshipProperty(items, expand, relationship) + } + } + + private async expandRelationshipProperty( + items: any[], + expand: RemoteExpandProperty, + relationship: JoinerRelationship + ): Promise { + const field = relationship.inverse + ? relationship.primaryKey + : relationship.foreignKey.split(".").pop()! + const fieldsArray = field.split(",") + + const idsToFetch: any[] = [] + + items.forEach((item) => { + const values = fieldsArray + .map((field) => item[field]) + .filter((value) => value !== undefined) + + if (values.length === fieldsArray.length && !item[relationship.alias]) { + if (fieldsArray.length === 1) { + if (!idsToFetch.includes(values[0])) { + idsToFetch.push(values[0]) + } + } else { + // composite key + const valuesString = values.join(",") + + if (!idsToFetch.some((id) => id.join(",") === valuesString)) { + idsToFetch.push(values) + } + } + } + }) + + if (idsToFetch.length === 0) { + return + } + + const relatedDataArray = await this.fetchData( + expand, + field, + idsToFetch, + relationship + ) + + const joinFields = relationship.inverse + ? relationship.foreignKey.split(",") + : relationship.primaryKey.split(",") + + const relData = relatedDataArray.path + ? relatedDataArray.data[relatedDataArray.path!] + : relatedDataArray.data + + const relatedDataMap = RemoteJoiner.createRelatedDataMap( + relData, + joinFields + ) + + items.forEach((item) => { + if (!item[relationship.alias]) { + const itemKey = fieldsArray.map((field) => item[field]).join(",") + + if (Array.isArray(item[field])) { + item[relationship.alias] = item[field] + .map((id) => relatedDataMap[id]) + .filter((relatedItem) => relatedItem !== undefined) + } else { + item[relationship.alias] = relatedDataMap[itemKey] + } + } + }) + } + + private parseExpands( + initialService: RemoteExpandProperty, + query: RemoteJoinerQuery, + serviceConfig: JoinerServiceConfig, + expands: RemoteJoinerQuery["expands"] + ): Map { + const parsedExpands = this.parseProperties( + initialService, + query, + serviceConfig, + expands + ) + + const groupedExpands = this.groupExpands(parsedExpands) + + return groupedExpands + } + + private parseProperties( + initialService: RemoteExpandProperty, + query: RemoteJoinerQuery, + serviceConfig: JoinerServiceConfig, + expands: RemoteJoinerQuery["expands"] + ): Map { + const parsedExpands = new Map() + parsedExpands.set(BASE_PATH, initialService) + + for (const expand of expands || []) { + const properties = expand.property.split(".") + let currentServiceConfig = serviceConfig as any + const currentPath: string[] = [] + + for (const prop of properties) { + const fullPath = [BASE_PATH, ...currentPath, prop].join(".") + const relationship = currentServiceConfig.relationships.find( + (relation) => relation.alias === prop + ) + + let fields: string[] | undefined = + fullPath === BASE_PATH + "." + expand.property + ? expand.fields + : undefined + const args = + fullPath === BASE_PATH + "." + expand.property + ? expand.args + : undefined + + if (relationship) { + const parentExpand = + parsedExpands.get([BASE_PATH, ...currentPath].join(".")) || query + + if (parentExpand) { + if (parentExpand.fields) { + const relField = relationship.inverse + ? relationship.primaryKey + : relationship.foreignKey.split(".").pop()! + + parentExpand.fields = parentExpand.fields + .concat(relField.split(",")) + .filter((field) => field !== relationship.alias) + + parentExpand.fields = [...new Set(parentExpand.fields)] + } + + if (fields) { + const relField = relationship.inverse + ? relationship.foreignKey.split(".").pop()! + : relationship.primaryKey + fields = fields.concat(relField.split(",")) + + fields = [...new Set(fields)] + } + } + + currentServiceConfig = this.findServiceConfig( + relationship.serviceName + ) + + if (!currentServiceConfig) { + throw new Error( + `Target service not found: ${relationship.serviceName}` + ) + } + } + + if (!parsedExpands.has(fullPath)) { + parsedExpands.set(fullPath, { + property: prop, + serviceConfig: currentServiceConfig, + fields, + args, + }) + } + + currentPath.push(prop) + } + } + + return parsedExpands + } + + private groupExpands( + parsedExpands: Map + ): Map { + const sortedParsedExpands = new Map( + Array.from(parsedExpands.entries()).sort() + ) + + const mergedExpands = new Map( + sortedParsedExpands + ) + const mergedPaths = new Map() + + let lastServiceName = "" + + for (const [path, expand] of sortedParsedExpands.entries()) { + const currentServiceName = expand.serviceConfig.serviceName + + let parentPath = path.split(".").slice(0, -1).join(".") + + // Check if the parentPath was merged before + while (mergedPaths.has(parentPath)) { + parentPath = mergedPaths.get(parentPath)! + } + + const canMerge = currentServiceName === lastServiceName + + if (mergedExpands.has(parentPath) && canMerge) { + const parentExpand = mergedExpands.get(parentPath)! + + if (parentExpand.serviceConfig.serviceName === currentServiceName) { + const nestedKeys = path.split(".").slice(parentPath.split(".").length) + let targetExpand: any = parentExpand + + for (let key of nestedKeys) { + if (!targetExpand.expands) { + targetExpand.expands = {} + } + if (!targetExpand.expands[key]) { + targetExpand.expands[key] = {} as any + } + targetExpand = targetExpand.expands[key] + } + + targetExpand.fields = expand.fields + targetExpand.args = expand.args + mergedPaths.set(path, parentPath) + } + } else { + lastServiceName = currentServiceName + } + } + + return mergedExpands + } + + async query(queryObj: RemoteJoinerQuery): Promise { + queryObj.service = toPascalCase(queryObj.service) + const serviceConfig = this.findServiceConfig(queryObj.service) + + if (!serviceConfig) { + throw new Error(`Service not found: ${queryObj.service}`) + } + + let pkName = serviceConfig.primaryKeys[0] + const primaryKeyArg = queryObj.args?.find((arg) => { + const inc = serviceConfig.primaryKeys.includes(arg.name) + if (inc) { + pkName = arg.name + } + return inc + }) + const otherArgs = queryObj.args?.filter( + (arg) => !serviceConfig.primaryKeys.includes(arg.name) + ) + + const parsedExpands = this.parseExpands( + { + property: "", + serviceConfig: serviceConfig, + fields: queryObj.fields, + args: otherArgs, + }, + queryObj, + serviceConfig, + queryObj.expands! + ) + + const root = parsedExpands.get(BASE_PATH)! + + const response = await this.fetchData( + root, + pkName, + primaryKeyArg?.value, + undefined + ) + + const data = response.path ? response.data[response.path!] : response.data + + await this.handleExpands( + Array.isArray(data) ? data : [data], + queryObj, + parsedExpands + ) + + return response.data + } +} diff --git a/packages/orchestration/tsconfig.json b/packages/orchestration/tsconfig.json new file mode 100644 index 0000000000000..9fa65c92eba2f --- /dev/null +++ b/packages/orchestration/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "lib": ["es5", "es6", "es2019"], + "target": "es5", + "outDir": "./dist", + "esModuleInterop": true, + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true, + "downlevelIteration": true + }, + "include": ["src"], + "exclude": [ + "dist", + "./src/**/__tests__", + "./src/**/__mocks__", + "./src/**/__fixtures__", + "node_modules" + ] +} diff --git a/packages/product/src/scripts/migration-up.ts b/packages/product/src/scripts/migration-up.ts index e0139040a9b69..46b9d6090caf7 100644 --- a/packages/product/src/scripts/migration-up.ts +++ b/packages/product/src/scripts/migration-up.ts @@ -35,7 +35,7 @@ export async function runMigrations({ const migrator = orm.getMigrator() const pendingMigrations = await migrator.getPendingMigrations() - logger.info("Running pending migrations:", pendingMigrations) + logger.info(`Running pending migrations: ${pendingMigrations}`) await migrator.up({ migrations: pendingMigrations.map((m) => m.name), diff --git a/packages/types/src/bundles.ts b/packages/types/src/bundles.ts index 95114a97855f8..cc95cd0e83928 100644 --- a/packages/types/src/bundles.ts +++ b/packages/types/src/bundles.ts @@ -8,3 +8,4 @@ export * as SearchTypes from "./search" export * as StockLocationTypes from "./stock-location" export * as TransactionBaseTypes from "./transaction-base" export * as DAL from "./dal" +export * as LoggerTypes from "./logger" diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 63df9857f334b..85be26902f302 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -3,6 +3,7 @@ export * from "./cache" export * from "./common" export * from "./event-bus" export * from "./inventory" +export * from "./joiner" export * from "./modules-sdk" export * from "./product" export * from "./product-category" @@ -11,3 +12,4 @@ export * from "./shared-context" export * from "./stock-location" export * from "./transaction-base" export * from "./dal" +export * from "./logger" diff --git a/packages/types/src/joiner/index.ts b/packages/types/src/joiner/index.ts new file mode 100644 index 0000000000000..3061d3f60f82c --- /dev/null +++ b/packages/types/src/joiner/index.ts @@ -0,0 +1,51 @@ +export type JoinerRelationship = { + alias: string + foreignKey: string + primaryKey: string + serviceName: string + inverse?: boolean // In an inverted relationship the foreign key is on the other service and the primary key is on the current service +} + +export interface JoinerServiceConfig { + serviceName: string + primaryKeys: string[] + relationships?: JoinerRelationship[] + extends?: { + serviceName: string + resolve: JoinerRelationship + }[] +} + +export interface JoinerArgument { + name: string + value?: any + field?: string +} + +export interface RemoteJoinerQuery { + service: string + expands?: Array<{ + property: string + fields: string[] + args?: JoinerArgument[] + relationships?: JoinerRelationship[] + }> + fields: string[] + args?: JoinerArgument[] +} + +export interface RemoteNestedExpands { + [key: string]: { + fields: string[] + args?: JoinerArgument[] + expands?: RemoteNestedExpands + } +} + +export interface RemoteExpandProperty { + property: string + serviceConfig: JoinerServiceConfig + fields: string[] + args?: JoinerArgument[] + expands?: RemoteNestedExpands +} diff --git a/packages/types/src/logger/index.ts b/packages/types/src/logger/index.ts new file mode 100644 index 0000000000000..83ddd1fc82f0a --- /dev/null +++ b/packages/types/src/logger/index.ts @@ -0,0 +1,15 @@ +export interface Logger { + panic: (data) => void + shouldLog: (level: string) => void + setLogLevel: (level: string) => void + unsetLogLevel: () => void + activity: (message: string, config?) => void + progress: (activityId, message) => void + error: (messageOrError, error?) => void + failure: (activityId, message) => void + success: (activityId, message) => void + debug: (message) => void + info: (message) => void + warn: (message) => void + log: (...args) => void +} diff --git a/packages/types/src/modules-sdk/index.ts b/packages/types/src/modules-sdk/index.ts index 13f20a065ef7a..b6f14f7c48180 100644 --- a/packages/types/src/modules-sdk/index.ts +++ b/packages/types/src/modules-sdk/index.ts @@ -1,5 +1,5 @@ -import { Logger as _Logger } from "winston" -import { MedusaContainer } from "../common/medusa-container" +import { MedusaContainer } from "../common" +import { Logger } from "../logger" export type Constructor = new (...args: any[]) => T export * from "../common/medusa-container" @@ -14,12 +14,6 @@ export type LogLevel = | "migration" export type LoggerOptions = boolean | "all" | LogLevel[] -export type Logger = _Logger & { - progress: (activityId: string, msg: string) => void - info: (msg: string) => void - warn: (msg: string) => void -} - export enum MODULE_SCOPE { INTERNAL = "internal", EXTERNAL = "external", diff --git a/packages/utils/package.json b/packages/utils/package.json index 35ac427e1cbdb..379cd200e627b 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -34,6 +34,6 @@ "prepare": "cross-env NODE_ENV=production yarn run build", "build": "tsc --build", "watch": "tsc --build --watch", - "test": "jest --passWithNoTests src" + "test": "jest" } } diff --git a/packages/utils/src/common/__tests__/build-query.spec.ts b/packages/utils/src/common/__tests__/build-query.spec.ts new file mode 100644 index 0000000000000..9ddc53dcf09cf --- /dev/null +++ b/packages/utils/src/common/__tests__/build-query.spec.ts @@ -0,0 +1,91 @@ +import { buildSelects } from "../build-query" + +describe("buildSelects", () => { + it("successfully build back select object shape to list", () => { + const q = buildSelects([ + "order", + "order.items", + "order.swaps", + "order.swaps.additional_items", + "order.discounts", + "order.discounts.rule", + "order.claims", + "order.claims.additional_items", + "additional_items", + "additional_items.variant", + "return_order", + "return_order.items", + "return_order.shipping_method", + "return_order.shipping_method.tax_lines", + ]) + + expect(q).toEqual({ + order: { + items: true, + swaps: { + additional_items: true, + }, + discounts: { + rule: true, + }, + claims: { + additional_items: true, + }, + }, + additional_items: { + variant: true, + }, + return_order: { + items: true, + shipping_method: { + tax_lines: true, + }, + }, + }) + }) +}) + +describe("buildSelects", () => { + it("successfully build back select object shape to list", () => { + const q = buildSelects([ + "order", + "order.items", + "order.swaps", + "order.swaps.additional_items", + "order.discounts", + "order.discounts.rule", + "order.claims", + "order.claims.additional_items", + "additional_items", + "additional_items.variant", + "return_order", + "return_order.items", + "return_order.shipping_method", + "return_order.shipping_method.tax_lines", + ]) + + expect(q).toEqual({ + order: { + items: true, + swaps: { + additional_items: true, + }, + discounts: { + rule: true, + }, + claims: { + additional_items: true, + }, + }, + additional_items: { + variant: true, + }, + return_order: { + items: true, + shipping_method: { + tax_lines: true, + }, + }, + }) + }) +}) diff --git a/packages/utils/src/common/__tests__/object-to-string-path.spec.ts b/packages/utils/src/common/__tests__/object-to-string-path.spec.ts new file mode 100644 index 0000000000000..b36b6d02bbc82 --- /dev/null +++ b/packages/utils/src/common/__tests__/object-to-string-path.spec.ts @@ -0,0 +1,42 @@ +import { objectToStringPath } from "../object-to-string-path" + +describe("objectToStringPath", function () { + it("should return only the properties path of the properties that are set to true", function () { + const res = objectToStringPath( + { + product: true, + variants: { + title: true, + prices: { + amount: true, + }, + }, + }, + { + includeParentPropertyFields: false, + } + ) + + expect(res).toEqual(["product", "variants.title", "variants.prices.amount"]) + }) + + it("should return a string path from an object including properties that are object and contains other properties set to true", function () { + const res = objectToStringPath({ + product: true, + variants: { + title: true, + prices: { + amount: true, + }, + }, + }) + + expect(res).toEqual([ + "product", + "variants", + "variants.title", + "variants.prices", + "variants.prices.amount", + ]) + }) +}) diff --git a/packages/utils/src/common/__tests__/to-camel-case.spec.ts b/packages/utils/src/common/__tests__/to-camel-case.spec.ts new file mode 100644 index 0000000000000..1435eae450bd7 --- /dev/null +++ b/packages/utils/src/common/__tests__/to-camel-case.spec.ts @@ -0,0 +1,36 @@ +import { toCamelCase } from "../to-camel-case" + +describe("toCamelCase", function () { + it("should convert all cases to camel case", function () { + const expectations = [ + { + input: "testing-camelize", + output: "testingCamelize", + }, + { + input: "testing-Camelize", + output: "testingCamelize", + }, + { + input: "TESTING-CAMELIZE", + output: "testingCamelize", + }, + { + input: "this_is-A-test", + output: "thisIsATest", + }, + { + input: "this_is-A-test ANOTHER", + output: "thisIsATestAnother", + }, + { + input: "testingAlreadyCamelized", + output: "testingAlreadyCamelized", + }, + ] + + expectations.forEach((expectation) => { + expect(toCamelCase(expectation.input)).toEqual(expectation.output) + }) + }) +}) diff --git a/packages/utils/src/common/__tests__/upper-case-first.spec.ts b/packages/utils/src/common/__tests__/upper-case-first.spec.ts new file mode 100644 index 0000000000000..584ac31124107 --- /dev/null +++ b/packages/utils/src/common/__tests__/upper-case-first.spec.ts @@ -0,0 +1,36 @@ +import { upperCaseFirst } from "../upper-case-first" + +describe("upperCaseFirst", function () { + it("should convert first letter of the word to capital letter", function () { + const expectations = [ + { + input: "testing capitalize", + output: "Testing capitalize", + }, + { + input: "testing", + output: "Testing", + }, + { + input: "Testing", + output: "Testing", + }, + { + input: "TESTING", + output: "TESTING", + }, + { + input: "t", + output: "T", + }, + { + input: "", + output: "", + }, + ] + + expectations.forEach((expectation) => { + expect(upperCaseFirst(expectation.input)).toEqual(expectation.output) + }) + }) +}) diff --git a/packages/utils/src/common/index.ts b/packages/utils/src/common/index.ts index beb84ecb81d1c..7270860f2a1e3 100644 --- a/packages/utils/src/common/index.ts +++ b/packages/utils/src/common/index.ts @@ -2,18 +2,22 @@ export * from "./build-query" export * from "./errors" export * from "./generate-entity-id" export * from "./get-config-file" +export * from "./handle-postgres-database-error" export * from "./is-date" export * from "./is-defined" export * from "./is-email" export * from "./is-object" export * from "./is-string" export * from "./lower-case-first" +export * from "./upper-case-first" export * from "./medusa-container" export * from "./object-to-string-path" export * from "./set-metadata" export * from "./simple-hash" export * from "./wrap-handler" export * from "./to-kebab-case" +export * from "./to-camel-case" export * from "./stringify-circular" -export * from "./build-query" -export * from "./handle-postgres-database-error" +export * from "./to-kebab-case" +export * from "./to-pascal-case" +export * from "./wrap-handler" diff --git a/packages/utils/src/common/object-to-string-path.ts b/packages/utils/src/common/object-to-string-path.ts index b1010df1abdeb..c3f64e56beced 100644 --- a/packages/utils/src/common/object-to-string-path.ts +++ b/packages/utils/src/common/object-to-string-path.ts @@ -4,7 +4,8 @@ import { isObject } from "./is-object" * Converts a structure of find options to an * array of string paths * @example - * input: { + * // With `includeTruePropertiesOnly` default value set to false + * const result = objectToStringPath({ * test: { * test1: true, * test2: true, @@ -13,20 +14,50 @@ import { isObject } from "./is-object" * }, * }, * test2: true - * } - * output: ['test.test1', 'test.test2', 'test.test3.test4', 'test2'] - * @param input + * }) + * console.log(result) + * // output: ['test', 'test.test1', 'test.test2', 'test.test3', 'test.test3.test4', 'test2'] + * + * @example + * // With `includeTruePropertiesOnly` set to true + * const result = objectToStringPath({ + * test: { + * test1: true, + * test2: true, + * test3: { + * test4: true + * }, + * }, + * test2: true + * }, { + * includeTruePropertiesOnly: true + * }) + * console.log(result) + * // output: ['test.test1', 'test.test2', 'test.test3.test4', 'test2'] + * + * @param {InputObject} input + * @param {boolean} includeParentPropertyFields If set to true (example 1), all properties will be included as well as the parents, + * otherwise (example 2) all properties path set to true will included, excluded the parents */ -export function objectToStringPath(input: object = {}): string[] { +export function objectToStringPath( + input: object = {}, + { includeParentPropertyFields }: { includeParentPropertyFields: boolean } = { + includeParentPropertyFields: true, + } +): string[] { if (!isObject(input) || !Object.keys(input).length) { return [] } - const output: Set = new Set(Object.keys(input)) + const output: Set = includeParentPropertyFields + ? new Set(Object.keys(input)) + : new Set() for (const key of Object.keys(input)) { - if (input[key] != undefined && typeof input[key] === "object") { - const deepRes = objectToStringPath(input[key]) + if (isObject(input[key])) { + const deepRes = objectToStringPath(input[key], { + includeParentPropertyFields, + }) const items = deepRes.reduce((acc, val) => { acc.push(`${key}.${val}`) @@ -37,7 +68,9 @@ export function objectToStringPath(input: object = {}): string[] { continue } - output.add(key) + if (isObject(key) || input[key] === true) { + output.add(key) + } } return Array.from(output) diff --git a/packages/utils/src/common/to-camel-case.ts b/packages/utils/src/common/to-camel-case.ts new file mode 100644 index 0000000000000..827d2f07bc131 --- /dev/null +++ b/packages/utils/src/common/to-camel-case.ts @@ -0,0 +1,7 @@ +export function toCamelCase(str: string): string { + return /^([a-z]+)(([A-Z]([a-z]+))+)$/.test(str) + ? str + : str + .toLowerCase() + .replace(/[^a-zA-Z0-9]+(.)/g, (m, chr) => chr.toUpperCase()) +} diff --git a/packages/utils/src/common/to-pascal-case.ts b/packages/utils/src/common/to-pascal-case.ts new file mode 100644 index 0000000000000..0d1b794ed281a --- /dev/null +++ b/packages/utils/src/common/to-pascal-case.ts @@ -0,0 +1,5 @@ +export function toPascalCase(s: string): string { + return s.replace(/(^\w|_\w)/g, (match) => + match.replace(/_/g, "").toUpperCase() + ) +} diff --git a/packages/utils/src/common/upper-case-first.ts b/packages/utils/src/common/upper-case-first.ts new file mode 100644 index 0000000000000..39b6888c05605 --- /dev/null +++ b/packages/utils/src/common/upper-case-first.ts @@ -0,0 +1,3 @@ +export function upperCaseFirst(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1) +} diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json index dea9c8e9f1a7f..9fa65c92eba2f 100644 --- a/packages/utils/tsconfig.json +++ b/packages/utils/tsconfig.json @@ -15,13 +15,15 @@ "strictFunctionTypes": true, "noImplicitThis": true, "allowJs": true, - "skipLibCheck": true + "skipLibCheck": true, + "downlevelIteration": true }, "include": ["src"], "exclude": [ "dist", "./src/**/__tests__", "./src/**/__mocks__", + "./src/**/__fixtures__", "node_modules" ] } diff --git a/www/docs/sidebars.js b/www/docs/sidebars.js index a000b46e27db2..d3998cd021cde 100644 --- a/www/docs/sidebars.js +++ b/www/docs/sidebars.js @@ -2244,15 +2244,6 @@ module.exports = { sidebar_icon: "javascript", }, }, - { - type: "doc", - id: "js-client/overview", - label: "Medusa JS Client", - customProps: { - sidebar_is_title: true, - sidebar_icon: "javascript", - }, - }, { type: "category", collapsed: false, diff --git a/yarn.lock b/yarn.lock index 4adaf2afa1a3a..9540319dbc1b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -255,9 +255,9 @@ __metadata: languageName: node linkType: hard -"@babel/cli@npm:^7.12.1, @babel/cli@npm:^7.14.3, @babel/cli@npm:^7.15.4, @babel/cli@npm:^7.7.5": - version: 7.18.6 - resolution: "@babel/cli@npm:7.18.6" +"@babel/cli@npm:^7.12.10": + version: 7.19.3 + resolution: "@babel/cli@npm:7.19.3" dependencies: "@jridgewell/trace-mapping": ^0.3.8 "@nicolo-ribaudo/chokidar-2": 2.1.8-no-fsevents.3 @@ -265,7 +265,7 @@ __metadata: commander: ^4.0.1 convert-source-map: ^1.1.0 fs-readdir-recursive: ^1.1.0 - glob: ^7.0.0 + glob: ^7.2.0 make-dir: ^2.1.0 slash: ^2.0.0 peerDependencies: @@ -278,13 +278,13 @@ __metadata: bin: babel: ./bin/babel.js babel-external-helpers: ./bin/babel-external-helpers.js - checksum: d9db59862c482a6013ecb89140d337a72b75d1221674cf7276c4a614dac24126f879c8561433c76f73c2683e03db5d5e83d6d0c042ad7a102985f96cc9b99bc3 + checksum: e996aa6a1cde07555ef83782d5809049e6ebecb16884e94acad6eea9a7f6323f6303ee74004a31b29b5ead257ad697f33650b6983e4e9fb14ef1f2908ea5c0a1 languageName: node linkType: hard -"@babel/cli@npm:^7.12.10": - version: 7.19.3 - resolution: "@babel/cli@npm:7.19.3" +"@babel/cli@npm:^7.14.3, @babel/cli@npm:^7.15.4, @babel/cli@npm:^7.7.5": + version: 7.18.6 + resolution: "@babel/cli@npm:7.18.6" dependencies: "@jridgewell/trace-mapping": ^0.3.8 "@nicolo-ribaudo/chokidar-2": 2.1.8-no-fsevents.3 @@ -292,7 +292,7 @@ __metadata: commander: ^4.0.1 convert-source-map: ^1.1.0 fs-readdir-recursive: ^1.1.0 - glob: ^7.2.0 + glob: ^7.0.0 make-dir: ^2.1.0 slash: ^2.0.0 peerDependencies: @@ -305,7 +305,7 @@ __metadata: bin: babel: ./bin/babel.js babel-external-helpers: ./bin/babel-external-helpers.js - checksum: e996aa6a1cde07555ef83782d5809049e6ebecb16884e94acad6eea9a7f6323f6303ee74004a31b29b5ead257ad697f33650b6983e4e9fb14ef1f2908ea5c0a1 + checksum: d9db59862c482a6013ecb89140d337a72b75d1221674cf7276c4a614dac24126f879c8561433c76f73c2683e03db5d5e83d6d0c042ad7a102985f96cc9b99bc3 languageName: node linkType: hard @@ -2294,7 +2294,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-typescript@npm:^7.13.0, @babel/plugin-transform-typescript@npm:^7.15.4, @babel/plugin-transform-typescript@npm:^7.18.6": +"@babel/plugin-transform-typescript@npm:^7.13.0, @babel/plugin-transform-typescript@npm:^7.18.6": version: 7.18.8 resolution: "@babel/plugin-transform-typescript@npm:7.18.8" dependencies: @@ -2504,7 +2504,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-flow@npm:^7.12.1, @babel/preset-flow@npm:^7.14.0": +"@babel/preset-flow@npm:^7.12.1": version: 7.18.6 resolution: "@babel/preset-flow@npm:7.18.6" dependencies: @@ -6238,12 +6238,8 @@ __metadata: version: 0.0.0-use.local resolution: "@medusajs/medusa-cli@workspace:packages/medusa-cli" dependencies: - "@babel/cli": ^7.7.5 - "@babel/core": ^7.7.5 - "@babel/plugin-proposal-class-properties": ^7.7.4 - "@babel/plugin-transform-runtime": ^7.7.6 - "@babel/preset-env": ^7.7.5 "@medusajs/utils": ^1.9.1 + "@types/yargs": ^15.0.15 axios: ^0.21.4 chalk: ^4.0.0 configstore: 5.0.1 @@ -6269,8 +6265,9 @@ __metadata: resolve-cwd: ^3.0.0 semver: ^7.3.8 stack-trace: ^0.0.10 + ts-jest: ^25.5.1 + typescript: ^4.9.5 ulid: ^2.3.0 - url: ^0.11.0 winston: ^3.8.2 yargs: ^15.3.1 bin: @@ -6461,6 +6458,20 @@ __metadata: languageName: unknown linkType: soft +"@medusajs/orchestration@workspace:packages/orchestration": + version: 0.0.0-use.local + resolution: "@medusajs/orchestration@workspace:packages/orchestration" + dependencies: + "@medusajs/types": ^1.8.7 + "@medusajs/utils": ^1.9.1 + cross-env: ^5.2.1 + graphql: ^16.6.0 + jest: ^25.5.4 + ts-jest: ^25.5.1 + typescript: ^4.4.4 + languageName: unknown + linkType: soft + "@medusajs/product@workspace:packages/product": version: 0.0.0-use.local resolution: "@medusajs/product@workspace:packages/product" @@ -6507,7 +6518,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/types@^1.8.8, @medusajs/types@^1.8.9, @medusajs/types@workspace:^, @medusajs/types@workspace:packages/types": +"@medusajs/types@^1.8.7, @medusajs/types@^1.8.8, @medusajs/types@^1.8.9, @medusajs/types@workspace:^, @medusajs/types@workspace:packages/types": version: 0.0.0-use.local resolution: "@medusajs/types@workspace:packages/types" dependencies: @@ -12623,6 +12634,15 @@ __metadata: languageName: node linkType: hard +"@types/yargs@npm:^15.0.15": + version: 15.0.15 + resolution: "@types/yargs@npm:15.0.15" + dependencies: + "@types/yargs-parser": "*" + checksum: b52519ba68a8d90996b54143ff74fcd8ac1722a1ef4a50ed8c3dbc1f7a76d14210f0262f8b91eabcdab202ff4babdd92ce7332ab1cdd6af4eae7c9fc81c83797 + languageName: node + linkType: hard + "@types/yargs@npm:^16.0.0": version: 16.0.4 resolution: "@types/yargs@npm:16.0.4" @@ -15057,7 +15077,7 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-corejs2@npm:^0.3.0, babel-plugin-polyfill-corejs2@npm:^0.3.1": +"babel-plugin-polyfill-corejs2@npm:^0.3.1": version: 0.3.1 resolution: "babel-plugin-polyfill-corejs2@npm:0.3.1" dependencies: @@ -15272,28 +15292,6 @@ __metadata: languageName: node linkType: hard -"babel-preset-gatsby-package@npm:^2.0.0": - version: 2.18.0 - resolution: "babel-preset-gatsby-package@npm:2.18.0" - dependencies: - "@babel/plugin-proposal-nullish-coalescing-operator": ^7.14.5 - "@babel/plugin-proposal-optional-chaining": ^7.14.5 - "@babel/plugin-syntax-dynamic-import": ^7.8.3 - "@babel/plugin-transform-runtime": ^7.15.0 - "@babel/plugin-transform-typescript": ^7.15.4 - "@babel/preset-env": ^7.15.4 - "@babel/preset-flow": ^7.14.0 - "@babel/preset-react": ^7.14.0 - "@babel/runtime": ^7.15.4 - babel-plugin-dynamic-import-node: ^2.3.3 - babel-plugin-lodash: ^3.3.4 - core-js: ^3.22.3 - peerDependencies: - "@babel/core": ^7.11.6 - checksum: 1c3aeda5169728507258d35e06da3dbb0559a132c8c6e82f6fbd9535aa76ce691451b0bc91398e5381f2c1484b5125dd8bb9473e06ead07bfe03af7d76891492 - languageName: node - linkType: hard - "babel-preset-gatsby@npm:^2.18.0": version: 2.18.0 resolution: "babel-preset-gatsby@npm:2.18.0" @@ -22912,13 +22910,12 @@ __metadata: dependencies: "@types/jest": ^27.0.1 axios: ^0.24.0 - babel-plugin-polyfill-corejs2: ^0.3.0 - babel-preset-gatsby-package: ^2.0.0 gatsby: ^4.1.0 gatsby-core-utils: ^3.3.0 gatsby-plugin-image: ^2.3.0 gatsby-source-filesystem: ^4.3.0 jest: ^27.0.6 + ts-jest: ^27.1.5 tsc-watch: ^4.5.0 typescript: ^4.5.2 peerDependencies: @@ -23798,6 +23795,13 @@ __metadata: languageName: node linkType: hard +"graphql@npm:^16.6.0": + version: 16.6.0 + resolution: "graphql@npm:16.6.0" + checksum: 3a2c15ff58b69d017618d2b224fa6f3c4a7937e1f711c3a5e0948db536b4931e6e649560b53de7cc26735e027ceea6e2d0a6bb7c29fc4639b290313e3aa71618 + languageName: node + linkType: hard + "growly@npm:^1.3.0": version: 1.3.0 resolution: "growly@npm:1.3.0" @@ -30308,10 +30312,6 @@ __metadata: version: 0.0.0-use.local resolution: "medusa-dev-cli@workspace:packages/medusa-dev-cli" dependencies: - "@babel/cli": ^7.12.1 - "@babel/core": ^7.12.3 - "@babel/runtime": ^7.12.5 - babel-preset-medusa-package: ^1.1.19 chokidar: ^3.5.3 configstore: ^5.0.1 cross-env: ^7.0.3 @@ -30325,6 +30325,8 @@ __metadata: jest: ^25.5.4 lodash: ^4.17.21 signal-exit: ^3.0.7 + ts-jest: ^25.5.1 + typescript: ^4.4.4 verdaccio: ^4.10.0 yargs: ^15.4.1 bin: