feat: Remote Joiner (#4098)
This commit is contained in:
1 parent
9dcdc0041a
commit
499c3478c9
20 files changed
+2227
-5
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/orchestration": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
"@medusajs/utils": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat: Remote Joiner
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
globals: {
|
||||||
|
"ts-jest": {
|
||||||
|
tsConfig: "tsconfig.json",
|
||||||
|
isolatedModules: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
transform: {
|
||||||
|
"^.+\\.[jt]s?$": "ts-jest",
|
||||||
|
},
|
||||||
|
testEnvironment: `node`,
|
||||||
|
moduleFileExtensions: [`js`, `ts`],
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -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"),
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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]) },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./joiner"
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./remote-joiner"
|
||||||
@@ -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<string, JoinerServiceConfig> = new Map()
|
||||||
|
|
||||||
|
private static filterFields(
|
||||||
|
data: any,
|
||||||
|
fields: string[],
|
||||||
|
expands?: RemoteNestedExpands
|
||||||
|
): Record<string, unknown> {
|
||||||
|
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<string, any> {
|
||||||
|
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<string, JoinerRelationship[]> = 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<string, RemoteExpandProperty>
|
||||||
|
): Promise<void> {
|
||||||
|
if (!parsedExpands) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const stack: [
|
||||||
|
any[],
|
||||||
|
RemoteJoinerQuery,
|
||||||
|
Map<string, RemoteExpandProperty>,
|
||||||
|
string,
|
||||||
|
Set<string>
|
||||||
|
][] = [[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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<string, RemoteExpandProperty> {
|
||||||
|
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<string, RemoteExpandProperty> {
|
||||||
|
const parsedExpands = new Map<string, any>()
|
||||||
|
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<string, RemoteExpandProperty>
|
||||||
|
): Map<string, RemoteExpandProperty> {
|
||||||
|
const sortedParsedExpands = new Map(
|
||||||
|
Array.from(parsedExpands.entries()).sort()
|
||||||
|
)
|
||||||
|
|
||||||
|
const mergedExpands = new Map<string, RemoteExpandProperty>(
|
||||||
|
sortedParsedExpands
|
||||||
|
)
|
||||||
|
const mergedPaths = new Map<string, string>()
|
||||||
|
|
||||||
|
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<any> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ export * from "./cache"
|
|||||||
export * from "./common"
|
export * from "./common"
|
||||||
export * from "./event-bus"
|
export * from "./event-bus"
|
||||||
export * from "./inventory"
|
export * from "./inventory"
|
||||||
|
export * from "./joiner"
|
||||||
export * from "./modules-sdk"
|
export * from "./modules-sdk"
|
||||||
export * from "./product"
|
export * from "./product"
|
||||||
export * from "./product-category"
|
export * from "./product-category"
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -34,6 +34,6 @@
|
|||||||
"prepare": "cross-env NODE_ENV=production yarn run build",
|
"prepare": "cross-env NODE_ENV=production yarn run build",
|
||||||
"build": "tsc --build",
|
"build": "tsc --build",
|
||||||
"watch": "tsc --build --watch",
|
"watch": "tsc --build --watch",
|
||||||
"test": "jest --passWithNoTests src"
|
"test": "jest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ export * from "./build-query"
|
|||||||
export * from "./errors"
|
export * from "./errors"
|
||||||
export * from "./generate-entity-id"
|
export * from "./generate-entity-id"
|
||||||
export * from "./get-config-file"
|
export * from "./get-config-file"
|
||||||
|
export * from "./handle-postgres-database-error"
|
||||||
export * from "./is-date"
|
export * from "./is-date"
|
||||||
export * from "./is-defined"
|
export * from "./is-defined"
|
||||||
export * from "./is-email"
|
export * from "./is-email"
|
||||||
@@ -17,5 +18,6 @@ export * from "./wrap-handler"
|
|||||||
export * from "./to-kebab-case"
|
export * from "./to-kebab-case"
|
||||||
export * from "./to-camel-case"
|
export * from "./to-camel-case"
|
||||||
export * from "./stringify-circular"
|
export * from "./stringify-circular"
|
||||||
export * from "./build-query"
|
export * from "./to-kebab-case"
|
||||||
export * from "./handle-postgres-database-error"
|
export * from "./to-pascal-case"
|
||||||
|
export * from "./wrap-handler"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export function toPascalCase(s: string): string {
|
||||||
|
return s.replace(/(^\w|_\w)/g, (match) =>
|
||||||
|
match.replace(/_/g, "").toUpperCase()
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -15,13 +15,15 @@
|
|||||||
"strictFunctionTypes": true,
|
"strictFunctionTypes": true,
|
||||||
"noImplicitThis": true,
|
"noImplicitThis": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"downlevelIteration": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"dist",
|
"dist",
|
||||||
"./src/**/__tests__",
|
"./src/**/__tests__",
|
||||||
"./src/**/__mocks__",
|
"./src/**/__mocks__",
|
||||||
|
"./src/**/__fixtures__",
|
||||||
"node_modules"
|
"node_modules"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -6458,6 +6458,20 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
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":
|
"@medusajs/product@workspace:packages/product":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@medusajs/product@workspace:packages/product"
|
resolution: "@medusajs/product@workspace:packages/product"
|
||||||
@@ -6504,7 +6518,7 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
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
|
version: 0.0.0-use.local
|
||||||
resolution: "@medusajs/types@workspace:packages/types"
|
resolution: "@medusajs/types@workspace:packages/types"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -23781,6 +23795,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"growly@npm:^1.3.0":
|
||||||
version: 1.3.0
|
version: 1.3.0
|
||||||
resolution: "growly@npm:1.3.0"
|
resolution: "growly@npm:1.3.0"
|
||||||
|
|||||||
Reference in new issue
Block a user