chore(orchestration): remote joiner query planner (#13364)

What:
 - Added query planning to the Remote Joiner, enabling phased and parallel execution of data aggregation.
- Replaced object deletes with non-enumerable property hiding to improve performance.
This commit is contained in:
Carlos R. L. Rodrigues
2025-09-04 11:18:02 -03:00
committed by GitHub
parent b7fef5b7ef
commit bd571aca82
16 changed files with 1234 additions and 191 deletions

View File

@@ -0,0 +1,26 @@
const { defineConfig } = require("@medusajs/framework/utils")
const DB_HOST = process.env.DB_HOST
const DB_USERNAME = process.env.DB_USERNAME
const DB_PASSWORD = process.env.DB_PASSWORD
const DB_NAME = process.env.DB_TEMP_NAME
const DB_URL = `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}`
process.env.DATABASE_URL = DB_URL
module.exports = defineConfig({
admin: {
disable: true,
},
projectConfig: {
http: {
jwtSecret: "secret",
},
},
modules: [
{
key: "translation",
resolve: "./src/modules/translation",
},
],
})

View File

@@ -0,0 +1,8 @@
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/utils"
import Translation from "../modules/translation"
export default defineLink(
ProductModule.linkable.productOption.id,
Translation.linkable.translation.id
)

View File

@@ -0,0 +1,8 @@
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/utils"
import Translation from "../modules/translation"
export default defineLink(
ProductModule.linkable.productCategory.id,
Translation.linkable.translation.id
)

View File

@@ -0,0 +1,8 @@
import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"
import Translation from "../modules/translation"
export default defineLink(
ProductModule.linkable.product.id,
Translation.linkable.translation.id
)

View File

@@ -0,0 +1,8 @@
import ProductModule from "@medusajs/medusa/product"
import { defineLink } from "@medusajs/utils"
import Translation from "../modules/translation"
export default defineLink(
ProductModule.linkable.productVariant.id,
Translation.linkable.translation.id
)

View File

@@ -0,0 +1,8 @@
import { Module } from "@medusajs/framework/utils";
import { TranslationModule } from "./service";
export const TRANSLATION = "translation";
export default Module(TRANSLATION, {
service: TranslationModule,
});

View File

@@ -0,0 +1,95 @@
{
"namespaces": [
"public"
],
"name": "public",
"tables": [
{
"columns": {
"id": {
"name": "id",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"key": {
"name": "key",
"type": "text",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "text"
},
"value": {
"name": "value",
"type": "jsonb",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"default": "'{}'",
"mappedType": "json"
},
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"deleted_at": {
"name": "deleted_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"length": 6,
"mappedType": "datetime"
}
},
"name": "translation",
"schema": "public",
"indexes": [
{
"keyName": "IDX_translation_key_unique",
"columnNames": [],
"composite": false,
"primary": false,
"unique": false,
"expression": "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_translation_key_unique\" ON \"translation\" (key) WHERE deleted_at IS NULL"
},
{
"keyName": "translation_pkey",
"columnNames": [
"id"
],
"composite": false,
"primary": true,
"unique": true
}
],
"checks": [],
"foreignKeys": {}
}
]
}

View File

@@ -0,0 +1,14 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20240907134741 extends Migration {
async up(): Promise<void> {
this.addSql('create table if not exists "translation" ("id" text not null, "key" text not null, "value" jsonb not null default \'{}\', "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "translation_pkey" primary key ("id"));');
this.addSql('CREATE UNIQUE INDEX IF NOT EXISTS "IDX_translation_key_unique" ON "translation" (key) WHERE deleted_at IS NULL;');
}
async down(): Promise<void> {
this.addSql('drop table if exists "translation" cascade;');
}
}

View File

@@ -0,0 +1 @@
export { default as Translation } from "./translation";

View File

@@ -0,0 +1,7 @@
import { model } from "@medusajs/framework/utils";
export default model.define("translation", {
id: model.id({ prefix: "i18n" }).primaryKey(),
key: model.text().unique(),
value: model.json().default({}),
});

View File

@@ -0,0 +1,49 @@
import { MedusaService } from "@medusajs/framework/utils"
import { Translation } from "./models"
export class TranslationModule extends MedusaService({
Translation,
}) {
private manager_
constructor({ manager }) {
super(...arguments)
this.manager_ = manager
}
// @ts-expect-error
async listTranslations(find, config, medusaContext) {
const { filters, context, id } = find ?? {}
let lang = null
if (filters || context) {
lang = filters?.lang ?? context?.lang
delete filters?.lang
}
const knex = this.manager_.getKnex()
const q = knex({ tr: "translation" }).select(["tr.id", "tr.key"])
// Select JSON content for a specific lang if provided
if (lang) {
q.select(
knex.raw("tr.value->? AS content", [lang]),
knex.raw("? AS lang", [lang])
)
} else {
q.select("tr.value")
}
const key = filters?.key
if (id) {
q.whereIn("tr.id", Array.isArray(id) ? id : [id])
} else if (key) {
q.whereIn("tr.key", Array.isArray(key) ? key : [key])
}
// console.log(q.toString())
return await q
}
}

View File

@@ -0,0 +1,554 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import path from "path"
jest.setTimeout(100000)
import { createProductsWorkflow } from "@medusajs/core-flows"
import { Modules } from "@medusajs/utils"
import { TranslationModule } from "../__fixtures__/translation-test/src/modules/translation/service"
const createTranslations = async (container, inputs) => {
const translationModule: any = container.resolve("translation")
const created = await translationModule.createTranslations(inputs as any)
return Array.isArray(created) ? created : [created]
}
const attachTranslationToProduct = async (
container,
{ productId, translation }
) => {
const [created] = await createTranslations(container, [translation])
const remoteLink: any = container.resolve("remoteLink")
await remoteLink.create({
[Modules.PRODUCT]: { product_id: productId },
translation: { translation_id: created.id },
})
return created
}
const attachTranslationToVariant = async (
container,
{ variantId, translation }
) => {
const [created] = await createTranslations(container, [translation])
const remoteLink: any = container.resolve("remoteLink")
await remoteLink.create({
[Modules.PRODUCT]: { product_variant_id: variantId },
translation: { translation_id: created.id },
})
return created
}
const attachTranslationToOption = async (
container,
{ optionId, translation }
) => {
const [created] = await createTranslations(container, [translation])
const remoteLink: any = container.resolve("remoteLink")
await remoteLink.create({
[Modules.PRODUCT]: { product_option_id: optionId },
translation: { translation_id: created.id },
})
return created
}
const attachTranslationToProductCategory = async (
container,
{ categoryId, translation }
) => {
const [created] = await createTranslations(container, [translation])
const remoteLink: any = container.resolve("remoteLink")
await remoteLink.create({
[Modules.PRODUCT]: { product_category_id: categoryId },
translation: { translation_id: created.id },
})
return created
}
medusaIntegrationTestRunner({
cwd: path.join(__dirname, "../__fixtures__/translation-test"),
testSuite: ({ getContainer }) => {
describe("query.graph()", () => {
beforeEach(async () => {
const container = getContainer()
const productService: any = container.resolve("product")
const categories = await Promise.all(
[1, 2, 3].map((i) =>
productService.createProductCategories({
name: `Category ${i}`,
})
)
)
const buildProduct = (i: number, categoryId: string) => ({
title: `Product ${i}`,
category_ids: [categoryId],
options: [
{
title: "size",
values: ["small", "large"],
},
],
variants: [
{
title: `P${i} Variant 1`,
options: { size: "small" },
prices: [
{
amount: 10,
currency_code: "usd",
},
],
},
{
title: `P${i} Variant 2`,
options: { size: "large" },
prices: [
{
amount: 20,
currency_code: "usd",
},
],
},
],
})
const createdProducts = await Promise.all(
[1, 2, 3].map(
async (i) =>
await createProductsWorkflow(container).run({
input: {
products: [buildProduct(i, categories[i - 1].id)],
},
})
)
)
const productsWithRels = await Promise.all(
createdProducts.map((p) =>
productService.retrieveProduct(p.result[0].id, {
relations: [
"variants",
"options",
"options.values",
"categories",
],
})
)
)
await Promise.all(
productsWithRels.map(async (p, idx) => {
const i = idx + 1
await attachTranslationToProduct(getContainer(), {
productId: p.id,
translation: {
key: p.id,
value: {
pt: { title: `Produto ${i}` },
fr: { title: `Produit ${i}` },
},
},
})
const cat = p.categories?.[0]
if (cat) {
await attachTranslationToProductCategory(getContainer(), {
categoryId: cat.id,
translation: {
key: cat.id,
value: {
pt: { name: `Categoria ${i}` },
fr: { name: `Catégorie ${i}` },
},
},
})
}
const opt = p.options?.[0]
if (opt) {
await attachTranslationToOption(getContainer(), {
optionId: opt.id,
translation: {
key: opt.id,
value: {
pt: { title: "Tamanho" },
fr: { title: "Taille" },
},
},
})
}
await Promise.all(
(p.variants || []).map((v, vi) => {
const variantNumber = v.title.split("").pop()
return attachTranslationToVariant(getContainer(), {
variantId: v.id,
translation: {
key: v.id,
value: {
pt: { title: `Variante ${variantNumber}` },
fr: { title: `Variante ${variantNumber}` },
},
},
})
})
)
})
)
})
it("should call same entity in different levels (variant)", async () => {
const container = getContainer()
const query = container.resolve("query")
const productService = container.resolve(Modules.PRODUCT)
const inventoryService = container.resolve(Modules.INVENTORY)
const productServiceSpy = jest.spyOn(
productService,
"listProductVariants"
)
const inventoryServiceSpy = jest.spyOn(
inventoryService,
"listInventoryItems"
)
const result = await query.graph({
entity: "variants",
fields: [
"id",
"manage_inventory",
"inventory.id",
"inventory.variants.id",
],
})
expect(productServiceSpy).toHaveBeenCalledTimes(2)
expect(inventoryServiceSpy).toHaveBeenCalledTimes(1)
})
it("should call services in correct order with parallel execution where possible", async () => {
const container = getContainer()
const query = container.resolve("query")
const productService = container.resolve(Modules.PRODUCT)
const priceService = container.resolve(Modules.PRICING)
const translationService = container.resolve(
"translation"
) as TranslationModule
const productServiceSpy = jest.spyOn(productService, "listProducts")
const translationServiceSpy = jest.spyOn(
translationService,
"listTranslations"
)
const priceServiceSpy = jest.spyOn(priceService, "listPriceSets")
// Execute the query
const result = await query.graph({
entity: "product",
fields: [
"sales_channels.name",
"title",
"translation.*",
"categories.name",
"categories.translation.*",
"variants.title",
"variants.translation.*",
"options.title",
"options.translation.*",
"variants.prices.amount",
"variants.prices.currency_code",
],
})
expect(productServiceSpy.mock.calls[0][1]).toEqual({
select: [
"title",
"variants_id",
"id",
"categories.name",
"categories.id",
"variants.title",
"variants.id",
"options.title",
"options.id",
],
relations: ["categories", "variants", "options"],
args: {},
})
expect(translationServiceSpy.mock.calls[0][0].id).toHaveLength(3)
expect(translationServiceSpy.mock.calls[1][0].id).toHaveLength(12)
expect(priceServiceSpy.mock.calls[0][0].id).toHaveLength(6)
expect(result.data).toEqual(
expect.arrayContaining([
expect.objectContaining({
title: "Product 3",
categories: [
expect.objectContaining({
name: "Category 3",
translation: expect.objectContaining({
value: {
fr: {
name: "Catégorie 3",
},
pt: {
name: "Categoria 3",
},
},
}),
}),
],
variants: expect.arrayContaining([
expect.objectContaining({
title: "P3 Variant 2",
translation: expect.objectContaining({
value: {
fr: {
title: "Variante 2",
},
pt: {
title: "Variante 2",
},
},
}),
prices: expect.arrayContaining([
expect.objectContaining({
amount: 20,
currency_code: "usd",
}),
]),
}),
expect.objectContaining({
title: "P3 Variant 1",
translation: expect.objectContaining({
value: {
fr: {
title: "Variante 1",
},
pt: {
title: "Variante 1",
},
},
}),
prices: expect.arrayContaining([
expect.objectContaining({
amount: 10,
currency_code: "usd",
}),
]),
}),
]),
options: expect.arrayContaining([
expect.objectContaining({
title: "size",
translation: expect.objectContaining({
value: {
fr: {
title: "Taille",
},
pt: {
title: "Tamanho",
},
},
}),
}),
]),
sales_channels: [],
translation: expect.objectContaining({
value: {
fr: {
title: "Produit 3",
},
pt: {
title: "Produto 3",
},
},
}),
}),
expect.objectContaining({
title: "Product 1",
categories: [
expect.objectContaining({
name: "Category 1",
translation: expect.objectContaining({
value: {
fr: {
name: "Catégorie 1",
},
pt: {
name: "Categoria 1",
},
},
}),
}),
],
variants: expect.arrayContaining([
expect.objectContaining({
title: "P1 Variant 2",
translation: expect.objectContaining({
value: {
fr: {
title: "Variante 2",
},
pt: {
title: "Variante 2",
},
},
}),
prices: expect.arrayContaining([
expect.objectContaining({
amount: 20,
currency_code: "usd",
}),
]),
}),
expect.objectContaining({
title: "P1 Variant 1",
translation: expect.objectContaining({
value: {
fr: {
title: "Variante 1",
},
pt: {
title: "Variante 1",
},
},
}),
prices: expect.arrayContaining([
expect.objectContaining({
amount: 10,
currency_code: "usd",
}),
]),
}),
]),
options: expect.arrayContaining([
expect.objectContaining({
title: "size",
translation: expect.objectContaining({
value: {
fr: {
title: "Taille",
},
pt: {
title: "Tamanho",
},
},
}),
}),
]),
sales_channels: [],
translation: expect.objectContaining({
value: {
fr: {
title: "Produit 1",
},
pt: {
title: "Produto 1",
},
},
}),
}),
expect.objectContaining({
title: "Product 2",
categories: [
expect.objectContaining({
name: "Category 2",
translation: expect.objectContaining({
value: {
fr: {
name: "Catégorie 2",
},
pt: {
name: "Categoria 2",
},
},
}),
}),
],
variants: expect.arrayContaining([
expect.objectContaining({
title: "P2 Variant 1",
translation: expect.objectContaining({
value: {
fr: {
title: "Variante 1",
},
pt: {
title: "Variante 1",
},
},
}),
prices: expect.arrayContaining([
expect.objectContaining({
amount: 10,
currency_code: "usd",
}),
]),
}),
expect.objectContaining({
title: "P2 Variant 2",
translation: expect.objectContaining({
value: {
fr: {
title: "Variante 2",
},
pt: {
title: "Variante 2",
},
},
}),
prices: expect.arrayContaining([
expect.objectContaining({
amount: 20,
currency_code: "usd",
}),
]),
}),
]),
options: expect.arrayContaining([
expect.objectContaining({
title: "size",
translation: expect.objectContaining({
value: {
fr: {
title: "Taille",
},
pt: {
title: "Tamanho",
},
},
}),
}),
]),
sales_channels: [],
translation: expect.objectContaining({
value: {
fr: {
title: "Produit 2",
},
pt: {
title: "Produto 2",
},
},
}),
}),
])
)
})
})
},
})