feat(orchestration): hydrate resultset (#11263)
What: * Add support for aggregating data into existing resultset. * `query.graph` new option `initialData` containing the resultset to be hydrated * It fetches data where the requested fields are not present and merge with the existing resultset
This commit is contained in:
1 parent
c8376a9f15
commit
65fae943c9
4 files changed
+230
-17
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/orchestration": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(orchestration): hydrate resultset
|
||||||
@@ -849,4 +849,151 @@ describe("RemoteJoiner", () => {
|
|||||||
"order: Primary key(s) [id] not found in filters"
|
"order: Primary key(s) [id] not found in filters"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("Should merge initial data with data fetched", async () => {
|
||||||
|
const query = RemoteJoiner.parseQuery(`
|
||||||
|
query {
|
||||||
|
order {
|
||||||
|
id
|
||||||
|
number
|
||||||
|
products {
|
||||||
|
product {
|
||||||
|
handler
|
||||||
|
user {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
const initialData = [
|
||||||
|
{
|
||||||
|
id: 201,
|
||||||
|
extra_field: "extra",
|
||||||
|
metadata: {
|
||||||
|
some: "data",
|
||||||
|
},
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
product_id: 101,
|
||||||
|
color: "red",
|
||||||
|
product: {
|
||||||
|
id: 101,
|
||||||
|
product_extra_field: "extra 101 - red",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product_id: 101,
|
||||||
|
color: "green",
|
||||||
|
product: {
|
||||||
|
id: 101,
|
||||||
|
product_extra_field: "extra 101 - green",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 205,
|
||||||
|
extra_field: "extra",
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
product_id: [101, 103],
|
||||||
|
product: [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
color: "blue",
|
||||||
|
product_extra_field: "extra 101 - blue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 103,
|
||||||
|
color: "yellow",
|
||||||
|
product_extra_field: "extra 101 - yellow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const data = await joiner.query(query, {
|
||||||
|
initialData,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(data).toEqual([
|
||||||
|
{
|
||||||
|
id: 201,
|
||||||
|
number: "ORD-001",
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
product_id: 101,
|
||||||
|
color: "red",
|
||||||
|
product: {
|
||||||
|
id: 101,
|
||||||
|
product_extra_field: "extra 101 - red",
|
||||||
|
handler: "product-1-handler",
|
||||||
|
user_id: 2,
|
||||||
|
user: {
|
||||||
|
name: "Jane Doe",
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
product_id: 101,
|
||||||
|
color: "green",
|
||||||
|
product: {
|
||||||
|
id: 101,
|
||||||
|
product_extra_field: "extra 101 - green",
|
||||||
|
handler: "product-1-handler",
|
||||||
|
user_id: 2,
|
||||||
|
user: {
|
||||||
|
name: "Jane Doe",
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extra_field: "extra",
|
||||||
|
metadata: {
|
||||||
|
some: "data",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 205,
|
||||||
|
number: "ORD-202",
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
product_id: [101, 103],
|
||||||
|
product: [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
color: "blue",
|
||||||
|
product_extra_field: "extra 101 - blue",
|
||||||
|
handler: "product-1-handler",
|
||||||
|
user_id: 2,
|
||||||
|
user: {
|
||||||
|
name: "Jane Doe",
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 103,
|
||||||
|
color: "yellow",
|
||||||
|
product_extra_field: "extra 101 - yellow",
|
||||||
|
handler: "product-3-handler",
|
||||||
|
user_id: 3,
|
||||||
|
user: {
|
||||||
|
name: "aaa bbb",
|
||||||
|
id: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
extra_field: "extra",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -765,35 +765,51 @@ export class RemoteJoiner {
|
|||||||
: relationship.foreignKey.split(".").pop()!
|
: relationship.foreignKey.split(".").pop()!
|
||||||
const fieldsArray = field.split(",")
|
const fieldsArray = field.split(",")
|
||||||
|
|
||||||
const idsToFetch: any[] = []
|
const idsToFetch: Set<any> = new Set()
|
||||||
|
|
||||||
|
const requestedFields = new Set(expand.fields ?? [])
|
||||||
|
const fieldsById = new Map<string, string[]>()
|
||||||
items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
const values = fieldsArray.map((field) => item?.[field])
|
const values = fieldsArray.map((field) => item?.[field])
|
||||||
|
|
||||||
if (values.length === fieldsArray.length && !item?.[relationship.alias]) {
|
if (values.length === fieldsArray.length) {
|
||||||
if (fieldsArray.length === 1) {
|
if (item?.[relationship.alias]) {
|
||||||
if (!idsToFetch.includes(values[0])) {
|
for (const field of requestedFields.values()) {
|
||||||
idsToFetch.push(values[0])
|
if (field in item[relationship.alias]) {
|
||||||
|
requestedFields.delete(field)
|
||||||
|
fieldsById.delete(field)
|
||||||
|
} else {
|
||||||
|
if (!fieldsById.has(field)) {
|
||||||
|
fieldsById.set(field, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldsById
|
||||||
|
.get(field)!
|
||||||
|
.push(fieldsArray.length === 1 ? values[0] : values)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// composite key
|
if (fieldsArray.length === 1) {
|
||||||
const valuesString = values.join(",")
|
idsToFetch.add(values[0])
|
||||||
|
} else {
|
||||||
if (!idsToFetch.some((id) => id.join(",") === valuesString)) {
|
idsToFetch.add(values)
|
||||||
idsToFetch.push(values)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (idsToFetch.length === 0) {
|
for (const values of fieldsById.values()) {
|
||||||
|
values.forEach((v) => idsToFetch.add(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idsToFetch.size === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const relatedDataArray = await this.fetchData({
|
const relatedDataArray = await this.fetchData({
|
||||||
expand,
|
expand,
|
||||||
pkField: field,
|
pkField: field,
|
||||||
ids: idsToFetch,
|
ids: Array.from(idsToFetch),
|
||||||
relationship,
|
relationship,
|
||||||
options,
|
options,
|
||||||
})
|
})
|
||||||
@@ -812,12 +828,31 @@ export class RemoteJoiner {
|
|||||||
)
|
)
|
||||||
|
|
||||||
items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
if (!item || item[relationship.alias]) {
|
if (!item) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemKey = fieldsArray.map((field) => item[field]).join(",")
|
const itemKey = fieldsArray.map((field) => item[field]).join(",")
|
||||||
|
|
||||||
|
if (item[relationship.alias]) {
|
||||||
|
if (Array.isArray(item[field])) {
|
||||||
|
for (let i = 0; i < item[relationship.alias].length; i++) {
|
||||||
|
const it = item[relationship.alias][i]
|
||||||
|
item[relationship.alias][i] = Object.assign(
|
||||||
|
it,
|
||||||
|
relatedDataMap[it[relationship.primaryKey]]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item[relationship.alias] = Object.assign(
|
||||||
|
item[relationship.alias],
|
||||||
|
relatedDataMap[itemKey]
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(item[field])) {
|
if (Array.isArray(item[field])) {
|
||||||
item[relationship.alias] = item[field].map((id) => {
|
item[relationship.alias] = item[field].map((id) => {
|
||||||
if (relationship.isList && !Array.isArray(relatedDataMap[id])) {
|
if (relationship.isList && !Array.isArray(relatedDataMap[id])) {
|
||||||
@@ -874,7 +909,6 @@ export class RemoteJoiner {
|
|||||||
parsedExpands.set(BASE_PATH, initialService)
|
parsedExpands.set(BASE_PATH, initialService)
|
||||||
|
|
||||||
const forwardArgumentsOnPath: string[] = []
|
const forwardArgumentsOnPath: string[] = []
|
||||||
|
|
||||||
for (const expand of expands || []) {
|
for (const expand of expands || []) {
|
||||||
const properties = expand.property.split(".")
|
const properties = expand.property.split(".")
|
||||||
const currentPath: string[] = []
|
const currentPath: string[] = []
|
||||||
@@ -883,7 +917,6 @@ export class RemoteJoiner {
|
|||||||
|
|
||||||
for (const prop of properties) {
|
for (const prop of properties) {
|
||||||
const fieldAlias = currentServiceConfig.fieldAlias ?? {}
|
const fieldAlias = currentServiceConfig.fieldAlias ?? {}
|
||||||
|
|
||||||
if (fieldAlias[prop]) {
|
if (fieldAlias[prop]) {
|
||||||
const aliasPath = [BASE_PATH, ...currentPath, prop].join(".")
|
const aliasPath = [BASE_PATH, ...currentPath, prop].join(".")
|
||||||
|
|
||||||
@@ -901,9 +934,7 @@ export class RemoteJoiner {
|
|||||||
})
|
})
|
||||||
|
|
||||||
currentAliasPath.push(prop)
|
currentAliasPath.push(prop)
|
||||||
|
|
||||||
currentServiceConfig = lastServiceConfig
|
currentServiceConfig = lastServiceConfig
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1202,6 +1233,21 @@ export class RemoteJoiner {
|
|||||||
throw new Error(`Service "${queryObj.service}" was not found.`)
|
throw new Error(`Service "${queryObj.service}" was not found.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const iniDataArray = options?.initialData
|
||||||
|
? Array.isArray(options.initialData)
|
||||||
|
? options.initialData
|
||||||
|
: [options.initialData]
|
||||||
|
: []
|
||||||
|
|
||||||
|
if (options?.initialData) {
|
||||||
|
let pkName = serviceConfig.primaryKeys[0]
|
||||||
|
queryObj.args ??= []
|
||||||
|
queryObj.args.push({
|
||||||
|
name: pkName,
|
||||||
|
value: iniDataArray.map((dt) => dt[pkName]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const { primaryKeyArg, otherArgs, pkName } = gerPrimaryKeysAndOtherFilters({
|
const { primaryKeyArg, otherArgs, pkName } = gerPrimaryKeysAndOtherFilters({
|
||||||
serviceConfig,
|
serviceConfig,
|
||||||
queryObj,
|
queryObj,
|
||||||
@@ -1262,6 +1308,19 @@ export class RemoteJoiner {
|
|||||||
|
|
||||||
const data = response.path ? response.data[response.path!] : response.data
|
const data = response.path ? response.data[response.path!] : response.data
|
||||||
|
|
||||||
|
if (options?.initialData) {
|
||||||
|
// merge initial data with fetched data matching the primary key
|
||||||
|
const initialDataMap = new Map(iniDataArray.map((dt) => [dt[pkName], dt]))
|
||||||
|
for (const resData of data) {
|
||||||
|
const iniData = initialDataMap.get(resData[pkName])
|
||||||
|
|
||||||
|
if (iniData) {
|
||||||
|
Object.assign(resData, iniData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete options?.initialData
|
||||||
|
}
|
||||||
|
|
||||||
await this.handleExpands({
|
await this.handleExpands({
|
||||||
items: Array.isArray(data) ? data : [data],
|
items: Array.isArray(data) ? data : [data],
|
||||||
parsedExpands,
|
parsedExpands,
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export interface RemoteJoinerQuery {
|
|||||||
export interface RemoteJoinerOptions {
|
export interface RemoteJoinerOptions {
|
||||||
throwIfKeyNotFound?: boolean
|
throwIfKeyNotFound?: boolean
|
||||||
throwIfRelationNotFound?: boolean | string[]
|
throwIfRelationNotFound?: boolean | string[]
|
||||||
|
initialData?: object | object[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RemoteNestedExpands {
|
export interface RemoteNestedExpands {
|
||||||
|
|||||||
Reference in new issue
Block a user