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
@@ -323,6 +323,7 @@ export const ModulesDefinition: {
Modules.EVENT_BUS,
"logger",
ContainerRegistrationKeys.REMOTE_QUERY,
ContainerRegistrationKeys.QUERY,
],
defaultModuleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
@@ -1,5 +1,5 @@
import { MedusaModule } from "../medusa-module"
import { FileSystem } from "@medusajs/utils"
import { FileSystem, toCamelCase } from "@medusajs/utils"
import { GraphQLSchema } from "graphql/type"
import { parse, printSchema } from "graphql"
import { codegen } from "@graphql-codegen/core"
@@ -21,13 +21,13 @@ function buildEntryPointsTypeMap(
return aliases.flatMap((alias) => {
const names = Array.isArray(alias.name) ? alias.name : [alias.name]
const entity = alias.args?.["entity"]
const entity = alias?.["entity"]
return names.map((aliasItem) => {
return {
entryPoint: aliasItem,
entityType: entity
? schema.includes(`export type ${entity} `)
? alias.args?.["entity"]
? alias?.["entity"]
: "any"
: "any",
}
@@ -39,9 +39,11 @@ function buildEntryPointsTypeMap(
async function generateTypes({
outputDir,
filename,
config,
}: {
outputDir: string
filename: string
config: Parameters<typeof codegen>[0]
}) {
const fileSystem = new FileSystem(outputDir)
@@ -49,9 +51,11 @@ async function generateTypes({
let output = await codegen(config)
const entryPoints = buildEntryPointsTypeMap(output)
const interfaceName = toCamelCase(filename)
const remoteQueryEntryPoints = `
declare module '@medusajs/types' {
interface RemoteQueryEntryPoints {
interface ${interfaceName} {
${entryPoints
.map((entry) => ` ${entry.entryPoint}: ${entry.entityType}`)
.join("\n")}
@@ -60,19 +64,31 @@ ${entryPoints
output += remoteQueryEntryPoints
await fileSystem.create("remote-query-types.d.ts", output)
await fileSystem.create(
"index.d.ts",
"export * as RemoteQueryTypes from './remote-query-types'"
)
await fileSystem.create(filename + ".d.ts", output)
const doesBarrelExists = await fileSystem.exists("index.d.ts")
if (!doesBarrelExists) {
await fileSystem.create(
"index.d.ts",
`export * as ${interfaceName}Types from './${filename}'`
)
} else {
const content = await fileSystem.contents("index.d.ts")
if (!content.includes(`${interfaceName}Types`)) {
const newContent = `export * as ${interfaceName}Types from './${filename}'\n${content}`
await fileSystem.create("index.d.ts", newContent)
}
}
}
export async function gqlSchemaToTypes({
schema,
outputDir,
filename,
}: {
schema: GraphQLSchema
outputDir: string
filename: string
}) {
const config = {
documents: [],
@@ -98,5 +114,5 @@ export async function gqlSchemaToTypes({
},
}
await generateTypes({ outputDir, config })
await generateTypes({ outputDir, filename, config })
}
@@ -0,0 +1,63 @@
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 }
}
export type Product = {
__typename?: "Product"
id?: Maybe<Scalars["String"]["output"]>
title?: Maybe<Scalars["String"]["output"]>
variants?: Maybe<Array<Maybe<ProductVariant>>>
}
export type ProductVariant = {
__typename?: "ProductVariant"
id?: Maybe<Scalars["String"]["output"]>
product_id?: Maybe<Scalars["String"]["output"]>
sku?: Maybe<Scalars["String"]["output"]>
prices?: Maybe<Array<Maybe<Price>>>
}
export type Price = {
__typename?: "Price"
amount?: Maybe<Scalars["Int"]["output"]>
}
export interface FixtureEntryPoints {
product_variant: ProductVariant
product_variants: ProductVariant
variant: ProductVariant
variants: ProductVariant
product: Product
products: Product
price: Price
prices: Price
}
declare module "../index-service-entry-points" {
interface IndexServiceEntryPoints extends FixtureEntryPoints {}
}
@@ -0,0 +1,59 @@
import { expectTypeOf } from "expect-type"
import "../__fixtures__/index-service-entry-points"
import { OperatorMap } from "../operator-map"
import { IndexQueryConfig, OrderBy } from "../query-config"
describe("IndexQueryConfig", () => {
it("should infer the config types properly", async () => {
type IndexConfig = IndexQueryConfig<"product">
expectTypeOf<IndexConfig["fields"]>().toEqualTypeOf<
(
| "id"
| "title"
| "variants.*"
| "variants.id"
| "variants.product_id"
| "variants.sku"
| "variants.prices.*"
| "variants.prices.amount"
)[]
>()
expectTypeOf<IndexConfig["filters"]>().toEqualTypeOf<
| {
id?: string | string[] | OperatorMap<string>
title?: string | string[] | OperatorMap<string>
variants?: {
id?: string | string[] | OperatorMap<string>
product_id?: string | string[] | OperatorMap<string>
sku?: string | string[] | OperatorMap<string>
prices?: {
amount?: number | number[] | OperatorMap<number>
}
}
}
| undefined
>()
expectTypeOf<IndexConfig["pagination"]>().toEqualTypeOf<
| {
skip?: number
take?: number
order?: {
id?: OrderBy
title?: OrderBy
variants?: {
id?: OrderBy
product_id?: OrderBy
sku?: OrderBy
prices?: {
amount?: OrderBy
}
}
}
}
| undefined
>()
})
})
+85
View File
@@ -0,0 +1,85 @@
import { ModuleJoinerConfig } from "../modules-sdk"
export type SchemaObjectEntityRepresentation = {
/**
* The name of the type/entity in the schema
*/
entity: string
/**
* All parents a type/entity refers to in the schema
* or through links
*/
parents: {
/**
* The reference to the schema object representation
* of the parent
*/
ref: SchemaObjectEntityRepresentation
/**
* When a link is inferred between two types/entities
* we are configuring the link tree, and therefore we are
* storing the reference to the parent type/entity within the
* schema which defer from the true parent from a pure entity
* point of view
*/
inSchemaRef?: SchemaObjectEntityRepresentation
/**
* The property the data should be assigned to in the parent
*/
targetProp: string
/**
* Are the data expected to be a list or not
*/
isList?: boolean
}[]
/**
* The default fields to query for the type/entity
*/
fields: string[]
/**
* @Listerners directive is required and all listeners found
* for the type will be stored here
*/
listeners: string[]
/**
* The alias for the type/entity retrieved in the corresponding
* module
*/
alias: string
/**
* The module joiner config corresponding to the module the type/entity
* refers to
*/
moduleConfig: ModuleJoinerConfig
}
export type EntityNameModuleConfigMap = {
[key: string]: ModuleJoinerConfig
}
export type SchemaPropertiesMap = {
[key: string]: {
shortCutOf?: string
ref: SchemaObjectEntityRepresentation
}
}
/**
* Represents the schema objects representation once the schema has been processed
*/
export type SchemaObjectRepresentation =
| {
[key: string]: SchemaObjectEntityRepresentation
}
| {
_schemaPropertiesMap: SchemaPropertiesMap
_serviceNameModuleConfigMap: EntityNameModuleConfigMap
}
@@ -0,0 +1,4 @@
/**
* Bucket filled with map of entry point -> types that are autogenerated by the codegen from the config schema
*/
export interface IndexServiceEntryPoints {}
+5
View File
@@ -1 +1,6 @@
export * from "./service"
export * from "./index-service-entry-points"
export * from "./query-config"
export * from "./operator-map"
export * from "./common"
export * from "./sotrage-provider"
@@ -0,0 +1,12 @@
export type OperatorMap<T> = {
$eq: T
$lt: T
$lte: T
$gt: T
$gte: T
$ne: T
$in: T
$is: T
$like: T
$ilike: T
}
@@ -0,0 +1,9 @@
import { Prettify } from "../../common"
export type ExcludedProps = "__typename"
export type Depth = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
export type CleanupObject<T> = Prettify<Omit<Exclude<T, symbol>, ExcludedProps>>
export type OmitNever<T extends object> = {
[K in keyof T as TypeOnly<T[K]> extends never ? never : K]: T[K]
}
export type TypeOnly<T> = Required<Exclude<T, null | undefined>>
@@ -0,0 +1,4 @@
export * from "./query-input-config"
export * from "./query-input-config-fields"
export * from "./query-input-config-filters"
export * from "./query-input-config-order-by"
@@ -0,0 +1,58 @@
import { ExcludedProps, TypeOnly } from "./common"
type Marker = [never, 0, 1, 2, 3, 4]
type RawBigNumberPrefix = "raw_"
type ExpandStarSelector<
T extends object,
Depth extends number,
Exclusion extends string[]
> = ObjectToIndexFields<T & { "*": "*" }, Depth, Exclusion>
/**
* Output an array of strings representing the path to each leaf node in an object
*/
export type ObjectToIndexFields<
MaybeT,
Depth extends number = 2,
Exclusion extends string[] = [],
T = TypeOnly<MaybeT>
> = Depth extends never
? never
: T extends object
? {
[K in keyof T]: K extends // handle big number
`${RawBigNumberPrefix}${string}`
? Exclude<K, symbol>
: // Special props that should be excluded
K extends ExcludedProps
? never
: // Prevent recursive reference to itself
K extends Exclusion[number]
? never
: TypeOnly<T[K]> extends Array<infer R>
? TypeOnly<R> extends Date
? Exclude<K, symbol>
: 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 { __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
@@ -0,0 +1,66 @@
import { Prettify } from "../../common"
import { IndexServiceEntryPoints } from "../index-service-entry-points"
import { OperatorMap } from "../operator-map"
import {
CleanupObject,
Depth,
ExcludedProps,
OmitNever,
TypeOnly,
} from "./common"
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
: TypeOnly<T[Key]> extends string | number | boolean | Date
? TypeOnly<T[Key]> | TypeOnly<T[Key]>[] | OperatorMap<TypeOnly<T[Key]>>
: TypeOnly<T[Key]> extends Array<infer R>
? TypeOnly<R> extends { __typename: any }
? IndexFilters<Key & string, T, [Key & string, ...Exclusion], Depth[Lim]>
: TypeOnly<R> extends object
? CleanupObject<TypeOnly<R>>
: never
: TypeOnly<T[Key]> extends { __typename: any }
? IndexFilters<
Key & string,
T[Key],
[Key & string, ...Exclusion],
Depth[Lim]
>
: TypeOnly<T[Key]> extends object
? CleanupObject<TypeOnly<T[Key]>>
: never
}
/**
* Extract all available filters from an index entry point deeply
*/
export type IndexFilters<
TEntry extends string,
IndexEntryPointsLevel = IndexServiceEntryPoints,
Exclusion extends string[] = [],
Lim extends number = Depth[3]
> = Lim extends number
? TEntry extends keyof IndexEntryPointsLevel
? TypeOnly<IndexEntryPointsLevel[TEntry]> extends Array<infer V>
? Prettify<
OmitNever<ExtractFiltersOperators<V, Lim, [TEntry, ...Exclusion]>>
>
: Prettify<
OmitNever<
ExtractFiltersOperators<
IndexEntryPointsLevel[TEntry],
Lim,
[TEntry, ...Exclusion]
>
>
>
: Record<string, any>
: never
@@ -0,0 +1,67 @@
import { Prettify } from "../../common"
import { IndexServiceEntryPoints } from "../index-service-entry-points"
import {
CleanupObject,
Depth,
ExcludedProps,
OmitNever,
TypeOnly,
} from "./common"
export type OrderBy = "ASC" | "DESC" | 1 | -1 | true | false
type ExtractOrderByOperators<
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
: TypeOnly<T[Key]> extends string | number | boolean | Date
? OrderBy
: TypeOnly<T[Key]> extends Array<infer R>
? TypeOnly<R> extends { __typename: any }
? IndexOrderBy<Key & string, T, [Key & string, ...Exclusion], Depth[Lim]>
: TypeOnly<R> extends object
? CleanupObject<TypeOnly<R>>
: never
: TypeOnly<T[Key]> extends { __typename: any }
? IndexOrderBy<
Key & string,
T[Key],
[Key & string, ...Exclusion],
Depth[Lim]
>
: TypeOnly<T[Key]> extends object
? CleanupObject<TypeOnly<T[Key]>>
: never
}
/**
* Extract all available orderBy from a remote entry point deeply
*/
export type IndexOrderBy<
TEntry extends string,
IndexEntryPointsLevel = IndexServiceEntryPoints,
Exclusion extends string[] = [],
Lim extends number = Depth[3]
> = Lim extends number
? TEntry extends keyof IndexEntryPointsLevel
? TypeOnly<IndexEntryPointsLevel[TEntry]> extends Array<infer V>
? Prettify<
OmitNever<ExtractOrderByOperators<V, Lim, [TEntry, ...Exclusion]>>
>
: Prettify<
OmitNever<
ExtractOrderByOperators<
IndexEntryPointsLevel[TEntry],
Lim,
[TEntry, ...Exclusion]
>
>
>
: Record<string, any>
: never
@@ -0,0 +1,40 @@
import { ObjectToIndexFields } from "./query-input-config-fields"
import { IndexFilters } from "./query-input-config-filters"
import { IndexOrderBy } from "./query-input-config-order-by"
import { IndexServiceEntryPoints } from "../index-service-entry-points"
export type IndexQueryConfig<TEntry extends string> = {
fields: ObjectToIndexFields<
IndexServiceEntryPoints[TEntry & keyof IndexServiceEntryPoints]
> extends never
? string[]
: ObjectToIndexFields<
IndexServiceEntryPoints[TEntry & keyof IndexServiceEntryPoints]
>[]
filters?: IndexFilters<TEntry>
joinFilters?: IndexFilters<TEntry>
pagination?: {
skip?: number
take?: number
order?: IndexOrderBy<TEntry>
}
keepFilteredEntities?: boolean
}
export type QueryFunctionReturnPagination = {
skip?: number
take?: number
count: number
}
/**
* The QueryResultSet presents a typed output for the
* result returned by the index search engine, it doesnt narrow down the type
* based on the intput fields.
*/
export type QueryResultSet<TEntry extends string> = {
data: TEntry extends keyof IndexServiceEntryPoints
? IndexServiceEntryPoints[TEntry][]
: any[]
metadata?: QueryFunctionReturnPagination
}
+17 -3
View File
@@ -1,6 +1,20 @@
import { IModuleService } from "../modules-sdk"
import { IModuleService, ModuleServiceInitializeOptions } from "../modules-sdk"
import { IndexQueryConfig, QueryResultSet } from "./query-config"
/**
* Represents the module options that can be provided
*/
export interface IndexModuleOptions {
customAdapter?: {
constructor: new (...args: any[]) => any
options: any
}
defaultAdapterOptions?: ModuleServiceInitializeOptions
schema: string
}
export interface IIndexService extends IModuleService {
query(...args): Promise<any>
queryAndCount(...args): Promise<any>
query<const TEntry extends string>(
config: IndexQueryConfig<TEntry>
): Promise<QueryResultSet<TEntry>>
}
@@ -0,0 +1,27 @@
import { IndexQueryConfig, QueryResultSet } from "./query-config"
import { Subscriber } from "../event-bus"
import { SchemaObjectEntityRepresentation } from "./common"
/**
* Represents the storage provider interface,
*/
export interface StorageProvider {
/*new (
container: Record<string, any>,
options: {
schemaObjectRepresentation: SchemaObjectRepresentation
entityMap: Record<string, any>
},
moduleOptions: IndexModuleOptions
)*/
onApplicationStart?(): Promise<void>
query<const TEntry extends string>(
config: IndexQueryConfig<TEntry>
): Promise<QueryResultSet<TEntry>>
consumeEvent(
schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation
): Subscriber<any>
}