chore: query graph api (#9125)

CLOSES: FRMW-2704

**What**
Re-structure the Query graph API as well as introduce dynamic typing from schemas on the filters and better handling of relation treatment for fields/filters inference

Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
This commit is contained in:
Carlos R. L. Rodrigues
2024-09-16 08:32:44 -03:00
committed by GitHub
parent 3e97a64b21
commit 8829f89402
21 changed files with 2164 additions and 52 deletions

View File

@@ -48,7 +48,7 @@ declare module "@medusajs/types" {
[ContainerRegistrationKeys.CONFIG_MODULE]: ConfigModule
[ContainerRegistrationKeys.PG_CONNECTION]: Knex<any>
[ContainerRegistrationKeys.REMOTE_QUERY]: RemoteQueryFunction
[ContainerRegistrationKeys.QUERY]: RemoteQueryFunction
[ContainerRegistrationKeys.QUERY]: Omit<RemoteQueryFunction, symbol>
[ContainerRegistrationKeys.LOGGER]: Logger
}
}
@@ -520,7 +520,7 @@ async function MedusaApp_({
onApplicationStart,
modules: allModules,
link: remoteLink,
query: createQuery(remoteQuery),
query: createQuery(remoteQuery) as any, // TODO: rm any once we remove the old RemoteQueryFunction and rely on the Query object instead,
entitiesMap: schema.getTypeMap(),
gqlSchema: schema,
notFound,

View File

@@ -0,0 +1,454 @@
export type Maybe<T> = T | null
export type InputMaybe<T> = Maybe<T>
export type Exact<T extends { [key: string]: unknown }> = {
[K in keyof T]: T[K]
}
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & {
[SubKey in K]?: Maybe<T[SubKey]>
}
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & {
[SubKey in K]: Maybe<T[SubKey]>
}
export type MakeEmpty<
T extends { [key: string]: unknown },
K extends keyof T
> = { [_ in K]?: never }
export type Incremental<T> =
| T
| {
[P in keyof T]?: P extends " $fragmentName" | "__typename" ? T[P] : never
}
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: { input: string; output: string }
String: { input: string; output: string }
Boolean: { input: boolean; output: boolean }
Int: { input: number; output: number }
Float: { input: number; output: number }
DateTime: { input: Date | string; output: Date | string }
JSON: { input: Record<string, unknown>; output: Record<string, unknown> }
}
export type SimpleProduct = {
id: Scalars["ID"]["output"]
handle: string
title?: Scalars["String"]["output"]
variants?: Maybe<Array<Maybe<Pick<ProductVariant, "id">>>>
sales_channels_link?: Array<
Pick<LinkProductSalesChannel, "product_id" | "sales_channel_id">
>
sales_channels?: Array<Pick<SalesChannel, "id" | "name">>
}
export type Product = {
__typename?: "Product"
id: Scalars["ID"]["output"]
handle: Scalars["String"]["output"]
title: Scalars["String"]["output"]
description?: Scalars["String"]["output"]
variants?: Array<ProductVariant>
sales_channels_link?: Array<LinkProductSalesChannel>
sales_channels?: Array<SalesChannel>
metadata?: Maybe<Scalars["JSON"]["output"]>
}
export type ProductVariant = {
__typename?: "ProductVariant"
id: Scalars["ID"]["output"]
handle: Scalars["String"]["output"]
title: Scalars["String"]["output"]
sku: Scalars["String"]["output"]
product?: Maybe<Product>
}
export type ProductCategory = {
__typename?: "ProductCategory"
id: Scalars["ID"]["output"]
handle: Scalars["String"]["output"]
title?: Maybe<Scalars["String"]["output"]>
}
export type SalesChannel = {
__typename?: "SalesChannel"
id: Scalars["ID"]["output"]
name?: Maybe<Scalars["String"]["output"]>
description?: Maybe<Scalars["String"]["output"]>
created_at?: Maybe<Scalars["DateTime"]["output"]>
updated_at?: Maybe<Scalars["DateTime"]["output"]>
products_link?: Maybe<Array<Maybe<LinkProductSalesChannel>>>
api_keys_link?: Maybe<Array<Maybe<LinkPublishableApiKeySalesChannel>>>
locations_link?: Maybe<Array<Maybe<LinkSalesChannelStockLocation>>>
}
export type LinkCartPaymentCollection = {
__typename?: "LinkCartPaymentCollection"
cart_id: Scalars["String"]["output"]
payment_collection_id: Scalars["String"]["output"]
createdAt: Scalars["String"]["output"]
updatedAt: Scalars["String"]["output"]
deletedAt?: Maybe<Scalars["String"]["output"]>
}
export type LinkCartPromotion = {
__typename?: "LinkCartPromotion"
cart_id: Scalars["String"]["output"]
promotion_id: Scalars["String"]["output"]
createdAt: Scalars["String"]["output"]
updatedAt: Scalars["String"]["output"]
deletedAt?: Maybe<Scalars["String"]["output"]>
}
export type LinkLocationFulfillmentProvider = {
__typename?: "LinkLocationFulfillmentProvider"
stock_location_id: Scalars["String"]["output"]
fulfillment_provider_id: Scalars["String"]["output"]
createdAt: Scalars["String"]["output"]
updatedAt: Scalars["String"]["output"]
deletedAt?: Maybe<Scalars["String"]["output"]>
}
export type LinkLocationFulfillmentSet = {
__typename?: "LinkLocationFulfillmentSet"
stock_location_id: Scalars["String"]["output"]
fulfillment_set_id: Scalars["String"]["output"]
createdAt: Scalars["String"]["output"]
updatedAt: Scalars["String"]["output"]
deletedAt?: Maybe<Scalars["String"]["output"]>
}
export type LinkOrderCart = {
__typename?: "LinkOrderCart"
order_id: Scalars["String"]["output"]
cart_id: Scalars["String"]["output"]
createdAt: Scalars["String"]["output"]
updatedAt: Scalars["String"]["output"]
deletedAt?: Maybe<Scalars["String"]["output"]>
}
export type LinkOrderFulfillment = {
__typename?: "LinkOrderFulfillment"
order_id: Scalars["String"]["output"]
fulfillment_id: Scalars["String"]["output"]
createdAt: Scalars["String"]["output"]
updatedAt: Scalars["String"]["output"]
deletedAt?: Maybe<Scalars["String"]["output"]>
}
export type LinkOrderPaymentCollection = {
__typename?: "LinkOrderPaymentCollection"
order_id: Scalars["String"]["output"]
payment_collection_id: Scalars["String"]["output"]
createdAt: Scalars["String"]["output"]
updatedAt: Scalars["String"]["output"]
deletedAt?: Maybe<Scalars["String"]["output"]>
}
export type LinkOrderPromotion = {
__typename?: "LinkOrderPromotion"
order_id: Scalars["String"]["output"]
promotion_id: Scalars["String"]["output"]
createdAt: Scalars["String"]["output"]
updatedAt: Scalars["String"]["output"]
deletedAt?: Maybe<Scalars["String"]["output"]>
}
export type LinkReturnFulfillment = {
__typename?: "LinkReturnFulfillment"
return_id: Scalars["String"]["output"]
fulfillment_id: Scalars["String"]["output"]
createdAt: Scalars["String"]["output"]
updatedAt: Scalars["String"]["output"]
deletedAt?: Maybe<Scalars["String"]["output"]>
}
export type LinkProductSalesChannel = {
__typename?: "LinkProductSalesChannel"
product_id: Scalars["String"]["output"]
sales_channel_id: Scalars["String"]["output"]
product?: Maybe<Product>
sales_channel?: Maybe<SalesChannel>
createdAt: Scalars["String"]["output"]
updatedAt: Scalars["String"]["output"]
deletedAt?: Maybe<Scalars["String"]["output"]>
}
export type LinkProductVariantInventoryItem = {
__typename?: "LinkProductVariantInventoryItem"
variant_id: Scalars["String"]["output"]
inventory_item_id: Scalars["String"]["output"]
required_quantity: Scalars["Int"]["output"]
variant?: Maybe<Product>
createdAt: Scalars["String"]["output"]
updatedAt: Scalars["String"]["output"]
deletedAt?: Maybe<Scalars["String"]["output"]>
}
export type LinkProductVariantPriceSet = {
__typename?: "LinkProductVariantPriceSet"
variant_id: Scalars["String"]["output"]
price_set_id: Scalars["String"]["output"]
variant?: Maybe<Product>
createdAt: Scalars["String"]["output"]
updatedAt: Scalars["String"]["output"]
deletedAt?: Maybe<Scalars["String"]["output"]>
}
export type LinkPublishableApiKeySalesChannel = {
__typename?: "LinkPublishableApiKeySalesChannel"
publishable_key_id: Scalars["String"]["output"]
sales_channel_id: Scalars["String"]["output"]
sales_channel?: Maybe<SalesChannel>
createdAt: Scalars["String"]["output"]
updatedAt: Scalars["String"]["output"]
deletedAt?: Maybe<Scalars["String"]["output"]>
}
export type LinkRegionPaymentProvider = {
__typename?: "LinkRegionPaymentProvider"
region_id: Scalars["String"]["output"]
payment_provider_id: Scalars["String"]["output"]
createdAt: Scalars["String"]["output"]
updatedAt: Scalars["String"]["output"]
deletedAt?: Maybe<Scalars["String"]["output"]>
}
export type LinkSalesChannelStockLocation = {
__typename?: "LinkSalesChannelStockLocation"
sales_channel_id: Scalars["String"]["output"]
stock_location_id: Scalars["String"]["output"]
sales_channel?: Maybe<SalesChannel>
createdAt: Scalars["String"]["output"]
updatedAt: Scalars["String"]["output"]
deletedAt?: Maybe<Scalars["String"]["output"]>
}
export type LinkShippingOptionPriceSet = {
__typename?: "LinkShippingOptionPriceSet"
shipping_option_id: Scalars["String"]["output"]
price_set_id: Scalars["String"]["output"]
createdAt: Scalars["String"]["output"]
updatedAt: Scalars["String"]["output"]
deletedAt?: Maybe<Scalars["String"]["output"]>
}
export interface FixtureEntryPoints {
file: any
files: any
workflow_execution: any
workflow_executions: any
inventory_items: any
inventory_item: any
inventory: any
reservation: any
reservations: any
reservation_item: any
reservation_items: any
inventory_level: any
inventory_levels: any
stock_location_address: any
stock_location_addresses: any
stock_location: any
stock_locations: any
price_set: any
price_sets: any
price_list: any
price_lists: any
price: any
prices: any
price_preference: any
price_preferences: any
product_variant: ProductVariant
product_variants: ProductVariant
variant: ProductVariant
variants: ProductVariant
product: Product
products: Product
simple_product: SimpleProduct
product_option: any
product_options: any
product_type: any
product_types: any
product_image: any
product_images: any
product_tag: any
product_tags: any
product_collection: any
product_collections: any
product_category: ProductCategory
product_categories: ProductCategory
sales_channel: SalesChannel
sales_channels: SalesChannel
customer_address: any
customer_addresses: any
customer_group_customer: any
customer_group_customers: any
customer_group: any
customer_groups: any
customer: any
customers: any
cart: any
carts: any
address: any
addresses: any
line_item: any
line_items: any
line_item_adjustment: any
line_item_adjustments: any
line_item_tax_line: any
line_item_tax_lines: any
shipping_method: any
shipping_methods: any
shipping_method_adjustment: any
shipping_method_adjustments: any
shipping_method_tax_line: any
shipping_method_tax_lines: any
promotion: any
promotions: any
campaign: any
campaigns: any
promotion_rule: any
promotion_rules: any
api_key: any
api_keys: any
tax_rate: any
tax_rates: any
tax_region: any
tax_regions: any
tax_rate_rule: any
tax_rate_rules: any
tax_provider: any
tax_providers: any
store: any
stores: any
store_currency: any
store_currencies: any
user: any
users: any
invite: any
invites: any
auth_identity: any
auth_identities: any
order: any
orders: any
order_address: any
order_addresses: any
order_line_item: any
order_line_items: any
order_line_item_adjustment: any
order_line_item_adjustments: any
order_line_item_tax_line: any
order_line_item_tax_lines: any
order_shipping_method: any
order_shipping_methods: any
order_shipping_method_adjustment: any
order_shipping_method_adjustments: any
order_shipping_method_tax_line: any
order_shipping_method_tax_lines: any
order_transaction: any
order_transactions: any
order_change: any
order_changes: any
order_change_action: any
order_change_actions: any
order_item: any
order_items: any
order_summary: any
order_summaries: any
order_shipping: any
order_shippings: any
return_reason: any
return_reasons: any
return: any
returns: any
return_item: any
return_items: any
order_claim: any
order_claims: any
order_claim_item: any
order_claim_items: any
order_claim_item_image: any
order_claim_item_images: any
order_exchange: any
order_exchanges: any
order_exchange_item: any
order_exchange_items: any
payment: any
payments: any
payment_collection: any
payment_collections: any
payment_provider: any
payment_providers: any
payment_session: any
payment_sessions: any
refund_reason: any
refund_reasons: any
fulfillment_address: any
fulfillment_addresses: any
fulfillment_item: any
fulfillment_items: any
fulfillment_label: any
fulfillment_labels: any
fulfillment_provider: any
fulfillment_providers: any
fulfillment_set: any
fulfillment_sets: any
fulfillment: any
fulfillments: any
geo_zone: any
geo_zones: any
service_zone: any
service_zones: any
shipping_option_rule: any
shipping_option_rules: any
shipping_option_type: any
shipping_option_types: any
shipping_option: any
shipping_options: any
shipping_profile: any
shipping_profiles: any
notification: any
notifications: any
region: any
regions: any
country: any
countries: any
currency: any
currencies: any
cart_payment_collection: LinkCartPaymentCollection
cart_payment_collections: LinkCartPaymentCollection
cart_promotion: LinkCartPromotion
cart_promotions: LinkCartPromotion
location_fulfillment_provider: LinkLocationFulfillmentProvider
location_fulfillment_providers: LinkLocationFulfillmentProvider
location_fulfillment_set: LinkLocationFulfillmentSet
location_fulfillment_sets: LinkLocationFulfillmentSet
order_cart: LinkOrderCart
order_carts: LinkOrderCart
order_fulfillment: LinkOrderFulfillment
order_fulfillments: LinkOrderFulfillment
order_payment_collection: LinkOrderPaymentCollection
order_payment_collections: LinkOrderPaymentCollection
order_promotion: LinkOrderPromotion
order_promotions: LinkOrderPromotion
return_fulfillment: LinkReturnFulfillment
return_fulfillments: LinkReturnFulfillment
product_sales_channel: LinkProductSalesChannel
product_sales_channels: LinkProductSalesChannel
product_variant_inventory_item: LinkProductVariantInventoryItem
product_variant_inventory_items: LinkProductVariantInventoryItem
product_variant_price_set: LinkProductVariantPriceSet
product_variant_price_sets: LinkProductVariantPriceSet
publishable_api_key_sales_channel: LinkPublishableApiKeySalesChannel
publishable_api_key_sales_channels: LinkPublishableApiKeySalesChannel
region_payment_provider: LinkRegionPaymentProvider
region_payment_providers: LinkRegionPaymentProvider
sales_channel_location: LinkSalesChannelStockLocation
sales_channel_locations: LinkSalesChannelStockLocation
shipping_option_price_set: LinkShippingOptionPriceSet
shipping_option_price_sets: LinkShippingOptionPriceSet
}
declare module "@medusajs/types" {
export interface RemoteQueryEntryPoints extends FixtureEntryPoints {}
}

View File

@@ -0,0 +1,218 @@
import { QueryContext, QueryFilter } from "@medusajs/utils"
import "../__fixtures__/remote-query-type"
import { toRemoteQuery } from "../to-remote-query"
describe("toRemoteQuery", () => {
it("should transform a query with top level filtering", () => {
const format = toRemoteQuery({
entity: "product",
fields: ["id", "handle", "description"],
filters: QueryFilter<"product">({
handle: {
$ilike: "abc%",
},
}),
})
expect(format).toEqual({
product: {
__fields: ["id", "handle", "description"],
__args: {
filters: {
handle: {
$ilike: "abc%",
},
},
},
},
})
})
it("should transform a query with pagination", () => {
const format = toRemoteQuery({
entity: "product",
fields: ["id", "handle", "description"],
pagination: {
skip: 5,
take: 10,
},
})
expect(format).toEqual({
product: {
__fields: ["id", "handle", "description"],
__args: {
skip: 5,
take: 10,
},
},
})
})
it("should transform a query with top level filtering and pagination", () => {
const format = toRemoteQuery({
entity: "product",
fields: ["id", "handle", "description"],
pagination: {
skip: 5,
take: 10,
},
filters: QueryFilter<"product">({
handle: {
$ilike: "abc%",
},
}),
})
expect(format).toEqual({
product: {
__fields: ["id", "handle", "description"],
__args: {
skip: 5,
take: 10,
filters: {
handle: {
$ilike: "abc%",
},
},
},
},
})
})
it("should transform a query with filters and context into remote query input [1]", () => {
const format = toRemoteQuery({
entity: "product",
fields: [
"id",
"description",
"variants.title",
"variants.calculated_price",
"variants.options.*",
],
filters: {
variants: QueryFilter<"variants">({
sku: {
$ilike: "abc%",
},
}),
},
context: {
variants: {
calculated_price: QueryContext({
region_id: "reg_123",
currency_code: "usd",
}),
},
},
})
expect(format).toEqual({
product: {
__fields: ["id", "description"],
variants: {
__args: {
filters: {
sku: {
$ilike: "abc%",
},
},
},
calculated_price: {
__args: {
context: {
region_id: "reg_123",
currency_code: "usd",
},
},
},
__fields: ["title", "calculated_price"],
options: {
__fields: ["*"],
},
},
},
})
})
it("should transform a query with filters and context into remote query input [2]", () => {
const langContext = QueryContext({
context: {
lang: "pt-br",
},
})
const format = toRemoteQuery({
entity: "product",
fields: [
"id",
"title",
"description",
"product_translation.*",
"categories.*",
"categories.category_translation.*",
"variants.*",
"variants.variant_translation.*",
],
filters: QueryFilter<"product">({
id: "prod_01J742X0QPFW3R2ZFRTRC34FS8",
}),
context: {
product_translation: langContext,
categories: {
category_translation: langContext,
},
variants: {
variant_translation: langContext,
},
},
})
expect(format).toEqual({
product: {
__fields: ["id", "title", "description"],
__args: {
filters: {
id: "prod_01J742X0QPFW3R2ZFRTRC34FS8",
},
},
product_translation: {
__args: {
context: {
context: {
lang: "pt-br",
},
},
},
__fields: ["*"],
},
categories: {
category_translation: {
__args: {
context: {
context: {
lang: "pt-br",
},
},
},
__fields: ["*"],
},
__fields: ["*"],
},
variants: {
variant_translation: {
__args: {
context: {
context: {
lang: "pt-br",
},
},
},
__fields: ["*"],
},
__fields: ["*"],
},
},
})
})
})

View File

@@ -1,10 +1,10 @@
import { RemoteQuery } from "./remote-query"
import {
GraphResultSet,
RemoteJoinerOptions,
RemoteJoinerQuery,
RemoteQueryFunction,
RemoteQueryFunctionReturnPagination,
RemoteQueryInput,
RemoteQueryObjectConfig,
RemoteQueryObjectFromStringResult,
} from "@medusajs/types"
@@ -13,6 +13,8 @@ import {
MedusaError,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { RemoteQuery } from "./remote-query"
import { toRemoteQuery } from "./to-remote-query"
/**
* API wrapper around the remoteQuery
@@ -25,7 +27,7 @@ export class Query {
*/
static traceGraphQuery?: (
queryFn: () => Promise<any>,
queryOptions: RemoteQueryObjectConfig<any>
queryOptions: RemoteQueryInput<any>
) => Promise<any>
/**
@@ -58,14 +60,16 @@ export class Query {
#unwrapQueryConfig(
config:
| RemoteQueryObjectConfig<any>
| RemoteQueryObjectFromStringResult<any>
| RemoteQueryObjectConfig<any>
| RemoteJoinerQuery
): object {
let normalizedQuery: any = config
if ("__value" in config) {
normalizedQuery = config.__value
} else if ("entity" in normalizedQuery) {
normalizedQuery = toRemoteQuery(normalizedQuery)
} else if (
"entryPoint" in normalizedQuery ||
"service" in normalizedQuery
@@ -95,6 +99,7 @@ export class Query {
async query(
queryOptions:
| RemoteQueryInput<any>
| RemoteQueryObjectConfig<any>
| RemoteQueryObjectFromStringResult<any>
| RemoteJoinerQuery,
@@ -110,7 +115,7 @@ export class Query {
const config = this.#unwrapQueryConfig(queryOptions)
if (Query.traceRemoteQuery) {
return await Query.traceRemoteQuery(
async () => this.#remoteQuery.query(config, undefined, options),
async () => await this.#remoteQuery.query(config, undefined, options),
queryOptions
)
}
@@ -133,10 +138,10 @@ export class Query {
* returns a result set
*/
async graph<const TEntry extends string>(
queryOptions: RemoteQueryObjectConfig<TEntry>,
queryOptions: RemoteQueryInput<TEntry>,
options?: RemoteJoinerOptions
): Promise<GraphResultSet<TEntry>> {
const normalizedQuery = remoteQueryObjectFromString(queryOptions).__value
const normalizedQuery = toRemoteQuery<TEntry>(queryOptions)
let response:
| any[]
| { rows: any[]; metadata: RemoteQueryFunctionReturnPagination }
@@ -148,8 +153,8 @@ export class Query {
if (Query.traceGraphQuery) {
response = await Query.traceGraphQuery(
async () =>
this.#remoteQuery.query(normalizedQuery, undefined, options),
queryOptions
await this.#remoteQuery.query(normalizedQuery, undefined, options),
queryOptions as RemoteQueryInput<any>
)
} else {
response = await this.#remoteQuery.query(
@@ -167,7 +172,7 @@ export class Query {
* API wrapper around the remoteQuery with backward compatibility support
* @param remoteQuery
*/
export function createQuery(remoteQuery: RemoteQuery): RemoteQueryFunction {
export function createQuery(remoteQuery: RemoteQuery) {
const query = new Query(remoteQuery)
function backwardCompatibleQuery(...args: any[]) {
@@ -177,5 +182,5 @@ export function createQuery(remoteQuery: RemoteQuery): RemoteQueryFunction {
backwardCompatibleQuery.graph = query.graph.bind(query)
backwardCompatibleQuery.gql = query.gql.bind(query)
return backwardCompatibleQuery
return backwardCompatibleQuery as Omit<RemoteQueryFunction, symbol>
}

View File

@@ -0,0 +1,120 @@
import {
RemoteQueryEntryPoints,
RemoteQueryFilters,
RemoteQueryGraph,
RemoteQueryObjectConfig,
} from "@medusajs/types"
import { QueryContext, QueryFilter, isObject } from "@medusajs/utils"
const FIELDS = "__fields"
const ARGUMENTS = "__args"
/**
* convert a specific API configuration to a remote query object
*
* @param config
*
* @example
* const remoteQueryObject = toRemoteQuery({
* entity: "product",
* fields,
* filters: { variants: QueryFilter({ sku: "abc" }) },
* context: {
* variants: { calculated_price: QueryContext({ region_id: "reg_123" }) }
* }
* });
*
* console.log(remoteQueryObject);
*/
export function toRemoteQuery<const TEntity extends string>(config: {
entity: TEntity | keyof RemoteQueryEntryPoints
fields: RemoteQueryObjectConfig<TEntity>["fields"]
filters?: RemoteQueryFilters<TEntity>
pagination?: {
skip?: number
take?: number
}
context?: Record<string, any>
}): RemoteQueryGraph<TEntity> {
const { entity, fields = [], filters = {}, context = {} } = config
const joinerQuery: Record<string, any> = {
[entity]: {
__fields: [],
},
}
function processNestedObjects(
target: any,
source: Record<string, any>,
topLevel = true
) {
for (const key in source) {
const src = topLevel ? source : source[key]
if (!isObject(src)) {
target[key] = src
continue
}
if (QueryContext.isQueryContext(src) || QueryFilter.isQueryFilter(src)) {
const normalizedFilters = { ...src } as any
delete normalizedFilters.__type
const prop = QueryFilter.isQueryFilter(src) ? "filters" : "context"
if (topLevel) {
target[ARGUMENTS] ??= {}
target[ARGUMENTS][prop] = normalizedFilters
} else {
target[key] ??= {}
target[key][ARGUMENTS] ??= {}
target[key][ARGUMENTS][prop] = normalizedFilters
}
} else {
if (!topLevel) {
target[key] ??= {}
}
const nextTarget = topLevel ? target : target[key]
processNestedObjects(nextTarget, src, false)
}
}
}
// Process filters and context recursively
processNestedObjects(joinerQuery[entity], filters)
processNestedObjects(joinerQuery[entity], context)
for (const field of fields) {
const fieldAsString = field as string
if (!fieldAsString.includes(".")) {
joinerQuery[entity][FIELDS].push(field)
continue
}
const fieldSegments = fieldAsString.split(".")
const fieldProperty = fieldSegments.pop()
let combinedPath = ""
const deepConfigRef = fieldSegments.reduce((acc, curr) => {
combinedPath = combinedPath ? combinedPath + "." + curr : curr
acc[curr] ??= {}
return acc[curr]
}, joinerQuery[entity])
deepConfigRef[FIELDS] ??= []
deepConfigRef[FIELDS].push(fieldProperty)
}
if (config.pagination) {
joinerQuery[entity][ARGUMENTS] ??= {}
joinerQuery[entity][ARGUMENTS] = {
...joinerQuery[entity][ARGUMENTS],
...config.pagination,
}
}
return joinerQuery as RemoteQueryGraph<TEntity>
}

View File

@@ -78,8 +78,11 @@ export async function gqlSchemaToTypes({
documents: [],
config: {
scalars: {
DateTime: { output: "Date | string" },
JSON: { output: "Record<any, unknown>" },
DateTime: { input: "Date | string", output: "Date | string" },
JSON: {
input: "Record<string, unknown>",
output: "Record<string, unknown>",
},
},
},
filename: "",

View File

@@ -17,7 +17,7 @@ export function toRemoteJoinerQuery(
const canExpand =
typeof value === "object" &&
!["fields", "__args", "__directives"].includes(key)
!["fields", "__fields", "__args", "__directives"].includes(key) // TODO: deprecate "fields"
if (!canExpand) {
continue
@@ -65,8 +65,8 @@ export function toRemoteJoinerQuery(
)
}
if (value.fields) {
reference.fields = value.fields
if (value.__fields || value.fields) {
reference.fields = value.__fields ?? value.fields
}
if (isEntryPoint) {

View File

@@ -26,25 +26,23 @@ export type Scalars = {
Boolean: { input: boolean; output: boolean }
Int: { input: number; output: number }
Float: { input: number; output: number }
DateTime: {
input: { output: "Date | string" }
output: { output: "Date | string" }
}
JSON: {
input: { output: "Record<any, unknown>" }
output: { output: "Record<any, unknown>" }
}
DateTime: { input: Date | string; output: Date | string }
JSON: { input: Record<string, unknown>; output: Record<string, unknown> }
}
export type SimpleProduct = {
__typename?: "SimpleProduct"
id: Scalars["ID"]["output"]
handle: string
title?: Scalars["String"]["output"]
variants?: Maybe<Array<Maybe<Pick<ProductVariant, "id">>>>
variants?: Maybe<Array<Maybe<Pick<ProductVariant, "id" | "__typename">>>>
sales_channels_link?: Array<
Pick<LinkProductSalesChannel, "product_id" | "sales_channel_id">
Pick<
LinkProductSalesChannel,
"product_id" | "sales_channel_id" | "__typename"
>
>
sales_channels?: Array<Pick<SalesChannel, "id" | "name">>
sales_channels?: Array<Pick<SalesChannel, "id" | "name" | "__typename">>
}
export type Product = {

View File

@@ -9,11 +9,13 @@ describe("ObjectToRemoteQueryFields", () => {
description: string
date: Date
variants: {
__typename: string
id: string
sku: string
title: string
}[]
sales_channel: {
__typename: string
id: string
name: string
value: string
@@ -51,12 +53,14 @@ describe("ObjectToRemoteQueryFields", () => {
date: Date
variants: Maybe<
Maybe<{
__typename: string
id: string
sku: string
title: string
}>[]
>
sales_channel?: Maybe<{
__typename: string
id: string
name: string
value: string

View File

@@ -10,7 +10,7 @@ describe("Query", () => {
it("should infer return type of a known entry", async () => {
const graph = (() => {}) as unknown as QueryGraphFunction
const result = await graph({
entryPoint: "product",
entity: "product",
fields: ["handle", "id"],
})
@@ -23,7 +23,7 @@ describe("Query", () => {
it("should infer as any for an known entry", async () => {
const graph = (() => {}) as unknown as QueryGraphFunction
const result = await graph({
entryPoint: "foo",
entity: "foo",
fields: ["handle", "id"],
})

View File

@@ -4,11 +4,18 @@ import { MedusaContainer } from "../common"
import { RepositoryService } from "../dal"
import { Logger } from "../logger"
import {
RemoteQueryGraph,
RemoteQueryInput,
RemoteQueryObjectConfig,
RemoteQueryObjectFromStringResult,
} from "./remote-query-object-from-string"
export { RemoteQueryObjectConfig, RemoteQueryObjectFromStringResult }
export {
RemoteQueryGraph,
RemoteQueryInput,
RemoteQueryObjectConfig,
RemoteQueryObjectFromStringResult,
}
export type Constructor<T> = new (...args: any[]) => T | (new () => T)
@@ -17,6 +24,8 @@ export * from "./medusa-internal-service"
export * from "./module-provider"
export * from "./remote-query"
export * from "./remote-query-entry-points"
export * from "./to-remote-query"
export * from "./query-filter"
export type LogLevel =
| "query"

View File

@@ -1,7 +1,6 @@
type Marker = [never, 0, 1, 2, 3, 4]
type ExcludedProps = ["__typename"]
type SpecialNonRelationProps = ["metadata"]
type ExcludedProps = "__typename"
type RawBigNumberPrefix = "raw_"
type ExpandStarSelector<
@@ -26,12 +25,9 @@ export type ObjectToRemoteQueryFields<
? {
[K in keyof T]: K extends // handle big number
`${RawBigNumberPrefix}${string}`
? Exclude<K, symbol>
: // handle metadata
K extends SpecialNonRelationProps[number]
? Exclude<K, symbol>
: // Special props that should be excluded
K extends ExcludedProps[number]
K extends ExcludedProps
? never
: // Prevent recursive reference to itself
K extends Exclusion[number]
@@ -39,21 +35,25 @@ export type ObjectToRemoteQueryFields<
: TypeOnly<T[K]> extends Array<infer R>
? TypeOnly<R> extends Date
? Exclude<K, symbol>
: TypeOnly<R> extends object
: TypeOnly<R> extends { __typename: any }
? `${Exclude<K, symbol>}.${ExpandStarSelector<
TypeOnly<R>,
Marker[Depth],
[K & string, ...Exclusion]
>}`
: TypeOnly<R> extends object
? Exclude<K, symbol>
: never
: TypeOnly<T[K]> extends Date
? Exclude<K, symbol>
: TypeOnly<T[K]> extends object
: TypeOnly<T[K]> extends { __typename: any }
? `${Exclude<K, symbol>}.${ExpandStarSelector<
TypeOnly<T[K]>,
Marker[Depth],
[K & string, ...Exclusion]
>}`
: T[K] extends object
? Exclude<K, symbol>
: Exclude<K, symbol>
}[keyof T]
: never

View File

@@ -0,0 +1,9 @@
import { RemoteQueryFilters } from "./to-remote-query"
export type QueryFilterType = {
<TEntry extends string>(
query: RemoteQueryFilters<TEntry>
): RemoteQueryFilters<TEntry> & { __type: "QueryFilter" }
isQueryFilter: (obj: any) => boolean
}

View File

@@ -1,5 +1,6 @@
import { RemoteQueryEntryPoints } from "./remote-query-entry-points"
import { ObjectToRemoteQueryFields } from "./object-to-remote-query-fields"
import { RemoteQueryEntryPoints } from "./remote-query-entry-points"
import { RemoteQueryFilters } from "./to-remote-query"
export type RemoteQueryObjectConfig<TEntry extends string> = {
// service: string This property is still supported under the hood but part of the type due to types missmatch towards fields
@@ -20,3 +21,25 @@ export type RemoteQueryObjectFromStringResult<
__TConfig: TConfig
__value: object
}
export type RemoteQueryInput<TEntry extends string> = {
// service: string This property is still supported under the hood but part of the type due to types missmatch towards fields
entity: TEntry | keyof RemoteQueryEntryPoints
fields: ObjectToRemoteQueryFields<
RemoteQueryEntryPoints[TEntry & keyof RemoteQueryEntryPoints]
> extends never
? string[]
: ObjectToRemoteQueryFields<
RemoteQueryEntryPoints[TEntry & keyof RemoteQueryEntryPoints]
>[]
pagination?: {
skip: number
take?: number
}
filters?: RemoteQueryFilters<TEntry>
context?: any
}
export type RemoteQueryGraph<TEntry extends string> = {
__TConfig: RemoteQueryObjectConfig<TEntry>
}

View File

@@ -2,6 +2,7 @@ import { Prettify } from "../common"
import { RemoteJoinerOptions, RemoteJoinerQuery } from "../joiner"
import { RemoteQueryEntryPoints } from "./remote-query-entry-points"
import {
RemoteQueryInput,
RemoteQueryObjectConfig,
RemoteQueryObjectFromStringResult,
} from "./remote-query-object-from-string"
@@ -32,7 +33,7 @@ export type GraphResultSet<TEntry extends string> = {
*/
export type QueryGraphFunction = {
<const TEntry extends string>(
queryConfig: RemoteQueryObjectConfig<TEntry>,
queryConfig: RemoteQueryInput<TEntry>,
options?: RemoteJoinerOptions
): Promise<Prettify<GraphResultSet<TEntry>>>
}

View File

@@ -0,0 +1,72 @@
import { Prettify } from "../common"
import { OperatorMap } from "../dal"
import { RemoteQueryEntryPoints } from "./remote-query-entry-points"
type ExcludedProps = "__typename"
type Depth = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
type CleanupObject<T> = Prettify<Omit<Exclude<T, symbol>, ExcludedProps>>
type OmitNever<T extends object> = {
[K in keyof T as T[K] extends never ? never : K]: T[K]
}
type TypeOnly<T> = Required<Exclude<T, null | undefined>>
type ExtractFiltersOperators<
MaybeT,
Lim extends number = Depth[2],
Exclusion extends string[] = [],
T = TypeOnly<MaybeT>
> = {
[Key in keyof T]?: Key extends Exclusion[number]
? never
: Key extends ExcludedProps
? never
: T[Key] extends string | number | boolean | Date
? T[Key] | OperatorMap<T[Key] | T[Key][]>
: T[Key] extends Array<infer R>
? TypeOnly<R> extends { __typename: any }
? RemoteQueryFilters<
Key & string,
T,
[Key & string, ...Exclusion],
Depth[Lim]
>
: R extends object
? CleanupObject<R>
: never
: T[Key] extends { __typename: any }
? RemoteQueryFilters<
Key & string,
T[Key],
[Key & string, ...Exclusion],
Depth[Lim]
>
: T[Key] extends object
? CleanupObject<T[Key]>
: never
}
/**
* Extract all available filters from a remote entry point deeply
*/
export type RemoteQueryFilters<
TEntry extends string,
RemoteQueryEntryPointsLevel = RemoteQueryEntryPoints,
Exclusion extends string[] = [],
Lim extends number = Depth[3]
> = Lim extends number
? TEntry extends keyof RemoteQueryEntryPointsLevel
? TypeOnly<RemoteQueryEntryPointsLevel[TEntry]> extends Array<infer V>
? Prettify<
OmitNever<ExtractFiltersOperators<V, Lim, [TEntry, ...Exclusion]>>
>
: Prettify<
OmitNever<
ExtractFiltersOperators<
RemoteQueryEntryPointsLevel[TEntry],
Lim,
[TEntry, ...Exclusion]
>
>
>
: Record<string, any>
: never

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
export * from "./build-query"
export * from "./create-pg-connection"
export * from "./decorators"
export * from "./define-link"
export * from "./definition"
export * from "./event-builder-factory"
export * from "./joiner-config-builder"
@@ -14,4 +15,5 @@ export * from "./medusa-service"
export * from "./migration-scripts"
export * from "./mikro-orm-cli-config-builder"
export * from "./module"
export * from "./define-link"
export * from "./query-context"
export * from "./query-filter"

View File

@@ -0,0 +1,19 @@
type QueryContextType = {
(query: Record<string, unknown>): Record<string, unknown>
isQueryContext: (obj: any) => boolean
}
const __type = "QueryContext"
function QueryContextFn(query: Record<string, unknown>) {
return {
...query,
__type,
}
}
QueryContextFn.isQueryContext = (obj: any) => {
return obj.__type === __type
}
export const QueryContext: QueryContextType = QueryContextFn

View File

@@ -0,0 +1,18 @@
import { RemoteQueryFilters } from "@medusajs/types"
const __type = "QueryFilter"
export function QueryFilterFn<TEntry extends string>(
query: RemoteQueryFilters<TEntry>
): RemoteQueryFilters<TEntry> & { __type: "QueryFilter" } {
return {
...query,
__type,
}
}
QueryFilterFn.isQueryFilter = (obj: any) => {
return obj.__type === __type
}
export const QueryFilter = QueryFilterFn

View File

@@ -1,14 +1,14 @@
import { snakeCase } from "lodash"
import { NodeSDK } from "@opentelemetry/sdk-node"
import { Resource } from "@opentelemetry/resources"
import { Query, RoutesLoader, Tracer } from "@medusajs/framework"
import { SpanStatusCode } from "@opentelemetry/api"
import { RoutesLoader, Tracer, Query } from "@medusajs/framework"
import {
type SpanExporter,
SimpleSpanProcessor,
} from "@opentelemetry/sdk-trace-node"
import { PgInstrumentation } from "@opentelemetry/instrumentation-pg"
import type { Instrumentation } from "@opentelemetry/instrumentation"
import { PgInstrumentation } from "@opentelemetry/instrumentation-pg"
import { Resource } from "@opentelemetry/resources"
import { NodeSDK } from "@opentelemetry/sdk-node"
import {
SimpleSpanProcessor,
type SpanExporter,
} from "@opentelemetry/sdk-trace-node"
import { snakeCase } from "lodash"
import start from "../commands/start"
@@ -132,7 +132,7 @@ export function instrumentRemoteQuery() {
Query.instrument.graphQuery(async function (queryFn, queryOptions) {
return await QueryTracer.trace(
`query.graph: ${queryOptions.entryPoint}`,
`query.graph: ${queryOptions.entity}`,
async (span) => {
span.setAttributes({
"query.fields": queryOptions.fields,