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:
Carlos R. L. Rodrigues
2024-03-29 06:26:24 -03:00
committed by GitHub
parent cbb5e6bd99
commit 1c6ba4468e
10 changed files with 356 additions and 34 deletions
+7
View File
@@ -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)
})
})
},
})
+6 -3
View File
@@ -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 (
+4 -2
View File
@@ -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
+5
View File
@@ -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[]
+3 -1
View File
@@ -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 {