feat(modules-sdk): remote query retrieve (#6849)
What: Remote Joiner options to check if keys exist on entry points or relations
This commit is contained in:
committed by
GitHub
parent
cbb5e6bd99
commit
1c6ba4468e
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@medusajs/modules-sdk": patch
|
||||
"@medusajs/orchestration": patch
|
||||
"@medusajs/types": patch
|
||||
---
|
||||
|
||||
Remote Joiner options to check if keys exist on entrypoints or relations
|
||||
@@ -0,0 +1,205 @@
|
||||
import { ModuleRegistrationName, Modules } from "@medusajs/modules-sdk"
|
||||
import { IPaymentModuleService, IRegionModuleService } from "@medusajs/types"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import { createAdminUser } from "../../..//helpers/create-admin-user"
|
||||
import { adminHeaders } from "../../../helpers/create-admin-user"
|
||||
|
||||
jest.setTimeout(50000)
|
||||
|
||||
const env = { MEDUSA_FF_MEDUSA_V2: true }
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
env,
|
||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||
describe("Remote Query", () => {
|
||||
let appContainer
|
||||
let regionModule: IRegionModuleService
|
||||
let paymentModule: IPaymentModuleService
|
||||
let remoteQuery
|
||||
let remoteLink
|
||||
|
||||
beforeAll(async () => {
|
||||
appContainer = getContainer()
|
||||
regionModule = appContainer.resolve(ModuleRegistrationName.REGION)
|
||||
paymentModule = appContainer.resolve(ModuleRegistrationName.PAYMENT)
|
||||
remoteQuery = appContainer.resolve(
|
||||
ContainerRegistrationKeys.REMOTE_QUERY
|
||||
)
|
||||
remoteLink = appContainer.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||
})
|
||||
|
||||
it("should fail to retrieve a single non-existing id", async () => {
|
||||
const region = await regionModule.create({
|
||||
name: "Test Region",
|
||||
currency_code: "usd",
|
||||
countries: ["us"],
|
||||
})
|
||||
|
||||
const getRegion = await remoteQuery({
|
||||
region: {
|
||||
fields: ["id", "currency_code"],
|
||||
__args: {
|
||||
id: region.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(getRegion).toEqual([
|
||||
{
|
||||
id: region.id,
|
||||
currency_code: "usd",
|
||||
},
|
||||
])
|
||||
|
||||
const getNonExistingRegion = remoteQuery(
|
||||
{
|
||||
region: {
|
||||
fields: ["id", "currency_code"],
|
||||
__args: {
|
||||
id: "region_123",
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{ throwIfKeyNotFound: true }
|
||||
)
|
||||
|
||||
expect(getNonExistingRegion).rejects.toThrow(
|
||||
"region id not found: region_123"
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail if a expected relation is not found", async () => {
|
||||
const region = await regionModule.create({
|
||||
name: "Test Region",
|
||||
currency_code: "usd",
|
||||
countries: ["us"],
|
||||
})
|
||||
|
||||
const regionWithPayment = await regionModule.create({
|
||||
name: "Test W/ Payment",
|
||||
currency_code: "brl",
|
||||
countries: ["br"],
|
||||
})
|
||||
|
||||
const regionNoLink = await regionModule.create({
|
||||
name: "No link",
|
||||
currency_code: "eur",
|
||||
countries: ["dk"],
|
||||
})
|
||||
|
||||
await remoteLink.create([
|
||||
{
|
||||
[Modules.REGION]: {
|
||||
region_id: region.id,
|
||||
},
|
||||
[Modules.PAYMENT]: {
|
||||
payment_provider_id: "pp_system_default_non_existent",
|
||||
},
|
||||
},
|
||||
{
|
||||
[Modules.REGION]: {
|
||||
region_id: regionWithPayment.id,
|
||||
},
|
||||
[Modules.PAYMENT]: {
|
||||
payment_provider_id: "pp_system_default", // default payment provider auto created
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
// Validate all relations, including the link
|
||||
expect(
|
||||
remoteQuery(
|
||||
{
|
||||
region: {
|
||||
fields: ["id"],
|
||||
__args: {
|
||||
id: regionNoLink.id,
|
||||
},
|
||||
payment_providers: {
|
||||
fields: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
throwIfRelationNotFound: true,
|
||||
}
|
||||
)
|
||||
).rejects.toThrow(
|
||||
`regionRegionPaymentPaymentProviderLink region_id not found: ${regionNoLink.id}`
|
||||
)
|
||||
|
||||
// Only validate the relations with Payment. It doesn't fail because the link didn't return any data
|
||||
expect(
|
||||
remoteQuery(
|
||||
{
|
||||
region: {
|
||||
fields: ["id"],
|
||||
__args: {
|
||||
id: regionNoLink.id,
|
||||
},
|
||||
payment_providers: {
|
||||
fields: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
throwIfRelationNotFound: [Modules.PAYMENT],
|
||||
}
|
||||
)
|
||||
).resolves.toHaveLength(1)
|
||||
|
||||
// The link exists, but the payment doesn't
|
||||
expect(
|
||||
remoteQuery(
|
||||
{
|
||||
region: {
|
||||
fields: ["id"],
|
||||
__args: {
|
||||
id: region.id,
|
||||
},
|
||||
payment_providers: {
|
||||
fields: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
throwIfRelationNotFound: [Modules.PAYMENT],
|
||||
}
|
||||
)
|
||||
).rejects.toThrow(
|
||||
"payment id not found: pp_system_default_non_existent"
|
||||
)
|
||||
|
||||
// everything is fine
|
||||
expect(
|
||||
remoteQuery(
|
||||
{
|
||||
region: {
|
||||
fields: ["id"],
|
||||
__args: {
|
||||
id: regionWithPayment.id,
|
||||
},
|
||||
payment_providers: {
|
||||
fields: ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
throwIfRelationNotFound: [Modules.PAYMENT],
|
||||
}
|
||||
)
|
||||
).resolves.toHaveLength(1)
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ModuleExports,
|
||||
ModuleJoinerConfig,
|
||||
ModuleServiceInitializeOptions,
|
||||
RemoteJoinerOptions,
|
||||
RemoteJoinerQuery,
|
||||
RemoteQueryFunction,
|
||||
} from "@medusajs/types"
|
||||
@@ -241,7 +242,8 @@ async function MedusaApp_({
|
||||
link: RemoteLink | undefined
|
||||
query: (
|
||||
query: string | RemoteJoinerQuery | object,
|
||||
variables?: Record<string, unknown>
|
||||
variables?: Record<string, unknown>,
|
||||
options?: RemoteJoinerOptions
|
||||
) => Promise<any>
|
||||
entitiesMap?: Record<string, any>
|
||||
notFound?: Record<string, Record<string, string>>
|
||||
@@ -345,9 +347,10 @@ async function MedusaApp_({
|
||||
|
||||
const query = async (
|
||||
query: string | RemoteJoinerQuery | object,
|
||||
variables?: Record<string, unknown>
|
||||
variables?: Record<string, unknown>,
|
||||
options?: RemoteJoinerOptions
|
||||
) => {
|
||||
return await remoteQuery.query(query, variables)
|
||||
return await remoteQuery.query(query, variables, options)
|
||||
}
|
||||
|
||||
const runMigrations: RunMigrationFn = async (
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "@medusajs/types"
|
||||
import { isString, toPascalCase } from "@medusajs/utils"
|
||||
|
||||
import { RemoteJoinerOptions } from "@medusajs/types"
|
||||
import { MedusaModule } from "./medusa-module"
|
||||
|
||||
export class RemoteQuery {
|
||||
@@ -230,7 +231,8 @@ export class RemoteQuery {
|
||||
|
||||
public async query(
|
||||
query: string | RemoteJoinerQuery | object,
|
||||
variables?: Record<string, unknown>
|
||||
variables?: Record<string, unknown>,
|
||||
options?: RemoteJoinerOptions
|
||||
): Promise<any> {
|
||||
let finalQuery: RemoteJoinerQuery = query as RemoteJoinerQuery
|
||||
|
||||
@@ -240,6 +242,6 @@ export class RemoteQuery {
|
||||
finalQuery = toRemoteJoinerQuery(query, variables)
|
||||
}
|
||||
|
||||
return await this.remoteJoiner.query(finalQuery)
|
||||
return await this.remoteJoiner.query(finalQuery, options)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,11 @@ export const mockServiceList = (serviceName) => {
|
||||
})
|
||||
}
|
||||
|
||||
// mock filtering on service order
|
||||
if (serviceName === "orderService" && data.options?.id) {
|
||||
resultset = resultset.filter((item) => data.options.id.includes(item.id))
|
||||
}
|
||||
|
||||
return {
|
||||
data: resultset,
|
||||
path: serviceName === "productService" ? "rows" : undefined,
|
||||
|
||||
@@ -241,12 +241,7 @@ describe("RemoteJoiner", () => {
|
||||
fields: ["name"],
|
||||
},
|
||||
],
|
||||
args: [
|
||||
{
|
||||
name: "id",
|
||||
value: "3",
|
||||
},
|
||||
],
|
||||
args: [],
|
||||
}
|
||||
|
||||
const data = await joiner.query(query)
|
||||
@@ -802,4 +797,34 @@ describe("RemoteJoiner", () => {
|
||||
`Service with alias "user" was not found.`
|
||||
)
|
||||
})
|
||||
|
||||
it("Should throw when any key of the entrypoint isn't found", async () => {
|
||||
const query = RemoteJoiner.parseQuery(`
|
||||
query {
|
||||
order (id: 201) {
|
||||
id
|
||||
number
|
||||
}
|
||||
}
|
||||
`)
|
||||
const data = await joiner.query(query, {
|
||||
throwIfKeyNotFound: true,
|
||||
})
|
||||
|
||||
expect(data.length).toEqual(1)
|
||||
|
||||
const queryNotFound = RemoteJoiner.parseQuery(`
|
||||
query {
|
||||
order (id: "ord_1234556") {
|
||||
id
|
||||
number
|
||||
}
|
||||
}
|
||||
`)
|
||||
const dataNotFound = joiner.query(queryNotFound, {
|
||||
throwIfKeyNotFound: true,
|
||||
})
|
||||
|
||||
expect(dataNotFound).rejects.toThrowError("order id not found: ord_1234556")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -569,19 +569,13 @@ describe("RemoteJoiner", () => {
|
||||
fields: ["name"],
|
||||
},
|
||||
],
|
||||
args: [
|
||||
{
|
||||
name: "id",
|
||||
value: "3",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
await joiner.query(query)
|
||||
|
||||
expect(serviceMock.orderService).toHaveBeenCalledTimes(1)
|
||||
expect(serviceMock.orderService).toHaveBeenCalledWith({
|
||||
args: [],
|
||||
args: undefined,
|
||||
fields: ["number", "date", "products", "user_id"],
|
||||
expands: {
|
||||
products: {
|
||||
@@ -589,7 +583,7 @@ describe("RemoteJoiner", () => {
|
||||
fields: ["product_id"],
|
||||
},
|
||||
},
|
||||
options: { id: ["3"] },
|
||||
options: { id: undefined },
|
||||
})
|
||||
|
||||
expect(serviceMock.userService).toHaveBeenCalledTimes(1)
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
RemoteNestedExpands,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { deduplicate, isDefined, isString } from "@medusajs/utils"
|
||||
import { RemoteJoinerOptions } from "@medusajs/types"
|
||||
import { MedusaError, deduplicate, isDefined, isString } from "@medusajs/utils"
|
||||
import GraphQLParser from "./graphql-ast"
|
||||
|
||||
const BASE_PATH = "_root"
|
||||
@@ -331,7 +332,8 @@ export class RemoteJoiner {
|
||||
expand: RemoteExpandProperty,
|
||||
pkField: string,
|
||||
ids?: (unknown | unknown[])[],
|
||||
relationship?: any
|
||||
relationship?: any,
|
||||
options?: RemoteJoinerOptions
|
||||
): Promise<{
|
||||
data: unknown[] | { [path: string]: unknown }
|
||||
path?: string
|
||||
@@ -372,6 +374,15 @@ export class RemoteJoiner {
|
||||
|
||||
resData = Array.isArray(resData) ? resData : [resData]
|
||||
|
||||
this.checkIfKeysExist(
|
||||
uniqueIds,
|
||||
resData,
|
||||
expand,
|
||||
pkField,
|
||||
relationship,
|
||||
options
|
||||
)
|
||||
|
||||
const filteredDataArray = resData.map((data: any) =>
|
||||
RemoteJoiner.filterFields(data, expand.fields, expand.expands)
|
||||
)
|
||||
@@ -385,6 +396,47 @@ export class RemoteJoiner {
|
||||
return response
|
||||
}
|
||||
|
||||
private checkIfKeysExist(
|
||||
uniqueIds: unknown[] | undefined,
|
||||
resData: any[],
|
||||
expand: RemoteExpandProperty,
|
||||
pkField: string,
|
||||
relationship?: any,
|
||||
options?: RemoteJoinerOptions
|
||||
) {
|
||||
if (
|
||||
!(
|
||||
isDefined(uniqueIds) &&
|
||||
((options?.throwIfKeyNotFound && !isDefined(relationship)) ||
|
||||
(options?.throwIfRelationNotFound && isDefined(relationship)))
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isDefined(relationship)) {
|
||||
if (
|
||||
Array.isArray(options?.throwIfRelationNotFound) &&
|
||||
!options?.throwIfRelationNotFound.includes(relationship.serviceName)
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const notFound = new Set(uniqueIds)
|
||||
resData.forEach((data) => {
|
||||
notFound.delete(data[pkField])
|
||||
})
|
||||
|
||||
if (notFound.size > 0) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`${expand.serviceConfig.serviceName} ${pkField} not found: ` +
|
||||
Array.from(notFound).join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private handleFieldAliases(
|
||||
items: any[],
|
||||
parsedExpands: Map<string, RemoteExpandProperty>,
|
||||
@@ -466,7 +518,8 @@ export class RemoteJoiner {
|
||||
private async handleExpands(
|
||||
items: any[],
|
||||
parsedExpands: Map<string, RemoteExpandProperty>,
|
||||
implodeMapping: InternalImplodeMapping[] = []
|
||||
implodeMapping: InternalImplodeMapping[] = [],
|
||||
options?: RemoteJoinerOptions
|
||||
): Promise<void> {
|
||||
if (!parsedExpands) {
|
||||
return
|
||||
@@ -488,7 +541,12 @@ export class RemoteJoiner {
|
||||
}
|
||||
|
||||
if (nestedItems.length > 0) {
|
||||
await this.expandProperty(nestedItems, expand.parentConfig!, expand)
|
||||
await this.expandProperty(
|
||||
nestedItems,
|
||||
expand.parentConfig!,
|
||||
expand,
|
||||
options
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,7 +556,8 @@ export class RemoteJoiner {
|
||||
private async expandProperty(
|
||||
items: any[],
|
||||
parentServiceConfig: JoinerServiceConfig,
|
||||
expand?: RemoteExpandProperty
|
||||
expand?: RemoteExpandProperty,
|
||||
options?: RemoteJoinerOptions
|
||||
): Promise<void> {
|
||||
if (!expand) {
|
||||
return
|
||||
@@ -509,14 +568,20 @@ export class RemoteJoiner {
|
||||
)
|
||||
|
||||
if (relationship) {
|
||||
await this.expandRelationshipProperty(items, expand, relationship)
|
||||
await this.expandRelationshipProperty(
|
||||
items,
|
||||
expand,
|
||||
relationship,
|
||||
options
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private async expandRelationshipProperty(
|
||||
items: any[],
|
||||
expand: RemoteExpandProperty,
|
||||
relationship: JoinerRelationship
|
||||
relationship: JoinerRelationship,
|
||||
options?: RemoteJoinerOptions
|
||||
): Promise<void> {
|
||||
const field = relationship.inverse
|
||||
? relationship.primaryKey
|
||||
@@ -552,7 +617,8 @@ export class RemoteJoiner {
|
||||
expand,
|
||||
field,
|
||||
idsToFetch,
|
||||
relationship
|
||||
relationship,
|
||||
options
|
||||
)
|
||||
|
||||
const joinFields = relationship.inverse
|
||||
@@ -602,14 +668,16 @@ export class RemoteJoiner {
|
||||
query: RemoteJoinerQuery,
|
||||
serviceConfig: JoinerServiceConfig,
|
||||
expands: RemoteJoinerQuery["expands"],
|
||||
implodeMapping: InternalImplodeMapping[]
|
||||
implodeMapping: InternalImplodeMapping[],
|
||||
options?: RemoteJoinerOptions
|
||||
): Map<string, RemoteExpandProperty> {
|
||||
const parsedExpands = this.parseProperties(
|
||||
initialService,
|
||||
query,
|
||||
serviceConfig,
|
||||
expands,
|
||||
implodeMapping
|
||||
implodeMapping,
|
||||
options
|
||||
)
|
||||
|
||||
const groupedExpands = this.groupExpands(parsedExpands)
|
||||
@@ -622,7 +690,8 @@ export class RemoteJoiner {
|
||||
query: RemoteJoinerQuery,
|
||||
serviceConfig: JoinerServiceConfig,
|
||||
expands: RemoteJoinerQuery["expands"],
|
||||
implodeMapping: InternalImplodeMapping[]
|
||||
implodeMapping: InternalImplodeMapping[],
|
||||
options?: RemoteJoinerOptions
|
||||
): Map<string, RemoteExpandProperty> {
|
||||
const aliasRealPathMap = new Map<string, string[]>()
|
||||
const parsedExpands = new Map<string, any>()
|
||||
@@ -913,7 +982,10 @@ export class RemoteJoiner {
|
||||
return mergedExpands
|
||||
}
|
||||
|
||||
async query(queryObj: RemoteJoinerQuery): Promise<any> {
|
||||
async query(
|
||||
queryObj: RemoteJoinerQuery,
|
||||
options?: RemoteJoinerOptions
|
||||
): Promise<any> {
|
||||
const serviceConfig = this.getServiceConfig(
|
||||
queryObj.service,
|
||||
queryObj.alias
|
||||
@@ -960,7 +1032,8 @@ export class RemoteJoiner {
|
||||
root,
|
||||
pkName,
|
||||
primaryKeyArg?.value,
|
||||
undefined
|
||||
undefined,
|
||||
options
|
||||
)
|
||||
|
||||
const data = response.path ? response.data[response.path!] : response.data
|
||||
@@ -968,7 +1041,8 @@ export class RemoteJoiner {
|
||||
await this.handleExpands(
|
||||
Array.isArray(data) ? data : [data],
|
||||
parsedExpands,
|
||||
implodeMapping
|
||||
implodeMapping,
|
||||
options
|
||||
)
|
||||
|
||||
return response.data
|
||||
|
||||
@@ -83,6 +83,11 @@ export interface RemoteJoinerQuery {
|
||||
directives?: { [field: string]: JoinerDirective[] }
|
||||
}
|
||||
|
||||
export interface RemoteJoinerOptions {
|
||||
throwIfKeyNotFound?: boolean
|
||||
throwIfRelationNotFound?: boolean | string[]
|
||||
}
|
||||
|
||||
export interface RemoteNestedExpands {
|
||||
[key: string]: {
|
||||
fields: string[]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
JoinerRelationship,
|
||||
JoinerServiceConfig,
|
||||
RemoteJoinerOptions,
|
||||
RemoteJoinerQuery,
|
||||
} from "../joiner"
|
||||
|
||||
@@ -278,7 +279,8 @@ export type ModuleBootstrapDeclaration =
|
||||
|
||||
export type RemoteQueryFunction = (
|
||||
query: string | RemoteJoinerQuery | object,
|
||||
variables?: Record<string, unknown>
|
||||
variables?: Record<string, unknown>,
|
||||
options?: RemoteJoinerOptions
|
||||
) => Promise<any> | null
|
||||
|
||||
export interface IModuleService {
|
||||
|
||||
Reference in New Issue
Block a user