From 499c3478c910c8b922a15cc6f4d9fbad122a347f Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Thu, 29 Jun 2023 10:29:01 -0300 Subject: [PATCH] feat: Remote Joiner (#4098) --- .changeset/afraid-otters-train.md | 7 + packages/orchestration/jest.config.js | 13 + packages/orchestration/package.json | 36 ++ .../src/__fixtures__/joiner/data.ts | 189 ++++++ .../src/__mocks__/joiner/mock_data.ts | 118 ++++ .../src/__tests__/joiner/graphql-ast.ts | 239 ++++++++ .../__tests__/joiner/remote-joiner-data.ts | 491 +++++++++++++++ .../src/__tests__/joiner/remote-joiner.ts | 287 +++++++++ packages/orchestration/src/index.ts | 1 + .../orchestration/src/joiner/graphql-ast.ts | 154 +++++ packages/orchestration/src/joiner/index.ts | 1 + .../orchestration/src/joiner/remote-joiner.ts | 575 ++++++++++++++++++ packages/orchestration/tsconfig.json | 29 + packages/types/src/index.ts | 1 + packages/types/src/joiner/index.ts | 51 ++ packages/utils/package.json | 2 +- packages/utils/src/common/index.ts | 6 +- packages/utils/src/common/to-pascal-case.ts | 5 + packages/utils/tsconfig.json | 4 +- yarn.lock | 23 +- 20 files changed, 2227 insertions(+), 5 deletions(-) create mode 100644 .changeset/afraid-otters-train.md create mode 100644 packages/orchestration/jest.config.js create mode 100644 packages/orchestration/package.json create mode 100644 packages/orchestration/src/__fixtures__/joiner/data.ts create mode 100644 packages/orchestration/src/__mocks__/joiner/mock_data.ts create mode 100644 packages/orchestration/src/__tests__/joiner/graphql-ast.ts create mode 100644 packages/orchestration/src/__tests__/joiner/remote-joiner-data.ts create mode 100644 packages/orchestration/src/__tests__/joiner/remote-joiner.ts create mode 100644 packages/orchestration/src/index.ts create mode 100644 packages/orchestration/src/joiner/graphql-ast.ts create mode 100644 packages/orchestration/src/joiner/index.ts create mode 100644 packages/orchestration/src/joiner/remote-joiner.ts create mode 100644 packages/orchestration/tsconfig.json create mode 100644 packages/types/src/joiner/index.ts create mode 100644 packages/utils/src/common/to-pascal-case.ts diff --git a/.changeset/afraid-otters-train.md b/.changeset/afraid-otters-train.md new file mode 100644 index 0000000000..26a15ed789 --- /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/packages/orchestration/jest.config.js b/packages/orchestration/jest.config.js new file mode 100644 index 0000000000..7de5bf104a --- /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 0000000000..6c14321aae --- /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 0000000000..9ce6aadab1 --- /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 0000000000..75ccb7a11b --- /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 0000000000..8ce56afd0a --- /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 0000000000..cd2b20ae38 --- /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 0000000000..5034c85931 --- /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 0000000000..3a2ffb55e4 --- /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 0000000000..c33e9fdb60 --- /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 0000000000..ba55313b31 --- /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 0000000000..799fece99b --- /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 0000000000..9fa65c92eb --- /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/types/src/index.ts b/packages/types/src/index.ts index c73f1c9a69..85be26902f 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" diff --git a/packages/types/src/joiner/index.ts b/packages/types/src/joiner/index.ts new file mode 100644 index 0000000000..3061d3f60f --- /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/utils/package.json b/packages/utils/package.json index 35ac427e1c..379cd200e6 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/index.ts b/packages/utils/src/common/index.ts index 331c6fa027..7270860f2a 100644 --- a/packages/utils/src/common/index.ts +++ b/packages/utils/src/common/index.ts @@ -2,6 +2,7 @@ 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" @@ -17,5 +18,6 @@ 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/to-pascal-case.ts b/packages/utils/src/common/to-pascal-case.ts new file mode 100644 index 0000000000..0d1b794ed2 --- /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/tsconfig.json b/packages/utils/tsconfig.json index dea9c8e9f1..9fa65c92eb 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/yarn.lock b/yarn.lock index 77c5299687..9540319dbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6458,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" @@ -6504,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: @@ -23781,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"