From 6057afdfaac3b7f223c907b3d717e07b1f49042e Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Thu, 27 Nov 2025 09:31:25 +0100 Subject: [PATCH] chore(): Add new regression tests to the remote joiner (#14119) * Add tests * Add tests * Add tests --- .../src/__fixtures__/joiner/data.ts | 58 +++++++++++ .../src/__mocks__/joiner/mock_data.ts | 95 ++++++++++++++++++- .../src/__tests__/joiner/remote-joiner.ts | 93 ++++++++++++++++++ 3 files changed, 244 insertions(+), 2 deletions(-) diff --git a/packages/core/orchestration/src/__fixtures__/joiner/data.ts b/packages/core/orchestration/src/__fixtures__/joiner/data.ts index d6db3eb7f3..be242a0366 100644 --- a/packages/core/orchestration/src/__fixtures__/joiner/data.ts +++ b/packages/core/orchestration/src/__fixtures__/joiner/data.ts @@ -186,4 +186,62 @@ export const remoteJoinerData = { user_id: 1, }, ], + link: [ + { + id: 1, + url: "https://example.com/post-1", + product_id: 101, + post_id: 501, + metadata: { + source: "blog", + category: "tech", + }, + }, + { + id: 2, + url: "https://example.com/post-2", + product_id: 102, + post_id: 502, + metadata: { + source: "news", + category: "general", + }, + }, + { + id: 3, + url: "https://example.com/post-3", + product_id: 103, + post_id: 503, + metadata: { + source: "forum", + category: "discussion", + }, + }, + ], + post: [ + { + id: 501, + title: "First Post", + content: "Content of first post", + author: "John Doe", + published: true, + views: 1000, + }, + { + id: 502, + title: "Second Post", + content: "Content of second post", + author: "Jane Smith", + published: true, + views: 2500, + }, + { + id: 503, + title: "Third Post", + content: "Content of third post", + author: "Bob Johnson", + published: false, + views: 150, + }, + ], } diff --git a/packages/core/orchestration/src/__mocks__/joiner/mock_data.ts b/packages/core/orchestration/src/__mocks__/joiner/mock_data.ts index 517463d383..9a28d089ee 100644 --- a/packages/core/orchestration/src/__mocks__/joiner/mock_data.ts +++ b/packages/core/orchestration/src/__mocks__/joiner/mock_data.ts @@ -1,7 +1,7 @@ -import { JoinerServiceConfig } from "@medusajs/types" +import { JoinerServiceConfig, ModuleJoinerConfig } from "@medusajs/types" import { remoteJoinerData } from "./../../__fixtures__/joiner/data" -export const serviceConfigs: JoinerServiceConfig[] = [ +export const serviceConfigs: (JoinerServiceConfig | ModuleJoinerConfig)[] = [ { serviceName: "user", primaryKeys: ["id"], @@ -49,6 +49,13 @@ export const serviceConfigs: JoinerServiceConfig[] = [ primaryKey: "id", alias: "user", }, + { + foreignKey: "product_id", + primaryKey: "id", + serviceName: "link", + alias: "links", + inverse: true, + }, ], }, { @@ -106,6 +113,74 @@ export const serviceConfigs: JoinerServiceConfig[] = [ }, ], }, + { + serviceName: "link", + isLink: true, + primaryKeys: ["id", "product_id", "post_id"], + relationships: [ + { + serviceName: "product", + entity: "Product", + primaryKey: "id", + foreignKey: "product_id", + alias: "product", + args: { + methodSuffix: "Products", + }, + }, + { + serviceName: "post", + entity: "Post", + primaryKey: "id", + foreignKey: "post_id", + alias: "post", + args: { + methodSuffix: "Posts", + }, + }, + ], + extends: [ + { + serviceName: "product", + entity: "Product", + fieldAlias: { + posts: "links.post", + }, + relationship: { + serviceName: "link", + primaryKey: "id", + foreignKey: "product_id", + alias: "links", + }, + }, + { + serviceName: "post", + entity: "Post", + fieldAlias: { + product: "links.product", + }, + relationship: { + serviceName: "link", + primaryKey: "id", + foreignKey: "post_id", + alias: "links", + }, + }, + ], + }, + { + serviceName: "post", + primaryKeys: ["id"], + relationships: [ + { + serviceName: "link", + primaryKey: "id", + foreignKey: "post_id", + alias: "links", + inverse: true, + }, + ], + }, ] export const mockServiceList = (serviceName) => { @@ -115,6 +190,8 @@ export const mockServiceList = (serviceName) => { productService: remoteJoinerData.product, variantService: remoteJoinerData.variant, orderService: remoteJoinerData.order, + linkService: remoteJoinerData.link, + postService: remoteJoinerData.post, } let resultset = JSON.parse(JSON.stringify(src[serviceName])) @@ -134,6 +211,18 @@ export const mockServiceList = (serviceName) => { resultset = resultset.filter((item) => data.options.id.includes(item.id)) } + // mock filtering on service link + if (serviceName === "linkService" && data.options?.product_id) { + resultset = resultset.filter((item) => + data.options.product_id.includes(item.product_id) + ) + } + + // mock filtering on service post + if (serviceName === "postService" && data.options?.id) { + resultset = resultset.filter((item) => data.options.id.includes(item.id)) + } + return { data: resultset, path: serviceName === "productService" ? "rows" : undefined, @@ -146,4 +235,6 @@ export const serviceMock = { userService: mockServiceList("userService"), productService: mockServiceList("productService"), variantService: mockServiceList("variantService"), + linkService: mockServiceList("linkService"), + postService: mockServiceList("postService"), } diff --git a/packages/core/orchestration/src/__tests__/joiner/remote-joiner.ts b/packages/core/orchestration/src/__tests__/joiner/remote-joiner.ts index 07bd04ef9b..5f264c99b3 100644 --- a/packages/core/orchestration/src/__tests__/joiner/remote-joiner.ts +++ b/packages/core/orchestration/src/__tests__/joiner/remote-joiner.ts @@ -13,6 +13,9 @@ const container = { list: (...args) => { return serviceMock[serviceName].apply(this, args) }, + getByProductId: (...args) => { + return serviceMock[serviceName].apply(this, args) + }, } }, } as MedusaContainer @@ -610,4 +613,94 @@ describe("RemoteJoiner", () => { options: { id: expect.arrayContaining([103, 102]) }, }) }) + + it("should not lose fields when querying with specific nested fields and wildcard on deeply nested relations", async () => { + // This ensures that when we have: + // - A specific field from the root entity (product.name) + // - A specific field from an intermediate relation (links.metadata) + // - A wildcard on a relation accessed through that intermediate (links.post.*) + // ...the intermediate field (links.metadata) is not lost + const query = { + alias: "product", + fields: ["id", "name"], + expands: [ + { + property: "links", + fields: ["metadata"], + }, + { + property: "posts", + fields: ["*"], + }, + ], + args: undefined, + } + + const result = await joiner.query(query) + + expect(serviceMock.productService).toHaveBeenCalledTimes(1) + expect(serviceMock.productService).toHaveBeenCalledWith({ + args: undefined, + fields: expect.arrayContaining(["id", "name"]), + expands: undefined, + options: { id: undefined }, + }) + + expect(serviceMock.linkService).toHaveBeenCalledTimes(1) + expect(serviceMock.linkService).toHaveBeenCalledWith({ + args: undefined, + expands: undefined, + fields: expect.arrayContaining(["metadata", "product_id"]), + options: { product_id: expect.arrayContaining([101, 102, 103]) }, + }) + + expect(serviceMock.postService).toHaveBeenCalledTimes(1) + expect(serviceMock.postService).toHaveBeenCalledWith({ + args: undefined, + expands: undefined, + fields: ["*", "id"], + options: { id: [501, 502, 503] }, // All posts are fetched + }) + + expect(result.rows).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 101, + name: "Product 1", + links: expect.objectContaining({ + metadata: expect.objectContaining({ + source: expect.any(String), + category: expect.any(String), + }), + }), + posts: expect.objectContaining({ + id: 501, + title: expect.any(String), + content: expect.any(String), + author: expect.any(String), + published: expect.any(Boolean), + views: expect.any(Number), + }), + }), + ]) + ) + + // Critical assertion: metadata must not be lost + const firstProduct = result.rows[0] + expect(firstProduct.links).toBeDefined() + expect(firstProduct.links).toHaveProperty("metadata") + expect(firstProduct.links.metadata).toEqual({ + source: "blog", + category: "tech", + }) + + // posts.* should include all fields + expect(firstProduct).toHaveProperty("posts") + expect(firstProduct.posts).toHaveProperty("id") + expect(firstProduct.posts).toHaveProperty("title") + expect(firstProduct.posts).toHaveProperty("content") + expect(firstProduct.posts).toHaveProperty("author") + expect(firstProduct.posts).toHaveProperty("published") + expect(firstProduct.posts).toHaveProperty("views") + }) })