feat: query.index (#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 <25098370+adrien2p@users.noreply.github.com>
This commit is contained in:
1 parent
8d10731343
commit
22276648ad
19 files changed
+1209
-316
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { RemoteFetchDataCallback } from "@medusajs/orchestration"
|
||||
import {
|
||||
ExternalModuleDeclaration,
|
||||
IIndexService,
|
||||
ILinkMigrationsPlanner,
|
||||
InternalModuleDeclaration,
|
||||
LoadedModule,
|
||||
@@ -27,13 +28,13 @@ import {
|
||||
promiseAll,
|
||||
} from "@medusajs/utils"
|
||||
import { asValue } from "awilix"
|
||||
import { Link } from "./link"
|
||||
import {
|
||||
MedusaModule,
|
||||
MigrationOptions,
|
||||
ModuleBootstrapOptions,
|
||||
RegisterModuleJoinerConfig,
|
||||
} from "./medusa-module"
|
||||
import { Link } from "./link"
|
||||
import { createQuery, RemoteQuery } from "./remote-query"
|
||||
import { MODULE_SCOPE } from "./types"
|
||||
|
||||
@@ -562,13 +563,20 @@ async function MedusaApp_({
|
||||
return getMigrationPlanner(options, linkModules)
|
||||
}
|
||||
|
||||
const indexModule = sharedContainer_.resolve(Modules.INDEX, {
|
||||
allowUnregistered: true,
|
||||
}) as IIndexService
|
||||
|
||||
return {
|
||||
onApplicationShutdown,
|
||||
onApplicationPrepareShutdown,
|
||||
onApplicationStart,
|
||||
modules: allModules,
|
||||
link: remoteLink,
|
||||
query: createQuery(remoteQuery) as any, // TODO: rm any once we remove the old RemoteQueryFunction and rely on the Query object instead,
|
||||
query: createQuery({
|
||||
remoteQuery,
|
||||
indexModule,
|
||||
}) as any, // TODO: rm any once we remove the old RemoteQueryFunction and rely on the Query object instead,
|
||||
entitiesMap,
|
||||
gqlSchema: schema,
|
||||
notFound,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { defineJoinerConfig } from "@medusajs/utils"
|
||||
import { MedusaModule } from "../../medusa-module"
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
|
||||
const customModuleJoinerConfig = defineJoinerConfig("custom_user", {
|
||||
schema: `
|
||||
@@ -62,12 +62,12 @@ const pricingJoinerConfig = defineJoinerConfig("pricing", {
|
||||
}
|
||||
|
||||
type Price {
|
||||
amount: Int
|
||||
amount: Float
|
||||
deep_nested_price: DeepNestedPrice
|
||||
}
|
||||
|
||||
type DeepNestedPrice {
|
||||
amount: Int
|
||||
amount: Float
|
||||
}
|
||||
`,
|
||||
alias: [
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
GraphResultSet,
|
||||
IIndexService,
|
||||
RemoteJoinerOptions,
|
||||
RemoteJoinerQuery,
|
||||
RemoteQueryFilters,
|
||||
RemoteQueryFunction,
|
||||
RemoteQueryFunctionReturnPagination,
|
||||
RemoteQueryInput,
|
||||
@@ -21,6 +23,7 @@ import { toRemoteQuery } from "./to-remote-query"
|
||||
*/
|
||||
export class Query {
|
||||
#remoteQuery: RemoteQuery
|
||||
#indexModule: IIndexService
|
||||
|
||||
/**
|
||||
* Method to wrap execution of the graph query for instrumentation
|
||||
@@ -54,8 +57,15 @@ export class Query {
|
||||
},
|
||||
}
|
||||
|
||||
constructor(remoteQuery: RemoteQuery) {
|
||||
constructor({
|
||||
remoteQuery,
|
||||
indexModule,
|
||||
}: {
|
||||
remoteQuery: RemoteQuery
|
||||
indexModule: IIndexService
|
||||
}) {
|
||||
this.#remoteQuery = remoteQuery
|
||||
this.#indexModule = indexModule
|
||||
}
|
||||
|
||||
#unwrapQueryConfig(
|
||||
@@ -172,14 +182,79 @@ export class Query {
|
||||
|
||||
return this.#unwrapRemoteQueryResponse(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Index function uses the Index module to query and hydrates the data with query.graph
|
||||
* returns a result set
|
||||
*/
|
||||
async index<const TEntry extends string>(
|
||||
queryOptions: RemoteQueryInput<TEntry> & {
|
||||
joinFilters?: RemoteQueryFilters<TEntry>
|
||||
},
|
||||
options?: RemoteJoinerOptions
|
||||
): Promise<GraphResultSet<TEntry>> {
|
||||
if (!this.#indexModule) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Index module is not loaded."
|
||||
)
|
||||
}
|
||||
|
||||
const mainEntity = queryOptions.entity
|
||||
|
||||
const fields = queryOptions.fields.map((field) => mainEntity + "." + field)
|
||||
const filters = queryOptions.filters
|
||||
? { [mainEntity]: queryOptions.filters }
|
||||
: ({} as any)
|
||||
const joinFilters = queryOptions.joinFilters
|
||||
? { [mainEntity]: queryOptions.joinFilters }
|
||||
: ({} as any)
|
||||
const pagination = queryOptions.pagination as any
|
||||
if (pagination?.order) {
|
||||
pagination.order = { [mainEntity]: pagination.order }
|
||||
}
|
||||
|
||||
const indexResponse = (await this.#indexModule.query({
|
||||
fields,
|
||||
filters,
|
||||
joinFilters,
|
||||
pagination,
|
||||
})) as unknown as GraphResultSet<TEntry>
|
||||
|
||||
delete queryOptions.pagination
|
||||
delete queryOptions.filters
|
||||
|
||||
let finalResultset: GraphResultSet<TEntry> = indexResponse
|
||||
|
||||
if (indexResponse.data.length) {
|
||||
finalResultset = await this.graph(queryOptions, {
|
||||
...options,
|
||||
initialData: indexResponse.data,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
data: finalResultset.data,
|
||||
metadata: indexResponse.metadata as RemoteQueryFunctionReturnPagination,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API wrapper around the remoteQuery with backward compatibility support
|
||||
* @param remoteQuery
|
||||
*/
|
||||
export function createQuery(remoteQuery: RemoteQuery) {
|
||||
const query = new Query(remoteQuery)
|
||||
export function createQuery({
|
||||
remoteQuery,
|
||||
indexModule,
|
||||
}: {
|
||||
remoteQuery: RemoteQuery
|
||||
indexModule: IIndexService
|
||||
}) {
|
||||
const query = new Query({
|
||||
remoteQuery,
|
||||
indexModule,
|
||||
})
|
||||
|
||||
function backwardCompatibleQuery(...args: any[]) {
|
||||
return query.query.apply(query, args)
|
||||
@@ -187,6 +262,7 @@ export function createQuery(remoteQuery: RemoteQuery) {
|
||||
|
||||
backwardCompatibleQuery.graph = query.graph.bind(query)
|
||||
backwardCompatibleQuery.gql = query.gql.bind(query)
|
||||
backwardCompatibleQuery.index = query.index.bind(query)
|
||||
|
||||
return backwardCompatibleQuery as Omit<RemoteQueryFunction, symbol>
|
||||
}
|
||||
@@ -14,9 +14,10 @@ import {
|
||||
RemoteJoinerQuery,
|
||||
RemoteNestedExpands,
|
||||
} from "@medusajs/types"
|
||||
import { isString, toPascalCase } from "@medusajs/utils"
|
||||
import { isPresent, isString, toPascalCase } from "@medusajs/utils"
|
||||
import { MedusaModule } from "../medusa-module"
|
||||
|
||||
const BASE_PREFIX = ""
|
||||
export class RemoteQuery {
|
||||
private remoteJoiner: RemoteJoiner
|
||||
private modulesMap: Map<string, LoadedModule> = new Map()
|
||||
@@ -99,7 +100,7 @@ export class RemoteQuery {
|
||||
|
||||
public static getAllFieldsAndRelations(
|
||||
expand: RemoteExpandProperty | RemoteNestedExpands[number],
|
||||
prefix = "",
|
||||
prefix = BASE_PREFIX,
|
||||
args: JoinerArgument = {} as JoinerArgument
|
||||
): {
|
||||
select?: string[]
|
||||
@@ -122,7 +123,14 @@ export class RemoteQuery {
|
||||
fields.add(prefix ? `${prefix}.${field}` : field)
|
||||
}
|
||||
|
||||
args[prefix] = expand.args
|
||||
const filters =
|
||||
expand.args?.find((arg) => arg.name === "filters")?.value ?? {}
|
||||
|
||||
if (isPresent(filters)) {
|
||||
args[prefix] = filters
|
||||
} else if (isPresent(expand.args)) {
|
||||
args[prefix] = expand.args
|
||||
}
|
||||
|
||||
for (const property in expand.expands ?? {}) {
|
||||
const newPrefix = prefix ? `${prefix}.${property}` : property
|
||||
@@ -147,7 +155,12 @@ export class RemoteQuery {
|
||||
: shouldSelectAll
|
||||
? undefined
|
||||
: []
|
||||
return { select, relations, args }
|
||||
|
||||
return {
|
||||
select,
|
||||
relations,
|
||||
args,
|
||||
}
|
||||
}
|
||||
|
||||
private hasPagination(options: { [attr: string]: unknown }): boolean {
|
||||
@@ -225,6 +238,15 @@ export class RemoteQuery {
|
||||
filters[keyField] = ids
|
||||
}
|
||||
|
||||
delete options.args?.[BASE_PREFIX]
|
||||
if (Object.keys(options.args ?? {}).length) {
|
||||
filters = {
|
||||
...filters,
|
||||
...options?.args,
|
||||
}
|
||||
options.args = {} as any
|
||||
}
|
||||
|
||||
const hasPagination = this.hasPagination(options)
|
||||
|
||||
let methodName = hasPagination ? "listAndCount" : "list"
|
||||
|
||||
@@ -178,7 +178,7 @@ export const remoteJoinerData = {
|
||||
date: "2023-04-01T12:00:00Z",
|
||||
products: [
|
||||
{
|
||||
product_id: [101, 103],
|
||||
product_id: 103,
|
||||
variant_id: 993,
|
||||
quantity: 4,
|
||||
},
|
||||
|
||||
@@ -59,7 +59,7 @@ export const serviceConfigs: JoinerServiceConfig[] = [
|
||||
fieldAlias: {
|
||||
user_shortcut: "product.user",
|
||||
},
|
||||
primaryKeys: ["id"],
|
||||
primaryKeys: ["id,product_id"],
|
||||
relationships: [
|
||||
{
|
||||
foreignKey: "product_id",
|
||||
|
||||
@@ -291,17 +291,11 @@ describe("RemoteJoiner", () => {
|
||||
date: "2023-04-01T12:00:00Z",
|
||||
products: [
|
||||
{
|
||||
product_id: [101, 103],
|
||||
product: [
|
||||
{
|
||||
name: "Product 1",
|
||||
id: 101,
|
||||
},
|
||||
{
|
||||
name: "Product 3",
|
||||
id: 103,
|
||||
},
|
||||
],
|
||||
product_id: 103,
|
||||
product: {
|
||||
name: "Product 3",
|
||||
id: 103,
|
||||
},
|
||||
},
|
||||
],
|
||||
user_id: 1,
|
||||
@@ -438,19 +432,18 @@ describe("RemoteJoiner", () => {
|
||||
number: "ORD-202",
|
||||
products: [
|
||||
{
|
||||
product_id: [101, 103],
|
||||
product_id: 103,
|
||||
variant_id: 993,
|
||||
quantity: 4,
|
||||
product: [
|
||||
{
|
||||
name: "Product 1",
|
||||
id: 101,
|
||||
},
|
||||
{
|
||||
name: "Product 3",
|
||||
id: 103,
|
||||
},
|
||||
],
|
||||
product: {
|
||||
name: "Product 3",
|
||||
id: 103,
|
||||
},
|
||||
variant: {
|
||||
id: 993,
|
||||
name: "Product variant 33",
|
||||
product_id: 103,
|
||||
},
|
||||
},
|
||||
],
|
||||
id: 205,
|
||||
@@ -468,19 +461,18 @@ describe("RemoteJoiner", () => {
|
||||
number: "ORD-202",
|
||||
products: [
|
||||
{
|
||||
product_id: [101, 103],
|
||||
product_id: 103,
|
||||
variant_id: 993,
|
||||
quantity: 4,
|
||||
product: [
|
||||
{
|
||||
name: "Product 1",
|
||||
id: 101,
|
||||
},
|
||||
{
|
||||
name: "Product 3",
|
||||
id: 103,
|
||||
},
|
||||
],
|
||||
product: {
|
||||
name: "Product 3",
|
||||
id: 103,
|
||||
},
|
||||
variant: {
|
||||
id: 993,
|
||||
name: "Product variant 33",
|
||||
product_id: 103,
|
||||
},
|
||||
},
|
||||
],
|
||||
id: 205,
|
||||
@@ -518,16 +510,10 @@ describe("RemoteJoiner", () => {
|
||||
],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
product_user_alias: [
|
||||
{
|
||||
email: "janedoe@example.com",
|
||||
id: 2,
|
||||
},
|
||||
{
|
||||
email: "aaa@example.com",
|
||||
id: 3,
|
||||
},
|
||||
],
|
||||
product_user_alias: {
|
||||
email: "aaa@example.com",
|
||||
id: 3,
|
||||
},
|
||||
}),
|
||||
])
|
||||
expect(data[0].products[0].product).toEqual(undefined)
|
||||
@@ -564,16 +550,10 @@ describe("RemoteJoiner", () => {
|
||||
],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
product_user_alias: [
|
||||
{
|
||||
email: "janedoe@example.com",
|
||||
id: 2,
|
||||
},
|
||||
{
|
||||
email: "aaa@example.com",
|
||||
id: 3,
|
||||
},
|
||||
],
|
||||
product_user_alias: {
|
||||
email: "aaa@example.com",
|
||||
id: 3,
|
||||
},
|
||||
}),
|
||||
])
|
||||
expect(data[0].products[0].product).toEqual({
|
||||
@@ -619,18 +599,11 @@ describe("RemoteJoiner", () => {
|
||||
],
|
||||
}),
|
||||
expect.objectContaining({
|
||||
product_user_alias: [
|
||||
{
|
||||
name: "Jane Doe",
|
||||
id: 2,
|
||||
email: "janedoe@example.com",
|
||||
},
|
||||
{
|
||||
name: "aaa bbb",
|
||||
id: 3,
|
||||
email: "aaa@example.com",
|
||||
},
|
||||
],
|
||||
product_user_alias: {
|
||||
name: "aaa bbb",
|
||||
id: 3,
|
||||
email: "aaa@example.com",
|
||||
},
|
||||
}),
|
||||
])
|
||||
expect(data[0].products[0].product).toEqual({
|
||||
@@ -725,16 +698,10 @@ describe("RemoteJoiner", () => {
|
||||
|
||||
expect(data[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
product_user_alias: [
|
||||
{
|
||||
id: 2,
|
||||
name: "Jane Doe",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "aaa bbb",
|
||||
},
|
||||
],
|
||||
product_user_alias: {
|
||||
id: 3,
|
||||
name: "aaa bbb",
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
@@ -857,10 +824,13 @@ describe("RemoteJoiner", () => {
|
||||
id
|
||||
number
|
||||
products {
|
||||
product {
|
||||
handler
|
||||
user {
|
||||
name
|
||||
variant {
|
||||
name
|
||||
product {
|
||||
handler
|
||||
user {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -878,18 +848,22 @@ describe("RemoteJoiner", () => {
|
||||
products: [
|
||||
{
|
||||
product_id: 101,
|
||||
variant_id: 991,
|
||||
color: "red",
|
||||
product: {
|
||||
id: 101,
|
||||
product_extra_field: "extra 101 - red",
|
||||
variant: {
|
||||
id: 991,
|
||||
product_id: 101,
|
||||
variant_extra_field: "extra 101 - var 991 - red",
|
||||
},
|
||||
},
|
||||
{
|
||||
product_id: 101,
|
||||
variant_id: 992,
|
||||
color: "green",
|
||||
product: {
|
||||
id: 101,
|
||||
product_extra_field: "extra 101 - green",
|
||||
variant: {
|
||||
id: 992,
|
||||
product_id: 101,
|
||||
variant_extra_field: "extra 101 - var 992 - green",
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -897,23 +871,6 @@ describe("RemoteJoiner", () => {
|
||||
{
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -928,29 +885,41 @@ describe("RemoteJoiner", () => {
|
||||
products: [
|
||||
{
|
||||
product_id: 101,
|
||||
variant_id: 991,
|
||||
color: "red",
|
||||
product: {
|
||||
id: 101,
|
||||
product_extra_field: "extra 101 - red",
|
||||
handler: "product-1-handler",
|
||||
user_id: 2,
|
||||
user: {
|
||||
name: "Jane Doe",
|
||||
id: 2,
|
||||
variant: {
|
||||
id: 991,
|
||||
product_id: 101,
|
||||
variant_extra_field: "extra 101 - var 991 - red",
|
||||
name: "Product variant 1",
|
||||
product: {
|
||||
handler: "product-1-handler",
|
||||
id: 101,
|
||||
user_id: 2,
|
||||
user: {
|
||||
name: "Jane Doe",
|
||||
id: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
product_id: 101,
|
||||
variant_id: 992,
|
||||
color: "green",
|
||||
product: {
|
||||
id: 101,
|
||||
product_extra_field: "extra 101 - green",
|
||||
handler: "product-1-handler",
|
||||
user_id: 2,
|
||||
user: {
|
||||
name: "Jane Doe",
|
||||
id: 2,
|
||||
variant: {
|
||||
id: 992,
|
||||
product_id: 101,
|
||||
variant_extra_field: "extra 101 - var 992 - green",
|
||||
name: "Product variant 2",
|
||||
product: {
|
||||
handler: "product-1-handler",
|
||||
id: 101,
|
||||
user_id: 2,
|
||||
user: {
|
||||
name: "Jane Doe",
|
||||
id: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -965,31 +934,22 @@ describe("RemoteJoiner", () => {
|
||||
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",
|
||||
variant_id: 993,
|
||||
product_id: 103,
|
||||
variant: {
|
||||
name: "Product variant 33",
|
||||
id: 993,
|
||||
product_id: 103,
|
||||
product: {
|
||||
handler: "product-3-handler",
|
||||
id: 103,
|
||||
user_id: 3,
|
||||
user: {
|
||||
name: "aaa bbb",
|
||||
id: 3,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
extra_field: "extra",
|
||||
|
||||
@@ -44,6 +44,7 @@ type InternalParseExpandsParams = {
|
||||
expands: RemoteJoinerQuery["expands"]
|
||||
implodeMapping: InternalImplodeMapping[]
|
||||
options?: RemoteJoinerOptions
|
||||
initialData?: any[]
|
||||
}
|
||||
|
||||
export class RemoteJoiner {
|
||||
@@ -799,7 +800,9 @@ export class RemoteJoiner {
|
||||
})
|
||||
|
||||
for (const values of fieldsById.values()) {
|
||||
values.forEach((v) => idsToFetch.add(v))
|
||||
values.forEach((val) => {
|
||||
idsToFetch.add(val)
|
||||
})
|
||||
}
|
||||
|
||||
if (idsToFetch.size === 0) {
|
||||
@@ -878,10 +881,17 @@ export class RemoteJoiner {
|
||||
private parseExpands(
|
||||
params: InternalParseExpandsParams
|
||||
): Map<string, RemoteExpandProperty> {
|
||||
const { initialService, query, serviceConfig, expands, implodeMapping } =
|
||||
params
|
||||
const {
|
||||
initialService,
|
||||
query,
|
||||
serviceConfig,
|
||||
expands,
|
||||
implodeMapping,
|
||||
options,
|
||||
initialData,
|
||||
} = params
|
||||
|
||||
const parsedExpands = this.parseProperties({
|
||||
const { parsedExpands, aliasRealPathMap } = this.parseProperties({
|
||||
initialService,
|
||||
query,
|
||||
serviceConfig,
|
||||
@@ -889,6 +899,14 @@ export class RemoteJoiner {
|
||||
implodeMapping,
|
||||
})
|
||||
|
||||
if (initialData?.length) {
|
||||
this.createFilterFromInitialData({
|
||||
initialData: options?.initialData as any,
|
||||
parsedExpands,
|
||||
aliasRealPathMap,
|
||||
})
|
||||
}
|
||||
|
||||
const groupedExpands = this.groupExpands(parsedExpands)
|
||||
|
||||
return groupedExpands
|
||||
@@ -900,7 +918,10 @@ export class RemoteJoiner {
|
||||
serviceConfig: InternalJoinerServiceConfig
|
||||
expands: RemoteJoinerQuery["expands"]
|
||||
implodeMapping: InternalImplodeMapping[]
|
||||
}): Map<string, RemoteExpandProperty> {
|
||||
}): {
|
||||
parsedExpands: Map<string, RemoteExpandProperty>
|
||||
aliasRealPathMap: Map<string, string[]>
|
||||
} {
|
||||
const { initialService, query, serviceConfig, expands, implodeMapping } =
|
||||
params
|
||||
|
||||
@@ -1039,7 +1060,7 @@ export class RemoteJoiner {
|
||||
}
|
||||
}
|
||||
|
||||
return parsedExpands
|
||||
return { parsedExpands, aliasRealPathMap }
|
||||
}
|
||||
|
||||
private getEntity({ entity, prop }: { entity: string; prop: string }) {
|
||||
@@ -1216,6 +1237,206 @@ export class RemoteJoiner {
|
||||
return mergedExpands
|
||||
}
|
||||
|
||||
private createFilterFromInitialData({
|
||||
initialData,
|
||||
parsedExpands,
|
||||
aliasRealPathMap,
|
||||
}: {
|
||||
initialData: any[]
|
||||
parsedExpands: Map<string, RemoteExpandProperty>
|
||||
aliasRealPathMap: Map<string, string[]>
|
||||
}): void {
|
||||
if (!initialData.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const getPkValues = ({
|
||||
initialData,
|
||||
serviceConfig,
|
||||
relationship,
|
||||
}: {
|
||||
initialData: any[]
|
||||
serviceConfig: InternalJoinerServiceConfig
|
||||
relationship?: JoinerRelationship
|
||||
}): Record<string, any> => {
|
||||
if (!initialData.length || !relationship || !serviceConfig) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const primaryKeys = relationship.primaryKey
|
||||
? relationship.primaryKey.split(",")
|
||||
: serviceConfig.primaryKeys
|
||||
|
||||
const filter: Record<string, any> = {}
|
||||
|
||||
// Collect IDs for the current level, considering composed keys
|
||||
primaryKeys.forEach((key) => {
|
||||
filter[key] = Array.from(
|
||||
new Set(initialData.map((dt) => dt[key]).filter(isDefined))
|
||||
)
|
||||
})
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
const parsedSegment = new Map<string, any>()
|
||||
|
||||
const aliasReversePathMap = new Map<string, string>(
|
||||
Array.from(aliasRealPathMap).map(([path, realPath]) => [
|
||||
realPath.join("."),
|
||||
path,
|
||||
])
|
||||
)
|
||||
|
||||
for (let [path, expand] of parsedExpands.entries()) {
|
||||
const serviceConfig = expand.serviceConfig
|
||||
const relationship =
|
||||
this.getEntityRelationship({
|
||||
parentServiceConfig: expand.parentConfig!,
|
||||
property: expand.property,
|
||||
}) ?? serviceConfig.relationships?.get(serviceConfig.serviceName)
|
||||
|
||||
if (!serviceConfig || !relationship) {
|
||||
continue
|
||||
}
|
||||
|
||||
let aliasToPath: string | null = null
|
||||
if (aliasReversePathMap.has(path)) {
|
||||
aliasToPath = path
|
||||
path = aliasReversePathMap.get(path)!
|
||||
}
|
||||
|
||||
const pathSegments = path.split(".")
|
||||
let relevantInitialData = initialData
|
||||
let fullPath: string[] = []
|
||||
|
||||
for (const segment of pathSegments) {
|
||||
fullPath.push(segment)
|
||||
if (segment === BASE_PATH) {
|
||||
continue
|
||||
}
|
||||
|
||||
const pathStr = fullPath.join(".")
|
||||
if (parsedSegment.has(pathStr)) {
|
||||
relevantInitialData = parsedSegment.get(pathStr)
|
||||
continue
|
||||
}
|
||||
|
||||
relevantInitialData =
|
||||
RemoteJoiner.getNestedItems(relevantInitialData, segment) ?? []
|
||||
|
||||
parsedSegment.set(pathStr, relevantInitialData)
|
||||
|
||||
if (!relevantInitialData.length) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!relevantInitialData.length) {
|
||||
continue
|
||||
}
|
||||
|
||||
const queryPath = expand.parent === "" ? BASE_PATH : aliasToPath ?? path
|
||||
const filter = getPkValues({
|
||||
initialData: relevantInitialData,
|
||||
serviceConfig,
|
||||
relationship,
|
||||
})
|
||||
|
||||
if (!Object.keys(filter).length) {
|
||||
continue
|
||||
}
|
||||
|
||||
const parsed = parsedExpands.get(queryPath)!
|
||||
parsed.args ??= []
|
||||
parsed.args.push({
|
||||
name: "filters",
|
||||
value: filter,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private mergeInitialData({
|
||||
items,
|
||||
initialData,
|
||||
serviceConfig,
|
||||
path,
|
||||
expands,
|
||||
relationship,
|
||||
}: {
|
||||
items: any[]
|
||||
initialData: any[]
|
||||
serviceConfig: InternalJoinerServiceConfig
|
||||
path: string
|
||||
expands?: RemoteNestedExpands
|
||||
relationship?: JoinerRelationship
|
||||
}) {
|
||||
if (!initialData.length || !relationship) {
|
||||
return items
|
||||
}
|
||||
|
||||
const primaryKeys = relationship?.primaryKey.split(",") || [
|
||||
serviceConfig.primaryKeys[0],
|
||||
]
|
||||
const expandKeys = Object.keys(expands ?? {})
|
||||
|
||||
const initialDataIndexMap = new Map(
|
||||
initialData.map((dt, index) => [
|
||||
primaryKeys.map((key) => dt[key]).join(","),
|
||||
index,
|
||||
])
|
||||
)
|
||||
const itemMap = new Map(
|
||||
items.map((item) => [primaryKeys.map((key) => item[key]).join(","), item])
|
||||
)
|
||||
|
||||
const orderedMergedItems = new Array(initialData.length)
|
||||
for (const [key, index] of initialDataIndexMap.entries()) {
|
||||
const iniData = initialData[index]
|
||||
const item = itemMap.get(key)
|
||||
|
||||
if (!item) {
|
||||
orderedMergedItems[index] = iniData
|
||||
continue
|
||||
}
|
||||
|
||||
// Only merge properties that are not relations
|
||||
const shallowProperty = { ...iniData }
|
||||
for (const key of expandKeys) {
|
||||
const isRel = !!this.getEntityRelationship({
|
||||
parentServiceConfig: serviceConfig,
|
||||
property: key,
|
||||
})
|
||||
if (isRel) {
|
||||
delete shallowProperty[key]
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(item, shallowProperty)
|
||||
orderedMergedItems[index] = item
|
||||
}
|
||||
|
||||
if (expands) {
|
||||
for (const expand of expandKeys) {
|
||||
this.mergeInitialData({
|
||||
items: items.flatMap((dt) => dt[expand] ?? []),
|
||||
initialData: initialData
|
||||
.flatMap((dt) => dt[expand] ?? [])
|
||||
.filter(isDefined),
|
||||
serviceConfig,
|
||||
path: `${path}.${expand}`,
|
||||
expands: expands[expand]?.expands,
|
||||
relationship: this.getEntityRelationship({
|
||||
parentServiceConfig: serviceConfig,
|
||||
property: expand,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return orderedMergedItems
|
||||
}
|
||||
|
||||
async query(
|
||||
queryObj: RemoteJoinerQuery,
|
||||
options?: RemoteJoinerOptions
|
||||
@@ -1239,20 +1460,35 @@ export class RemoteJoiner {
|
||||
: [options.initialData]
|
||||
: []
|
||||
|
||||
if (options?.initialData) {
|
||||
let pkName = serviceConfig.primaryKeys[0]
|
||||
queryObj.args ??= []
|
||||
queryObj.args.push({
|
||||
name: pkName,
|
||||
value: iniDataArray.map((dt) => dt[pkName]),
|
||||
})
|
||||
const implodeMapping: InternalImplodeMapping[] = []
|
||||
const parseExpandsConfig: InternalParseExpandsParams = {
|
||||
initialService: {
|
||||
property: "",
|
||||
parent: "",
|
||||
serviceConfig,
|
||||
entity: serviceConfig.entity,
|
||||
fields: queryObj.fields,
|
||||
},
|
||||
query: queryObj,
|
||||
serviceConfig,
|
||||
expands: queryObj.expands!,
|
||||
implodeMapping,
|
||||
options,
|
||||
initialData: iniDataArray,
|
||||
}
|
||||
|
||||
const parsedExpands = this.parseExpands(parseExpandsConfig)
|
||||
const root = parsedExpands.get(BASE_PATH)!
|
||||
|
||||
const { primaryKeyArg, otherArgs, pkName } = gerPrimaryKeysAndOtherFilters({
|
||||
serviceConfig,
|
||||
queryObj,
|
||||
})
|
||||
|
||||
if (otherArgs) {
|
||||
parseExpandsConfig.initialService.args = otherArgs
|
||||
}
|
||||
|
||||
if (options?.throwIfKeyNotFound) {
|
||||
if (primaryKeyArg?.value == undefined) {
|
||||
if (!primaryKeyArg) {
|
||||
@@ -1275,30 +1511,6 @@ export class RemoteJoiner {
|
||||
}
|
||||
}
|
||||
|
||||
const implodeMapping: InternalImplodeMapping[] = []
|
||||
const parseExpandsConfig: InternalParseExpandsParams = {
|
||||
initialService: {
|
||||
property: "",
|
||||
parent: "",
|
||||
serviceConfig,
|
||||
entity: serviceConfig.entity,
|
||||
fields: queryObj.fields,
|
||||
},
|
||||
query: queryObj,
|
||||
serviceConfig,
|
||||
expands: queryObj.expands!,
|
||||
implodeMapping,
|
||||
options,
|
||||
}
|
||||
|
||||
if (otherArgs) {
|
||||
parseExpandsConfig.initialService.args = otherArgs
|
||||
}
|
||||
|
||||
const parsedExpands = this.parseExpands(parseExpandsConfig)
|
||||
|
||||
const root = parsedExpands.get(BASE_PATH)!
|
||||
|
||||
const response = await this.fetchData({
|
||||
expand: root,
|
||||
pkField: pkName,
|
||||
@@ -1306,28 +1518,40 @@ export class RemoteJoiner {
|
||||
options,
|
||||
})
|
||||
|
||||
const data = response.path ? response.data[response.path!] : response.data
|
||||
let data = response.path ? response.data[response.path!] : response.data
|
||||
const isDataArray = Array.isArray(data)
|
||||
|
||||
data = isDataArray ? data : [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])
|
||||
data = this.mergeInitialData({
|
||||
items: data,
|
||||
initialData: iniDataArray,
|
||||
serviceConfig,
|
||||
path: BASE_PATH,
|
||||
expands: parsedExpands.get(BASE_PATH)?.expands,
|
||||
relationship: serviceConfig.relationships?.get(
|
||||
serviceConfig.serviceName
|
||||
) as JoinerRelationship,
|
||||
})
|
||||
|
||||
if (iniData) {
|
||||
Object.assign(resData, iniData)
|
||||
}
|
||||
}
|
||||
delete options?.initialData
|
||||
}
|
||||
|
||||
await this.handleExpands({
|
||||
items: Array.isArray(data) ? data : [data],
|
||||
items: data,
|
||||
parsedExpands,
|
||||
implodeMapping,
|
||||
options,
|
||||
})
|
||||
|
||||
const retData = isDataArray ? data : data[0]
|
||||
if (response.path) {
|
||||
response.data[response.path] = retData
|
||||
} else {
|
||||
response.data = retData
|
||||
}
|
||||
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
@@ -1350,10 +1574,10 @@ function gerPrimaryKeysAndOtherFilters({ serviceConfig, queryObj }): {
|
||||
(arg) => !serviceConfig.primaryKeys.includes(arg.name)
|
||||
)
|
||||
|
||||
const filters =
|
||||
queryObj.args?.find((arg) => arg.name === "filters")?.value ?? {}
|
||||
|
||||
if (!primaryKeyArg) {
|
||||
const filters =
|
||||
queryObj.args?.find((arg) => arg.name === "filters")?.value ?? {}
|
||||
|
||||
const primaryKeyFilter = Object.keys(filters).find((key) => {
|
||||
return serviceConfig.primaryKeys.includes(key)
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ export type IndexQueryConfig<TEntry extends string> = {
|
||||
export type QueryFunctionReturnPagination = {
|
||||
skip?: number
|
||||
take?: number
|
||||
count: number
|
||||
count?: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
RemoteQueryObjectConfig,
|
||||
RemoteQueryObjectFromStringResult,
|
||||
} from "./remote-query-object-from-string"
|
||||
import { RemoteQueryFilters } from "./to-remote-query"
|
||||
|
||||
/*type ExcludedProps = "__typename"*/
|
||||
|
||||
@@ -38,6 +39,20 @@ export type QueryGraphFunction = {
|
||||
): Promise<Prettify<GraphResultSet<TEntry>>>
|
||||
}
|
||||
|
||||
/**
|
||||
* QueryIndexFunction is a wrapper on top of remoteQuery
|
||||
* that simplifies the input it accepts and returns
|
||||
* a normalized/consistent output.
|
||||
*/
|
||||
export type QueryIndexFunction = {
|
||||
<const TEntry extends string>(
|
||||
queryOptions: RemoteQueryInput<TEntry> & {
|
||||
joinFilters?: RemoteQueryFilters<TEntry>
|
||||
},
|
||||
options?: RemoteJoinerOptions
|
||||
): Promise<Prettify<GraphResultSet<TEntry>>>
|
||||
}
|
||||
|
||||
/*export type RemoteQueryReturnedData<TEntry extends string> =
|
||||
TEntry extends keyof RemoteQueryEntryPoints
|
||||
? Prettify<Omit<RemoteQueryEntryPoints[TEntry], ExcludedProps>>
|
||||
@@ -102,6 +117,12 @@ export type RemoteQueryFunction = {
|
||||
*/
|
||||
graph: QueryGraphFunction
|
||||
|
||||
/**
|
||||
* Index function uses the index module to query and remoteQuery to hydrate the data
|
||||
* returns a result set
|
||||
*/
|
||||
index: QueryIndexFunction
|
||||
|
||||
/**
|
||||
* Query wrapper to provide specific GraphQL like API around remoteQuery.query
|
||||
* @param query
|
||||
|
||||
Reference in new issue
Block a user