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 12:02:42 +02:00
committed by GitHub
parent 1215a7c094
commit 3084008fc9
40 changed files with 1145 additions and 578 deletions

View File

@@ -1,4 +1,4 @@
dist/
node_modules
*yarn-error.log
.medusa

View File

@@ -58,25 +58,35 @@ medusaIntegrationTestRunner({
// Timeout to allow indexing to finish
await setTimeout(2000)
const [results, count] = await indexEngine.queryAndCount(
{
select: {
product: {
variants: {
prices: true,
const { data: results } = await indexEngine.query<"product">({
fields: [
"product.*",
"product.variants.*",
"product.variants.prices.*",
],
filters: {
product: {
variants: {
prices: {
amount: { $gt: 50 },
},
},
},
where: {
"product.variants.prices.amount": { $gt: 50 },
},
pagination: {
order: {
product: {
variants: {
prices: {
amount: "DESC",
},
},
},
},
},
{
orderBy: [{ "product.variants.prices.amount": "DESC" }],
}
)
})
expect(count).toBe(1)
expect(results.length).toBe(1)
const variants = results[0].variants
@@ -118,26 +128,36 @@ medusaIntegrationTestRunner({
// Timeout to allow indexing to finish
await setTimeout(2000)
const [results, count] = await indexEngine.queryAndCount(
{
select: {
product: {
variants: {
prices: true,
const { data: results } = await indexEngine.query<"product">({
fields: [
"product.*",
"product.variants.*",
"product.variants.prices.*",
],
filters: {
product: {
variants: {
prices: {
amount: { $gt: 50 },
currency_code: { $eq: "AUD" },
},
},
},
where: {
"product.variants.prices.amount": { $gt: 50 },
"product.variants.prices.currency_code": { $eq: "AUD" },
},
pagination: {
order: {
product: {
variants: {
prices: {
amount: "DESC",
},
},
},
},
},
{
orderBy: [{ "product.variants.prices.amount": "DESC" }],
}
)
})
expect(count).toBe(1)
expect(results.length).toBe(1)
const variants = results[0].variants
@@ -179,32 +199,40 @@ medusaIntegrationTestRunner({
await setTimeout(5000)
const queryArgs = [
{
select: {
product: {
variants: {
prices: true,
const queryArgs = {
fields: [
"product.*",
"product.variants.*",
"product.variants.prices.*",
],
filters: {
product: {
variants: {
prices: {
amount: { $gt: 50 },
currency_code: { $eq: "AUD" },
},
},
},
where: {
"product.variants.prices.amount": { $gt: 50 },
"product.variants.prices.currency_code": { $eq: "AUD" },
},
pagination: {
order: {
product: {
variants: {
prices: {
amount: "DESC",
},
},
},
},
},
{
orderBy: [{ "product.variants.prices.amount": "DESC" }],
},
]
}
await indexEngine.queryAndCount(...queryArgs)
await indexEngine.query<"product">(queryArgs)
const [results, count, perf] = await indexEngine.queryAndCount(
...queryArgs
const { data: results, metadata } = await indexEngine.query<"product">(
queryArgs
)
console.log(perf)
})
})
},

View File

@@ -19,7 +19,7 @@
"skipLibCheck": true,
"downlevelIteration": true // to use ES5 specific tooling
},
"include": ["src"],
"include": ["src", "./medusa/**/*"],
"exclude": [
"./dist/**/*",
"__tests__",

View File

@@ -323,6 +323,7 @@ export const ModulesDefinition: {
Modules.EVENT_BUS,
"logger",
ContainerRegistrationKeys.REMOTE_QUERY,
ContainerRegistrationKeys.QUERY,
],
defaultModuleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,

View File

@@ -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 })
}

View File

@@ -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 {}
}

View File

@@ -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
>()
})
})

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
}

View File

@@ -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 {}

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"

View File

@@ -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
}

View File

@@ -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>>

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

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>>
}

View File

@@ -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>
}

View File

@@ -33,7 +33,7 @@
"scripts": {
"watch": "tsc --watch -p ./tsconfig.build.json",
"watch:test": "tsc --build tsconfig.spec.json --watch",
"build": "rimraf dist && tsc --noEmit && tsc -p ./tsconfig.build.json && tsc-alias -p ./tsconfig.build.json",
"build": "rimraf dist && tsc -p ./tsconfig.spec.json --noEmit && tsc -p ./tsconfig.build.json && tsc-alias -p ./tsconfig.build.json",
"test": "jest --runInBand --bail --passWithNoTests --forceExit -- src",
"test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.ts"
},

View File

@@ -64,6 +64,7 @@ async function start({ port, directory, types }) {
const outputDirGeneratedTypes = path.join(directory, ".medusa")
await gqlSchemaToTypes({
outputDir: outputDirGeneratedTypes,
filename: "remote-query-entry-points",
schema: gqlSchema,
})
logger.info("Geneated modules types")

View File

@@ -4,3 +4,4 @@ node_modules
.env*
.env
*.sql
.medusa

View File

@@ -1,11 +1,11 @@
import {
MedusaAppLoader,
configLoader,
container,
logger,
MedusaAppLoader,
} from "@medusajs/framework"
import { MedusaAppOutput, MedusaModule } from "@medusajs/modules-sdk"
import { EventBusTypes } from "@medusajs/types"
import { EventBusTypes, IndexTypes } from "@medusajs/types"
import {
ContainerRegistrationKeys,
ModuleRegistrationName,
@@ -21,7 +21,10 @@ import { EventBusServiceMock } from "../__fixtures__"
import { dbName } from "../__fixtures__/medusa-config"
const eventBusMock = new EventBusServiceMock()
const remoteQueryMock = jest.fn()
const queryMock = {
graph: jest.fn(),
}
const dbUtils = dbTestUtilFactory()
jest.setTimeout(300000)
@@ -34,44 +37,55 @@ const linkId = "link_id_1"
const sendEvents = async (eventDataToEmit) => {
let a = 0
remoteQueryMock.mockImplementation((query) => {
query = query.__value
if (query.product) {
queryMock.graph = jest.fn().mockImplementation((query) => {
const entity = query.entity
if (entity === "product") {
return {
id: a++ > 0 ? "aaaa" : productId,
}
} else if (query.product_variant) {
return {
id: variantId,
sku: "aaa test aaa",
product: {
id: productId,
data: {
id: a++ > 0 ? "aaaa" : productId,
},
}
} else if (query.price_set) {
} else if (entity === "product_variant") {
return {
id: priceSetId,
}
} else if (query.price) {
return {
id: priceId,
amount: 100,
price_set: [
{
id: priceSetId,
data: {
id: variantId,
sku: "aaa test aaa",
product: {
id: productId,
},
],
},
}
} else if (query.product_variant_price_set) {
} else if (entity === "price_set") {
return {
id: linkId,
variant_id: variantId,
price_set_id: priceSetId,
variant: [
{
id: variantId,
},
],
data: {
id: priceSetId,
},
}
} else if (entity === "price") {
return {
data: {
id: priceId,
amount: 100,
price_set: [
{
id: priceSetId,
},
],
},
}
} else if (entity === "product_variant_price_set") {
return {
data: {
id: linkId,
variant_id: variantId,
price_set_id: priceSetId,
variant: [
{
id: variantId,
},
],
},
}
}
@@ -83,6 +97,7 @@ const sendEvents = async (eventDataToEmit) => {
let isFirstTime = true
let medusaAppLoader!: MedusaAppLoader
let index!: IndexTypes.IIndexService
const beforeAll_ = async () => {
try {
@@ -94,7 +109,7 @@ const beforeAll_ = async () => {
container.register({
[ContainerRegistrationKeys.LOGGER]: asValue(logger),
[ContainerRegistrationKeys.REMOTE_QUERY]: asValue(null),
[ContainerRegistrationKeys.QUERY]: asValue(null),
[ContainerRegistrationKeys.PG_CONNECTION]: asValue(dbUtils.pgConnection_),
})
@@ -112,16 +127,13 @@ const beforeAll_ = async () => {
// Bootstrap modules
const globalApp = await medusaAppLoader.load()
const index = container.resolve(Modules.INDEX)
index = container.resolve(Modules.INDEX)
// Mock event bus the index module
;(index as any).eventBusModuleService_ = eventBusMock
await globalApp.onApplicationStart()
jest
.spyOn((index as any).storageProvider_, "remoteQuery_")
.mockImplementation(remoteQueryMock)
;(index as any).storageProvider_.query_ = queryMock
return globalApp
} catch (error) {
@@ -236,8 +248,6 @@ describe("IndexModuleService", function () {
afterEach(afterEach_)
it("should create the corresponding index entries and index relation entries", async function () {
expect(remoteQueryMock).toHaveBeenCalledTimes(6)
/**
* Validate all index entries and index relation entries
*/
@@ -396,8 +406,6 @@ describe("IndexModuleService", function () {
afterEach(afterEach_)
it("should create the corresponding index entries and index relation entries", async function () {
expect(remoteQueryMock).toHaveBeenCalledTimes(6)
/**
* Validate all index entries and index relation entries
*/
@@ -554,35 +562,38 @@ describe("IndexModuleService", function () {
await updateData(manager)
remoteQueryMock.mockImplementation((query) => {
query = query.__value
if (query.product) {
queryMock.graph = jest.fn().mockImplementation((query) => {
const entity = query.entity
if (entity === "product") {
return {
id: productId,
title: "updated Title",
data: {
id: productId,
title: "updated Title",
},
}
} else if (query.product_variant) {
} else if (entity === "product_variant") {
return {
id: variantId,
sku: "updated sku",
product: [
{
id: productId,
},
],
data: {
id: variantId,
sku: "updated sku",
product: [
{
id: productId,
},
],
},
}
}
return {}
})
await eventBusMock.emit(eventDataToEmit)
})
afterEach(afterEach_)
it("should update the corresponding index entries", async () => {
expect(remoteQueryMock).toHaveBeenCalledTimes(4)
const updatedIndexEntries = await manager.find(IndexData, {})
expect(updatedIndexEntries).toHaveLength(2)
@@ -667,20 +678,24 @@ describe("IndexModuleService", function () {
medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX) as any
).container_.manager as EntityManager
remoteQueryMock.mockImplementation((query) => {
query = query.__value
if (query.product) {
queryMock.graph = jest.fn().mockImplementation((query) => {
const entity = query.entity
if (entity === "product") {
return {
id: productId,
data: {
id: productId,
},
}
} else if (query.product_variant) {
} else if (entity === "product_variant") {
return {
id: variantId,
product: [
{
id: productId,
},
],
data: {
id: variantId,
product: [
{
id: productId,
},
],
},
}
}
@@ -693,8 +708,6 @@ describe("IndexModuleService", function () {
afterEach(afterEach_)
it("should consume all deleted events and delete the index entries", async () => {
expect(remoteQueryMock).toHaveBeenCalledTimes(7)
const indexEntries = await manager.find(IndexData, {})
const indexRelationEntries = await manager.find(IndexRelation, {})

View File

@@ -1,8 +1,8 @@
import {
MedusaAppLoader,
configLoader,
container,
logger,
MedusaAppLoader,
} from "@medusajs/framework"
import { MedusaAppOutput, MedusaModule } from "@medusajs/modules-sdk"
import { IndexTypes } from "@medusajs/types"
@@ -21,7 +21,10 @@ import { EventBusServiceMock } from "../__fixtures__"
import { dbName } from "../__fixtures__/medusa-config"
const eventBusMock = new EventBusServiceMock()
const remoteQueryMock = jest.fn()
const queryMock = jest.fn().mockReturnValue({
graph: jest.fn(),
})
const dbUtils = dbTestUtilFactory()
jest.setTimeout(300000)
@@ -39,7 +42,7 @@ const beforeAll_ = async () => {
container.register({
[ContainerRegistrationKeys.LOGGER]: asValue(logger),
[ContainerRegistrationKeys.REMOTE_QUERY]: asValue(null),
[ContainerRegistrationKeys.QUERY]: asValue(null),
[ContainerRegistrationKeys.PG_CONNECTION]: asValue(dbUtils.pgConnection_),
})
@@ -64,9 +67,7 @@ const beforeAll_ = async () => {
await globalApp.onApplicationStart()
jest
.spyOn((index as any).storageProvider_, "remoteQuery_")
.mockImplementation(remoteQueryMock)
;(index as any).storageProvider_.query_ = queryMock
return globalApp
} catch (error) {
@@ -287,23 +288,20 @@ describe("IndexModuleService query", function () {
afterEach(afterEach_)
it("should query all products ordered by sku DESC", async () => {
const [result, count] = await module.queryAndCount(
{
select: {
const { data } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
pagination: {
order: {
product: {
variants: {
prices: true,
sku: "DESC",
},
},
},
},
{
orderBy: [{ "product.variants.sku": "DESC" }],
}
)
})
expect(count).toEqual(2)
expect(result).toEqual([
expect(data).toEqual([
{
id: "prod_2",
title: "Product 2 title",
@@ -345,20 +343,18 @@ describe("IndexModuleService query", function () {
})
it("should query products filtering by variant sku", async () => {
const result = await module.query({
select: {
const { data } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
filters: {
product: {
variants: {
prices: true,
sku: { $like: "aaa%" },
},
},
},
where: {
"product.variants.sku": { $like: "aaa%" },
},
})
expect(result).toEqual([
expect(data).toEqual([
{
id: "prod_1",
variants: [
@@ -378,24 +374,21 @@ describe("IndexModuleService query", function () {
})
it("should query products filtering by price and returning the complete entity", async () => {
const [result] = await module.queryAndCount(
{
select: {
product: {
variants: {
prices: true,
const { data } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
filters: {
product: {
variants: {
prices: {
amount: { $gt: 50 },
},
},
},
where: {
"product.variants.prices.amount": { $gt: "50" },
},
},
{
keepFilteredEntities: true,
}
)
expect(result).toEqual([
keepFilteredEntities: true,
})
expect(data).toEqual([
{
id: "prod_1",
variants: [
@@ -425,18 +418,11 @@ describe("IndexModuleService query", function () {
})
it("should query all products", async () => {
const [result, count] = await module.queryAndCount({
select: {
product: {
variants: {
prices: true,
},
},
},
const { data } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
})
expect(count).toEqual(2)
expect(result).toEqual([
expect(data).toEqual([
{
id: "prod_1",
variants: [
@@ -477,23 +463,15 @@ describe("IndexModuleService query", function () {
})
it("should paginate products", async () => {
const result = await module.query(
{
select: {
product: {
variants: {
prices: true,
},
},
},
},
{
const { data } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
pagination: {
take: 1,
skip: 1,
}
)
},
})
expect(result).toEqual([
expect(data).toEqual([
{
id: "prod_2",
title: "Product 2 title",
@@ -509,20 +487,18 @@ describe("IndexModuleService query", function () {
})
it("should handle null values on where clause", async () => {
const result = await module.query({
select: {
const { data } = await module.query({
fields: ["product.*", "product.variants.*", "product.variants.prices.*"],
filters: {
product: {
variants: {
prices: true,
sku: null,
},
},
},
where: {
"product.variants.sku": null,
},
})
expect(result).toEqual([
expect(data).toEqual([
{
id: "prod_2",
title: "Product 2 title",
@@ -538,15 +514,20 @@ describe("IndexModuleService query", function () {
})
it("should query products filtering by deep nested levels", async () => {
const [result] = await module.queryAndCount({
select: {
product: true,
},
where: {
"product.deep.obj.b": 15,
const { data } = await module.query({
fields: ["product.*"],
filters: {
product: {
deep: {
obj: {
b: 15,
},
},
},
},
})
expect(result).toEqual([
expect(data).toEqual([
{
id: "prod_2",
title: "Product 2 title",

View File

@@ -23,7 +23,7 @@
"scripts": {
"watch": "tsc --build --watch",
"watch:test": "tsc --build tsconfig.spec.json --watch",
"build": "rimraf dist && tsc --noEmit && tsc -p ./tsconfig.build.json && tsc-alias -p ./tsconfig.build.json",
"build": "rimraf dist && tsc -p ./tsconfig.spec.json --noEmit && tsc -p ./tsconfig.build.json && tsc-alias -p ./tsconfig.build.json",
"test": "jest --passWithNoTests ./src",
"test:integration": "jest --runInBand --forceExit -- integration-tests/**/__tests__/**/*.ts",
"migration:generate": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:generate",

View File

@@ -1,4 +1,5 @@
import {
Constructor,
IEventBusModuleService,
IndexTypes,
InternalModuleDeclaration,
@@ -9,36 +10,32 @@ import {
MikroOrmBaseRepository as BaseRepository,
Modules,
} from "@medusajs/utils"
import {
IndexModuleOptions,
SchemaObjectRepresentation,
schemaObjectRepresentationPropertiesToOmit,
StorageProvider,
} from "@types"
import { schemaObjectRepresentationPropertiesToOmit } from "@types"
import { buildSchemaObjectRepresentation } from "../utils/build-config"
import { defaultSchema } from "../utils/default-schema"
import { gqlSchemaToTypes } from "../utils/gql-to-types"
type InjectedDependencies = {
[Modules.EVENT_BUS]: IEventBusModuleService
storageProviderCtr: StorageProvider
storageProviderCtr: Constructor<IndexTypes.StorageProvider>
[ContainerRegistrationKeys.QUERY]: RemoteQueryFunction
storageProviderCtrOptions: unknown
[ContainerRegistrationKeys.REMOTE_QUERY]: RemoteQueryFunction
baseRepository: BaseRepository
}
export default class IndexModuleService implements IndexTypes.IIndexService {
private readonly container_: InjectedDependencies
private readonly moduleOptions_: IndexModuleOptions
private readonly moduleOptions_: IndexTypes.IndexModuleOptions
protected readonly eventBusModuleService_: IEventBusModuleService
protected schemaObjectRepresentation_: SchemaObjectRepresentation
protected schemaObjectRepresentation_: IndexTypes.SchemaObjectRepresentation
protected schemaEntitiesMap_: Record<string, any>
protected readonly storageProviderCtr_: StorageProvider
protected readonly storageProviderCtr_: Constructor<IndexTypes.StorageProvider>
protected readonly storageProviderCtrOptions_: unknown
protected storageProvider_: StorageProvider
protected storageProvider_: IndexTypes.StorageProvider
constructor(
container: InjectedDependencies,
@@ -46,7 +43,7 @@ export default class IndexModuleService implements IndexTypes.IIndexService {
) {
this.container_ = container
this.moduleOptions_ = (moduleDeclaration.options ??
moduleDeclaration) as unknown as IndexModuleOptions
moduleDeclaration) as unknown as IndexTypes.IndexModuleOptions
const {
[Modules.EVENT_BUS]: eventBusModuleService,
@@ -65,10 +62,6 @@ export default class IndexModuleService implements IndexTypes.IIndexService {
}
}
__joinerConfig() {
return {}
}
__hooks = {
onApplicationStart(this: IndexModuleService) {
return this.onApplicationStart_()
@@ -86,31 +79,29 @@ export default class IndexModuleService implements IndexTypes.IIndexService {
entityMap: this.schemaEntitiesMap_,
}),
this.moduleOptions_
)
) as IndexTypes.StorageProvider
this.registerListeners()
if (this.storageProvider_.onApplicationStart) {
await this.storageProvider_.onApplicationStart()
}
await gqlSchemaToTypes(this.moduleOptions_.schema ?? defaultSchema)
} catch (e) {
console.log(e)
}
}
async query(...args) {
return await this.storageProvider_.query.apply(this.storageProvider_, args)
}
async queryAndCount(...args) {
return await this.storageProvider_.queryAndCount.apply(
this.storageProvider_,
args
)
async query<const TEntry extends string>(
config: IndexTypes.IndexQueryConfig<TEntry>
): Promise<IndexTypes.QueryResultSet<TEntry>> {
return await this.storageProvider_.query(config)
}
protected registerListeners() {
const schemaObjectRepresentation = this.schemaObjectRepresentation_ ?? {}
const schemaObjectRepresentation = (this.schemaObjectRepresentation_ ??
{}) as IndexTypes.SchemaObjectRepresentation
for (const [entityName, schemaEntityObjectRepresentation] of Object.entries(
schemaObjectRepresentation
@@ -119,7 +110,9 @@ export default class IndexModuleService implements IndexTypes.IIndexService {
continue
}
schemaEntityObjectRepresentation.listeners.forEach((listener) => {
;(
schemaEntityObjectRepresentation as IndexTypes.SchemaObjectEntityRepresentation
).listeners.forEach((listener) => {
this.eventBusModuleService_.subscribe(
listener,
this.storageProvider_.consumeEvent(schemaEntityObjectRepresentation)
@@ -136,6 +129,7 @@ export default class IndexModuleService implements IndexTypes.IIndexService {
const [objectRepresentation, entityMap] = buildSchemaObjectRepresentation(
this.moduleOptions_.schema ?? defaultSchema
)
this.schemaObjectRepresentation_ = objectRepresentation
this.schemaEntitiesMap_ = entityMap

View File

@@ -1,6 +1,7 @@
import {
Context,
Event,
IndexTypes,
RemoteQueryFunction,
Subscriber,
} from "@medusajs/types"
@@ -11,27 +12,20 @@ import {
isDefined,
MedusaContext,
MikroOrmBaseRepository as BaseRepository,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { EntityManager, SqlEntityManager } from "@mikro-orm/postgresql"
import { IndexData, IndexRelation } from "@models"
import {
EntityNameModuleConfigMap,
IndexModuleOptions,
QueryFormat,
QueryOptions,
SchemaObjectEntityRepresentation,
SchemaObjectRepresentation,
} from "@types"
import { createPartitions, QueryBuilder } from "../utils"
import { flattenObjectKeys } from "../utils/flatten-object-keys"
import { normalizeFieldsSelection } from "../utils/normalize-fields-selection"
type InjectedDependencies = {
manager: EntityManager
[ContainerRegistrationKeys.REMOTE_QUERY]: RemoteQueryFunction
[ContainerRegistrationKeys.QUERY]: RemoteQueryFunction
baseRepository: BaseRepository
}
export class PostgresProvider {
export class PostgresProvider implements IndexTypes.StorageProvider {
#isReady_: Promise<boolean>
protected readonly eventActionToMethodMap_ = {
@@ -43,29 +37,25 @@ export class PostgresProvider {
}
protected container_: InjectedDependencies
protected readonly schemaObjectRepresentation_: SchemaObjectRepresentation
protected readonly schemaObjectRepresentation_: IndexTypes.SchemaObjectRepresentation
protected readonly schemaEntitiesMap_: Record<string, any>
protected readonly moduleOptions_: IndexModuleOptions
protected readonly moduleOptions_: IndexTypes.IndexModuleOptions
protected readonly manager_: SqlEntityManager
protected readonly remoteQuery_: RemoteQueryFunction
protected readonly query_: RemoteQueryFunction
protected baseRepository_: BaseRepository
constructor(
{
manager,
[ContainerRegistrationKeys.REMOTE_QUERY]: remoteQuery,
baseRepository,
}: InjectedDependencies,
container: InjectedDependencies,
options: {
schemaObjectRepresentation: SchemaObjectRepresentation
schemaObjectRepresentation: IndexTypes.SchemaObjectRepresentation
entityMap: Record<string, any>
},
moduleOptions: IndexModuleOptions
moduleOptions: IndexTypes.IndexModuleOptions
) {
this.manager_ = manager
this.remoteQuery_ = remoteQuery
this.manager_ = container.manager
this.query_ = container.query
this.moduleOptions_ = moduleOptions
this.baseRepository_ = baseRepository
this.baseRepository_ = container.baseRepository
this.schemaObjectRepresentation_ = options.schemaObjectRepresentation
this.schemaEntitiesMap_ = options.entityMap
@@ -122,7 +112,7 @@ export class PostgresProvider {
TData extends { id: string; [key: string]: unknown }
>(
data: TData | TData[],
schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation
schemaEntityObjectRepresentation: IndexTypes.SchemaObjectEntityRepresentation
) {
const data_ = Array.isArray(data) ? data : [data]
@@ -194,120 +184,8 @@ export class PostgresProvider {
return result
}
@InjectManager("baseRepository_")
async query(
selection: QueryFormat,
options?: QueryOptions,
@MedusaContext() sharedContext: Context = {}
) {
await this.#isReady_
const { manager } = sharedContext as { manager: SqlEntityManager }
let hasPagination = false
if (
typeof options?.take === "number" ||
typeof options?.skip === "number"
) {
hasPagination = true
}
const connection = manager.getConnection()
const qb = new QueryBuilder({
schema: this.schemaObjectRepresentation_,
entityMap: this.schemaEntitiesMap_,
knex: connection.getKnex(),
selector: selection,
options,
})
const sql = qb.buildQuery(hasPagination, !!options?.keepFilteredEntities)
let resultset = await manager.execute(sql)
if (options?.keepFilteredEntities) {
const mainEntity = Object.keys(selection.select)[0]
const ids = resultset.map((r) => r[`${mainEntity}.id`])
if (ids.length) {
const selection_ = {
select: selection.select,
joinWhere: selection.joinWhere,
where: {
[`${mainEntity}.id`]: ids,
},
}
return await this.query(selection_, undefined, sharedContext)
}
}
return qb.buildObjectFromResultset(resultset)
}
@InjectManager("baseRepository_")
async queryAndCount(
selection: QueryFormat,
options?: QueryOptions,
@MedusaContext() sharedContext: Context = {}
): Promise<[Record<string, any>[], number, PerformanceEntry]> {
await this.#isReady_
const { manager } = sharedContext as { manager: SqlEntityManager }
const connection = manager.getConnection()
const qb = new QueryBuilder({
schema: this.schemaObjectRepresentation_,
entityMap: this.schemaEntitiesMap_,
knex: connection.getKnex(),
selector: selection,
options,
})
const sql = qb.buildQuery(true, !!options?.keepFilteredEntities)
performance.mark("index-query-start")
let resultset = await connection.execute(sql)
performance.mark("index-query-end")
const performanceMesurements = performance.measure(
"index-query-end",
"index-query-start"
)
const count = +(resultset[0]?.count ?? 0)
if (options?.keepFilteredEntities) {
const mainEntity = Object.keys(selection.select)[0]
const ids = resultset.map((r) => r[`${mainEntity}.id`])
if (ids.length) {
const selection_ = {
select: selection.select,
joinWhere: selection.joinWhere,
where: {
[`${mainEntity}.id`]: ids,
},
}
performance.mark("index-query-start")
resultset = await this.query(selection_, undefined, sharedContext)
performance.mark("index-query-end")
const performanceMesurements = performance.measure(
"index-query-end",
"index-query-start"
)
return [resultset, count, performanceMesurements]
}
}
return [
qb.buildObjectFromResultset(resultset),
count,
performanceMesurements,
]
}
consumeEvent(
schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation
schemaEntityObjectRepresentation: IndexTypes.SchemaObjectEntityRepresentation
): Subscriber<{ id: string }> {
return async (data: Event) => {
await this.#isReady_
@@ -325,17 +203,13 @@ export class PostgresProvider {
}
const { fields, alias } = schemaEntityObjectRepresentation
const entityData = await this.remoteQuery_(
remoteQueryObjectFromString({
entryPoint: alias,
variables: {
filters: {
id: ids,
},
},
fields: [...new Set(["id", ...fields])],
})
)
const { data: entityData } = await this.query_.graph({
entity: alias,
filters: {
id: ids,
},
fields: [...new Set(["id", ...fields])],
})
const argument = {
entity: schemaEntityObjectRepresentation.entity,
@@ -353,6 +227,91 @@ export class PostgresProvider {
}
}
@InjectManager("baseRepository_")
async query<const TEntry extends string>(
config: IndexTypes.IndexQueryConfig<TEntry>,
@MedusaContext() sharedContext: Context = {}
): Promise<IndexTypes.QueryResultSet<TEntry>> {
await this.#isReady_
const {
keepFilteredEntities,
fields = [],
filters = {},
joinFilters = {},
} = config
const { take, skip, order: inputOrderBy = {} } = config.pagination ?? {}
const select = normalizeFieldsSelection(fields)
const where = flattenObjectKeys(filters)
const joinWhere = flattenObjectKeys(joinFilters)
const orderBy = flattenObjectKeys(inputOrderBy)
const { manager } = sharedContext as { manager: SqlEntityManager }
let hasPagination = false
if (isDefined(skip)) {
hasPagination = true
}
const connection = manager.getConnection()
const qb = new QueryBuilder({
schema: this.schemaObjectRepresentation_,
entityMap: this.schemaEntitiesMap_,
knex: connection.getKnex(),
selector: {
select,
where,
joinWhere,
},
options: {
skip,
take,
keepFilteredEntities,
orderBy,
},
})
const sql = qb.buildQuery(hasPagination, !!keepFilteredEntities)
let resultSet = await manager.execute(sql)
const count = hasPagination ? +(resultSet[0]?.count ?? 0) : undefined
if (keepFilteredEntities) {
const mainEntity = Object.keys(select)[0]
const ids = resultSet.map((r) => r[`${mainEntity}.id`])
if (ids.length) {
return await this.query<TEntry>(
{
fields,
joinFilters,
filters: {
[mainEntity]: {
id: ids,
},
},
pagination: undefined,
keepFilteredEntities: false,
} as IndexTypes.IndexQueryConfig<TEntry>,
sharedContext
)
}
}
return {
data: qb.buildObjectFromResultset(
resultSet
) as IndexTypes.QueryResultSet<TEntry>["data"],
metadata: hasPagination
? {
count: count!,
skip,
take,
}
: undefined,
}
}
/**
* Create the index entry and the index relation entry when this event is emitted.
* @param entity
@@ -372,7 +331,7 @@ export class PostgresProvider {
}: {
entity: string
data: TData | TData[]
schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation
schemaEntityObjectRepresentation: IndexTypes.SchemaObjectEntityRepresentation
},
@MedusaContext() sharedContext: Context = {}
) {
@@ -466,7 +425,7 @@ export class PostgresProvider {
}: {
entity: string
data: TData | TData[]
schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation
schemaEntityObjectRepresentation: IndexTypes.SchemaObjectEntityRepresentation
},
@MedusaContext() sharedContext: Context = {}
) {
@@ -513,7 +472,7 @@ export class PostgresProvider {
}: {
entity: string
data: TData | TData[]
schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation
schemaEntityObjectRepresentation: IndexTypes.SchemaObjectEntityRepresentation
},
@MedusaContext() sharedContext: Context = {}
) {
@@ -567,7 +526,7 @@ export class PostgresProvider {
}: {
entity: string
data: TData | TData[]
schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation
schemaEntityObjectRepresentation: IndexTypes.SchemaObjectEntityRepresentation
},
@MedusaContext() sharedContext: Context = {}
) {
@@ -595,7 +554,7 @@ export class PostgresProvider {
const parentEntityName = (
this.schemaObjectRepresentation_._serviceNameModuleConfigMap[
parentServiceName
] as EntityNameModuleConfigMap[0]
] as IndexTypes.EntityNameModuleConfigMap[0]
).linkableKeys?.[parentPropertyId]
if (!parentEntityName) {
@@ -617,7 +576,7 @@ export class PostgresProvider {
const childEntityName = (
this.schemaObjectRepresentation_._serviceNameModuleConfigMap[
childServiceName
] as EntityNameModuleConfigMap[0]
] as IndexTypes.EntityNameModuleConfigMap[0]
).linkableKeys?.[childPropertyId]
if (!childEntityName) {
@@ -688,7 +647,7 @@ export class PostgresProvider {
}: {
entity: string
data: TData | TData[]
schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation
schemaEntityObjectRepresentation: IndexTypes.SchemaObjectEntityRepresentation
},
@MedusaContext() sharedContext: Context = {}
) {

View File

@@ -1,133 +1,8 @@
import {
ModuleJoinerConfig,
ModulesSdkTypes,
Subscriber,
} from "@medusajs/types"
/**
* Represents the module options that can be provided
*/
export interface IndexModuleOptions {
customAdapter?: {
constructor: new (...args: any[]) => any
options: any
}
defaultAdapterOptions?: ModulesSdkTypes.ModuleServiceInitializeOptions
schema: string
}
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 SchemaPropertiesMap = {
[key: string]: {
shortCutOf?: string
ref: SchemaObjectEntityRepresentation
}
}
export type EntityNameModuleConfigMap = {
[key: string]: ModuleJoinerConfig
}
/**
* Represents the schema objects representation once the schema has been processed
*/
export type SchemaObjectRepresentation =
| {
[key: string]: SchemaObjectEntityRepresentation
}
| {
_schemaPropertiesMap: SchemaPropertiesMap
_serviceNameModuleConfigMap: EntityNameModuleConfigMap
}
export const schemaObjectRepresentationPropertiesToOmit = [
"_schemaPropertiesMap",
"_serviceNameModuleConfigMap",
]
/**
* Represents the storage provider interface,
*/
export interface StorageProvider {
new (
container: { [key: string]: any },
storageProviderOptions: unknown & {
schemaObjectRepresentation: SchemaObjectRepresentation
},
moduleOptions: IndexModuleOptions
): StorageProvider
onApplicationStart?(): Promise<void>
query(...args): unknown
queryAndCount(...args): unknown
consumeEvent(
schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation
): Subscriber
}
export type Select = {
[key: string]: boolean | Select | Select[]
}
@@ -152,18 +27,3 @@ export type QueryOptions = {
orderBy?: OrderBy | OrderBy[]
keepFilteredEntities?: boolean
}
// Preventing infinite depth
type ResultSetLimit = [never | 0 | 1 | 2]
export type Resultset<Select, Prev extends number = 3> = Prev extends never
? never
: {
[key in keyof Select]: Select[key] extends boolean
? string
: Select[key] extends Select[]
? Resultset<Select[key][0], ResultSetLimit[Prev]>[]
: Select[key] extends {}
? Resultset<Select[key], ResultSetLimit[Prev]>
: unknown
}

View File

@@ -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,
})
})
})

View File

@@ -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,
},
},
})
})
})

View File

@@ -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) {

View File

@@ -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")

View File

@@ -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]

View File

@@ -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
}

View File

@@ -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 }[]
}

View File

@@ -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
}

View File

@@ -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) => {

View File

@@ -7,5 +7,5 @@
"src/**/__mocks__",
"src/**/__fixtures__",
"node_modules"
],
]
}

View File

@@ -27,9 +27,9 @@
"@types": ["./src/types"]
}
},
"include": ["src", "integration-tests"],
"include": ["src"],
"exclude": [
"dist",
"node_modules"
]
],
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": ["src", "integration-tests"],
"exclude": ["node_modules", "dist"],
"compilerOptions": {
"sourceMap": true
}
}