feat(index): Provide a similar API to Query (#9193)

**What**
Align the index engine API to be similar to the Query API

## Example

```ts
        // Benefit from the same level of typing like the remote query

        const { data, metadata } = await indexEngine.query<'product'>({
          fields: [
            "product.*",
            "product.variants.*",
            "product.variants.prices.*",
          ],
          filters: {
            product: {
              variants: {
                prices: {
                  amount: { $gt: 50 },
                },
              },
            },
          },
          pagination: {
            order: {
              product: {
                variants: {
                  prices: {
                    amount: "DESC",
                  },
                },
              },
            },
          },
        })
```
This commit is contained in:
Adrien de Peretti
2024-09-20 10:02:42 +00:00
committed by GitHub
parent 1215a7c094
commit 3084008fc9
40 changed files with 1145 additions and 578 deletions
@@ -0,0 +1,29 @@
import { flattenObjectKeys } from "../flatten-object-keys"
describe("flattenWhereClauses", () => {
it("should flatten where clauses", () => {
const where = {
a: 1,
b: {
c: 2,
d: 3,
z: {
$ilike: "%test%",
},
y: null,
},
e: 4,
}
const result = flattenObjectKeys(where)
expect(result).toEqual({
a: 1,
"b.c": 2,
"b.d": 3,
"b.z": { $ilike: "%test%" },
"b.y": null,
e: 4,
})
})
})
@@ -0,0 +1,22 @@
import { normalizeFieldsSelection } from "../normalize-fields-selection"
describe("normalizeFieldsSelection", () => {
it("should normalize fields selection", () => {
const fields = [
"product.id",
"product.title",
"product.variants.*",
"product.variants.prices.*",
]
const result = normalizeFieldsSelection(fields)
expect(result).toEqual({
product: {
id: true,
title: true,
variants: {
prices: true,
},
},
})
})
})
@@ -5,16 +5,13 @@ import {
MedusaModule,
} from "@medusajs/modules-sdk"
import {
IndexTypes,
JoinerServiceConfigAlias,
ModuleJoinerConfig,
ModuleJoinerRelationship,
} from "@medusajs/types"
import { CommonEvents } from "@medusajs/utils"
import {
SchemaObjectEntityRepresentation,
SchemaObjectRepresentation,
schemaObjectRepresentationPropertiesToOmit,
} from "@types"
import { schemaObjectRepresentationPropertiesToOmit } from "@types"
import { Kind, ObjectTypeDefinitionNode } from "graphql/index"
export const CustomDirectives = {
@@ -27,7 +24,7 @@ export const CustomDirectives = {
},
}
function makeSchemaExecutable(inputSchema: string) {
export function makeSchemaExecutable(inputSchema: string) {
const { schema: cleanedSchema } = cleanGraphQLSchema(inputSchema)
return makeExecutableSchema({ typeDefs: cleanedSchema })
@@ -269,7 +266,7 @@ function retrieveLinkModuleAndAlias({
function getObjectRepresentationRef(
entityName,
{ objectRepresentationRef }
): SchemaObjectEntityRepresentation {
): IndexTypes.SchemaObjectEntityRepresentation {
return (objectRepresentationRef[entityName] ??= {
entity: entityName,
parents: [],
@@ -309,7 +306,7 @@ function processEntity(
}: {
entitiesMap: any
moduleJoinerConfigs: ModuleJoinerConfig[]
objectRepresentationRef: SchemaObjectRepresentation
objectRepresentationRef: IndexTypes.SchemaObjectRepresentation
}
) {
/**
@@ -616,8 +613,11 @@ function processEntity(
* }
* }
*/
function buildAliasMap(objectRepresentation: SchemaObjectRepresentation) {
const aliasMap: SchemaObjectRepresentation["_schemaPropertiesMap"] = {}
function buildAliasMap(
objectRepresentation: IndexTypes.SchemaObjectRepresentation
) {
const aliasMap: IndexTypes.SchemaObjectRepresentation["_schemaPropertiesMap"] =
{}
function recursivelyBuildAliasPath(
current,
@@ -705,7 +705,7 @@ function buildAliasMap(objectRepresentation: SchemaObjectRepresentation) {
*/
export function buildSchemaObjectRepresentation(
schema
): [SchemaObjectRepresentation, Record<string, any>] {
): [IndexTypes.SchemaObjectRepresentation, Record<string, any>] {
const moduleJoinerConfigs = MedusaModule.getAllJoinerConfigs()
const augmentedSchema = CustomDirectives.Listeners.definition + schema
const executableSchema = makeSchemaExecutable(augmentedSchema)
@@ -713,7 +713,7 @@ export function buildSchemaObjectRepresentation(
const objectRepresentation = {
_serviceNameModuleConfigMap: {},
} as SchemaObjectRepresentation
} as IndexTypes.SchemaObjectRepresentation
Object.entries(entitiesMap).forEach(([entityName, entityMapValue]) => {
if (!entityMapValue.astNode) {
@@ -1,11 +1,9 @@
import { SqlEntityManager } from "@mikro-orm/postgresql"
import {
SchemaObjectRepresentation,
schemaObjectRepresentationPropertiesToOmit,
} from "../types"
import { schemaObjectRepresentationPropertiesToOmit } from "@types"
import { IndexTypes } from "@medusajs/types"
export async function createPartitions(
schemaObjectRepresentation: SchemaObjectRepresentation,
schemaObjectRepresentation: IndexTypes.SchemaObjectRepresentation,
manager: SqlEntityManager
): Promise<void> {
const activeSchema = manager.config.get("schema")
@@ -1,5 +1,5 @@
export const defaultSchema = `
type Product @Listeners(values: ["Product.product.created", "Product.product.updated", "Product.product.deleted"]) {
type Product @Listeners(values: ["Product.product.created", "Product.product.updated", "Product.product.deleted"]) {
id: String
title: String
variants: [ProductVariant]
@@ -0,0 +1,59 @@
import { OPERATOR_MAP } from "./query-builder"
/**
* Flatten object keys
* @example
* input: {
* a: 1,
* b: {
* c: 2,
* d: 3,
* },
* e: 4,
* }
*
* output: {
* a: 1,
* b.c: 2,
* b.d: 3,
* e: 4,
* }
*
* @param input
*/
export function flattenObjectKeys(input: Record<string, any>) {
const result: Record<string, any> = {}
function flatten(obj: Record<string, any>, path?: string) {
for (const key in obj) {
const isOperatorMap = !!OPERATOR_MAP[key]
if (isOperatorMap) {
result[path ?? key] = obj
continue
}
const newPath = path ? `${path}.${key}` : key
if (obj[key] === null) {
result[newPath] = null
continue
}
if (Array.isArray(obj[key])) {
result[newPath] = obj[key]
continue
}
if (typeof obj[key] === "object" && !isOperatorMap) {
flatten(obj[key], newPath)
continue
}
result[newPath] = obj[key]
}
}
flatten(input)
return result
}
@@ -0,0 +1,82 @@
import { join } from "path"
import { CustomDirectives, makeSchemaExecutable } from "./build-config"
import {
gqlSchemaToTypes as ModulesSdkGqlSchemaToTypes,
MedusaModule,
} from "@medusajs/modules-sdk"
import { FileSystem } from "@medusajs/utils"
import * as process from "process"
export async function gqlSchemaToTypes(schema: string) {
const augmentedSchema = CustomDirectives.Listeners.definition + schema
const executableSchema = makeSchemaExecutable(augmentedSchema)
const filename = "index-service-entry-points"
const filenameWithExt = filename + ".d.ts"
const dir = join(process.cwd(), ".medusa")
await ModulesSdkGqlSchemaToTypes({
schema: executableSchema,
filename,
outputDir: dir,
})
const fileSystem = new FileSystem(dir)
let content = await fileSystem.contents(filenameWithExt)
await fileSystem.remove(filenameWithExt)
const entryPoints = buildEntryPointsTypeMap(content)
const indexEntryPoints = `
declare module '@medusajs/types' {
interface IndexServiceEntryPoints {
${entryPoints
.map((entry) => ` ${entry.entryPoint}: ${entry.entityType}`)
.join("\n")}
}
}`
const contentToReplaceMatcher = new RegExp(
`declare\\s+module\\s+['"][^'"]+['"]\\s*{([^}]*?)}\\s+}`,
"gm"
)
content = content.replace(contentToReplaceMatcher, indexEntryPoints)
await fileSystem.create(filenameWithExt, content)
}
function buildEntryPointsTypeMap(
schema: string
): { entryPoint: string; entityType: any }[] {
// build map entry point to there type to be merged and used by the remote query
const joinerConfigs = MedusaModule.getAllJoinerConfigs()
return joinerConfigs
.flatMap((config) => {
const aliases = Array.isArray(config.alias)
? config.alias
: config.alias
? [config.alias]
: []
return aliases.flatMap((alias) => {
const names = Array.isArray(alias.name) ? alias.name : [alias.name]
const entity = alias?.["entity"]
return names.map((aliasItem) => {
if (!schema.includes(`export type ${entity} `)) {
return
}
return {
entryPoint: aliasItem,
entityType: entity
? schema.includes(`export type ${entity} `)
? alias?.["entity"]
: "any"
: "any",
}
})
})
})
.filter(Boolean) as { entryPoint: string; entityType: any }[]
}
@@ -0,0 +1,7 @@
import { objectFromStringPath } from "@medusajs/utils"
export function normalizeFieldsSelection(fields: string[]) {
const normalizedFields = fields.map((field) => field.replace(/\.\*/g, ""))
const fieldsObject = objectFromStringPath(normalizedFields)
return fieldsObject
}
@@ -1,14 +1,21 @@
import { isObject, isString } from "@medusajs/utils"
import { IndexTypes } from "@medusajs/types"
import { GraphQLList } from "graphql"
import { Knex } from "knex"
import {
OrderBy,
QueryFormat,
QueryOptions,
SchemaObjectRepresentation,
SchemaPropertiesMap,
Select,
} from "../types"
import { OrderBy, QueryFormat, QueryOptions, Select } from "@types"
export const OPERATOR_MAP = {
$eq: "=",
$lt: "<",
$gt: ">",
$lte: "<=",
$gte: ">=",
$ne: "!=",
$in: "IN",
$is: "IS",
$like: "LIKE",
$ilike: "ILIKE",
}
export class QueryBuilder {
private readonly structure: Select
@@ -16,10 +23,10 @@ export class QueryBuilder {
private readonly knex: Knex
private readonly selector: QueryFormat
private readonly options?: QueryOptions
private readonly schema: SchemaObjectRepresentation
private readonly schema: IndexTypes.SchemaObjectRepresentation
constructor(args: {
schema: SchemaObjectRepresentation
schema: IndexTypes.SchemaObjectRepresentation
entityMap: Record<string, any>
knex: Knex
selector: QueryFormat
@@ -37,7 +44,7 @@ export class QueryBuilder {
return Object.keys(structure ?? {}).filter((key) => key !== "entity")
}
private getEntity(path): SchemaPropertiesMap[0] {
private getEntity(path): IndexTypes.SchemaPropertiesMap[0] {
if (!this.schema._schemaPropertiesMap[path]) {
throw new Error(`Could not find entity for path: ${path}`)
}
@@ -119,18 +126,6 @@ export class QueryBuilder {
obj: object,
builder: Knex.QueryBuilder
) {
const OPERATOR_MAP = {
$eq: "=",
$lt: "<",
$gt: ">",
$lte: "<=",
$gte: ">=",
$ne: "!=",
$in: "IN",
$is: "IS",
$like: "LIKE",
$ilike: "ILIKE",
}
const keys = Object.keys(obj)
const getPathAndField = (key: string) => {