Skip to content

Commit 2227664

Browse files
feat: query.index (medusajs#11348)
What: - `query.index` helper. It queries the index module, and aggregate the rest of requested fields/relations if needed like `query.graph`. Not covered in this PR: - Hydrate only sub entities returned by the query. Example: 1 out of 5 variants have returned, it should only hydrate the data of the single entity, currently it will merge all the variants of the product. - Generate types of indexed data example: ```ts const query = container.resolve(ContainerRegistrationKeys.QUERY) await query.index({ entity: "product", fields: [ "id", "description", "status", "variants.sku", "variants.barcode", "variants.material", "variants.options.value", "variants.prices.amount", "variants.prices.currency_code", "variants.inventory_items.inventory.sku", "variants.inventory_items.inventory.description", ], filters: { "variants.sku": { $like: "%-1" }, "variants.prices.amount": { $gt: 30 }, }, pagination: { order: { "variants.prices.amount": "DESC", }, }, }) ``` This query return all products where at least one variant has the title ending in `-1` and at least one price bigger than `30`. The Index Module only hold the data used to paginate and filter, and the returned object is: ```json { "id": "prod_01JKEAM2GJZ14K64R0DHK0JE72", "title": null, "variants": [ { "id": "variant_01JKEAM2HC89GWS95F6GF9C6YA", "sku": "extra-variant-1", "prices": [ { "id": "price_01JKEAM2JADEWWX72F8QDP6QXT", "amount": 80, "currency_code": "USD" } ] } ] } ``` All the rest of the fields will be hydrated from their respective modules, and the final result will be: ```json { "id": "prod_01JKEAY2RJTF8TW9A23KTGY1GD", "description": "extra description", "status": "draft", "variants": [ { "sku": "extra-variant-1", "barcode": null, "material": null, "id": "variant_01JKEAY2S945CRZ6X4QZJ7GVBJ", "options": [ { "value": "Red" } ], "prices": [ { "amount": 20, "currency_code": "CAD", "id": "price_01JKEAY2T2EEYSWZHPGG11B7W7" }, { "amount": 80, "currency_code": "USD", "id": "price_01JKEAY2T2NJK2E5468RK84CAR" } ], "inventory_items": [ { "variant_id": "variant_01JKEAY2S945CRZ6X4QZJ7GVBJ", "inventory_item_id": "iitem_01JKEAY2SNY2AWEHPZN0DDXVW6", "inventory": { "sku": "extra-variant-1", "description": "extra variant 1", "id": "iitem_01JKEAY2SNY2AWEHPZN0DDXVW6" } } ] } ] } ``` Co-authored-by: Adrien de Peretti <[email protected]>
1 parent 8d10731 commit 2227664

File tree

19 files changed

+1207
-314
lines changed

19 files changed

+1207
-314
lines changed

.changeset/fresh-beers-visit.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@medusajs/orchestration": patch
3+
"@medusajs/modules-sdk": patch
4+
"@medusajs/types": patch
5+
"@medusajs/index": patch
6+
---
7+
8+
feat: query.index
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
2+
import { RemoteQueryFunction } from "@medusajs/types"
3+
import { ContainerRegistrationKeys, defaultCurrencies } from "@medusajs/utils"
4+
import { setTimeout } from "timers/promises"
5+
import {
6+
adminHeaders,
7+
createAdminUser,
8+
} from "../../../helpers/create-admin-user"
9+
10+
jest.setTimeout(120000)
11+
12+
process.env.ENABLE_INDEX_MODULE = "true"
13+
14+
medusaIntegrationTestRunner({
15+
testSuite: ({ getContainer, dbConnection, api, dbConfig }) => {
16+
let appContainer
17+
18+
beforeAll(() => {
19+
appContainer = getContainer()
20+
})
21+
22+
afterAll(() => {
23+
process.env.ENABLE_INDEX_MODULE = "false"
24+
})
25+
26+
beforeEach(async () => {
27+
await createAdminUser(dbConnection, adminHeaders, appContainer)
28+
})
29+
30+
describe("Index engine - Query.index", () => {
31+
it("should use query.index to query the index module and hydrate the data", async () => {
32+
const shippingProfile = (
33+
await api.post(
34+
`/admin/shipping-profiles`,
35+
{ name: "Test", type: "default" },
36+
adminHeaders
37+
)
38+
).data.shipping_profile
39+
40+
const payload = [
41+
{
42+
title: "Test Product",
43+
description: "test-product-description",
44+
shipping_profile_id: shippingProfile.id,
45+
options: [{ title: "Denominations", values: ["100"] }],
46+
variants: [
47+
{
48+
title: `Test variant 1`,
49+
sku: `test-variant-1`,
50+
prices: [
51+
{
52+
currency_code: Object.values(defaultCurrencies)[0].code,
53+
amount: 30,
54+
},
55+
{
56+
currency_code: Object.values(defaultCurrencies)[2].code,
57+
amount: 50,
58+
},
59+
],
60+
options: {
61+
Denominations: "100",
62+
},
63+
},
64+
],
65+
},
66+
{
67+
title: "Extra product",
68+
description: "extra description",
69+
shipping_profile_id: shippingProfile.id,
70+
options: [{ title: "Colors", values: ["Red"] }],
71+
variants: new Array(2).fill(0).map((_, i) => ({
72+
title: `extra variant ${i}`,
73+
sku: `extra-variant-${i}`,
74+
prices: [
75+
{
76+
currency_code: Object.values(defaultCurrencies)[1].code,
77+
amount: 20,
78+
},
79+
{
80+
currency_code: Object.values(defaultCurrencies)[0].code,
81+
amount: 80,
82+
},
83+
],
84+
options: {
85+
Colors: "Red",
86+
},
87+
})),
88+
},
89+
]
90+
91+
for (const data of payload) {
92+
await api.post("/admin/products", data, adminHeaders).catch((err) => {
93+
console.log(err)
94+
})
95+
}
96+
await setTimeout(5000)
97+
98+
const query = appContainer.resolve(
99+
ContainerRegistrationKeys.QUERY
100+
) as RemoteQueryFunction
101+
102+
const resultset = await query.index({
103+
entity: "product",
104+
fields: [
105+
"id",
106+
"description",
107+
"status",
108+
109+
"variants.sku",
110+
"variants.barcode",
111+
"variants.material",
112+
"variants.options.value",
113+
"variants.prices.amount",
114+
"variants.prices.currency_code",
115+
"variants.inventory_items.inventory.sku",
116+
"variants.inventory_items.inventory.description",
117+
],
118+
filters: {
119+
"variants.sku": { $like: "%-1" },
120+
"variants.prices.amount": { $gt: 30 },
121+
},
122+
pagination: {
123+
order: {
124+
"variants.prices.amount": "DESC",
125+
},
126+
},
127+
})
128+
129+
expect(resultset.data).toEqual([
130+
{
131+
id: expect.any(String),
132+
description: "extra description",
133+
status: "draft",
134+
variants: [
135+
{
136+
sku: "extra-variant-0",
137+
barcode: null,
138+
material: null,
139+
id: expect.any(String),
140+
options: [
141+
{
142+
value: "Red",
143+
},
144+
],
145+
inventory_items: [
146+
{
147+
variant_id: expect.any(String),
148+
inventory_item_id: expect.any(String),
149+
inventory: {
150+
sku: "extra-variant-0",
151+
description: "extra variant 0",
152+
id: expect.any(String),
153+
},
154+
},
155+
],
156+
prices: expect.arrayContaining([]),
157+
},
158+
{
159+
sku: "extra-variant-1",
160+
barcode: null,
161+
material: null,
162+
id: expect.any(String),
163+
options: [
164+
{
165+
value: "Red",
166+
},
167+
],
168+
prices: expect.arrayContaining([
169+
{
170+
amount: 20,
171+
currency_code: "CAD",
172+
id: expect.any(String),
173+
},
174+
{
175+
amount: 80,
176+
currency_code: "USD",
177+
id: expect.any(String),
178+
},
179+
]),
180+
inventory_items: [
181+
{
182+
variant_id: expect.any(String),
183+
inventory_item_id: expect.any(String),
184+
inventory: {
185+
sku: "extra-variant-1",
186+
description: "extra variant 1",
187+
id: expect.any(String),
188+
},
189+
},
190+
],
191+
},
192+
],
193+
},
194+
{
195+
id: expect.any(String),
196+
description: "test-product-description",
197+
status: "draft",
198+
variants: [
199+
{
200+
sku: "test-variant-1",
201+
barcode: null,
202+
material: null,
203+
id: expect.any(String),
204+
options: [
205+
{
206+
value: "100",
207+
},
208+
],
209+
prices: expect.arrayContaining([
210+
{
211+
amount: 30,
212+
currency_code: "USD",
213+
id: expect.any(String),
214+
},
215+
{
216+
amount: 50,
217+
currency_code: "EUR",
218+
id: expect.any(String),
219+
},
220+
]),
221+
inventory_items: [
222+
{
223+
variant_id: expect.any(String),
224+
inventory_item_id: expect.any(String),
225+
inventory: {
226+
sku: "test-variant-1",
227+
description: "Test variant 1",
228+
id: expect.any(String),
229+
},
230+
},
231+
],
232+
},
233+
],
234+
},
235+
])
236+
})
237+
})
238+
},
239+
})

integration-tests/modules/__tests__/index/sync.spec.ts

-2
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,6 @@ medusaIntegrationTestRunner({
198198
expect(updatedResults.length).toBe(1)
199199
expect(updatedResults[0].variants.length).toBe(1)
200200

201-
/*
202201
let staledRaws = await dbConnection.raw(
203202
'SELECT * FROM "index_data" WHERE "staled_at" IS NOT NULL'
204203
)
@@ -209,7 +208,6 @@ medusaIntegrationTestRunner({
209208
'SELECT * FROM "index_relation" WHERE "staled_at" IS NOT NULL'
210209
)
211210
expect(staledRaws.rows.length).toBe(0)
212-
*/
213211
})
214212
},
215213
})

packages/core/modules-sdk/src/medusa-app.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { RemoteFetchDataCallback } from "@medusajs/orchestration"
22
import {
33
ExternalModuleDeclaration,
4+
IIndexService,
45
ILinkMigrationsPlanner,
56
InternalModuleDeclaration,
67
LoadedModule,
@@ -27,13 +28,13 @@ import {
2728
promiseAll,
2829
} from "@medusajs/utils"
2930
import { asValue } from "awilix"
31+
import { Link } from "./link"
3032
import {
3133
MedusaModule,
3234
MigrationOptions,
3335
ModuleBootstrapOptions,
3436
RegisterModuleJoinerConfig,
3537
} from "./medusa-module"
36-
import { Link } from "./link"
3738
import { createQuery, RemoteQuery } from "./remote-query"
3839
import { MODULE_SCOPE } from "./types"
3940

@@ -562,13 +563,20 @@ async function MedusaApp_({
562563
return getMigrationPlanner(options, linkModules)
563564
}
564565

566+
const indexModule = sharedContainer_.resolve(Modules.INDEX, {
567+
allowUnregistered: true,
568+
}) as IIndexService
569+
565570
return {
566571
onApplicationShutdown,
567572
onApplicationPrepareShutdown,
568573
onApplicationStart,
569574
modules: allModules,
570575
link: remoteLink,
571-
query: createQuery(remoteQuery) as any, // TODO: rm any once we remove the old RemoteQueryFunction and rely on the Query object instead,
576+
query: createQuery({
577+
remoteQuery,
578+
indexModule,
579+
}) as any, // TODO: rm any once we remove the old RemoteQueryFunction and rely on the Query object instead,
572580
entitiesMap,
573581
gqlSchema: schema,
574582
notFound,

packages/core/modules-sdk/src/remote-query/__fixtures__/parse-filters.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import { ModuleJoinerConfig } from "@medusajs/types"
12
import { defineJoinerConfig } from "@medusajs/utils"
23
import { MedusaModule } from "../../medusa-module"
3-
import { ModuleJoinerConfig } from "@medusajs/types"
44

55
const customModuleJoinerConfig = defineJoinerConfig("custom_user", {
66
schema: `
@@ -62,12 +62,12 @@ const pricingJoinerConfig = defineJoinerConfig("pricing", {
6262
}
6363
6464
type Price {
65-
amount: Int
65+
amount: Float
6666
deep_nested_price: DeepNestedPrice
6767
}
6868
6969
type DeepNestedPrice {
70-
amount: Int
70+
amount: Float
7171
}
7272
`,
7373
alias: [

0 commit comments

Comments
 (0)