diff --git a/integration-tests/modules/.gitignore b/integration-tests/modules/.gitignore index a2c424c876..6393d4a090 100644 --- a/integration-tests/modules/.gitignore +++ b/integration-tests/modules/.gitignore @@ -1,4 +1,4 @@ dist/ node_modules *yarn-error.log - +.medusa diff --git a/integration-tests/modules/__tests__/index/search.spec.ts b/integration-tests/modules/__tests__/index/search.spec.ts index 44917c906c..1ce15aaa12 100644 --- a/integration-tests/modules/__tests__/index/search.spec.ts +++ b/integration-tests/modules/__tests__/index/search.spec.ts @@ -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) }) }) }, diff --git a/integration-tests/modules/tsconfig.json b/integration-tests/modules/tsconfig.json index 13e3b45522..c17aadfdd5 100644 --- a/integration-tests/modules/tsconfig.json +++ b/integration-tests/modules/tsconfig.json @@ -19,7 +19,7 @@ "skipLibCheck": true, "downlevelIteration": true // to use ES5 specific tooling }, - "include": ["src"], + "include": ["src", "./medusa/**/*"], "exclude": [ "./dist/**/*", "__tests__", diff --git a/packages/core/modules-sdk/src/definitions.ts b/packages/core/modules-sdk/src/definitions.ts index 317804444e..468d071c9f 100644 --- a/packages/core/modules-sdk/src/definitions.ts +++ b/packages/core/modules-sdk/src/definitions.ts @@ -323,6 +323,7 @@ export const ModulesDefinition: { Modules.EVENT_BUS, "logger", ContainerRegistrationKeys.REMOTE_QUERY, + ContainerRegistrationKeys.QUERY, ], defaultModuleDeclaration: { scope: MODULE_SCOPE.INTERNAL, diff --git a/packages/core/modules-sdk/src/utils/gql-schema-to-types.ts b/packages/core/modules-sdk/src/utils/gql-schema-to-types.ts index d1a464bb2e..675a4c8dbb 100644 --- a/packages/core/modules-sdk/src/utils/gql-schema-to-types.ts +++ b/packages/core/modules-sdk/src/utils/gql-schema-to-types.ts @@ -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[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 }) } diff --git a/packages/core/types/src/index/__fixtures__/index-service-entry-points.ts b/packages/core/types/src/index/__fixtures__/index-service-entry-points.ts new file mode 100644 index 0000000000..42aec29043 --- /dev/null +++ b/packages/core/types/src/index/__fixtures__/index-service-entry-points.ts @@ -0,0 +1,63 @@ +export type Maybe = T | null +export type InputMaybe = Maybe +export type Exact = { + [K in keyof T]: T[K] +} +export type MakeOptional = Omit & { + [SubKey in K]?: Maybe +} +export type MakeMaybe = Omit & { + [SubKey in K]: Maybe +} +export type MakeEmpty< + T extends { [key: string]: unknown }, + K extends keyof T +> = { [_ in K]?: never } +export type Incremental = + | 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 + title?: Maybe + variants?: Maybe>> +} + +export type ProductVariant = { + __typename?: "ProductVariant" + id?: Maybe + product_id?: Maybe + sku?: Maybe + prices?: Maybe>> +} + +export type Price = { + __typename?: "Price" + amount?: Maybe +} + +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 {} +} diff --git a/packages/core/types/src/index/__tests__/index.spec.ts b/packages/core/types/src/index/__tests__/index.spec.ts new file mode 100644 index 0000000000..3e2b387e12 --- /dev/null +++ b/packages/core/types/src/index/__tests__/index.spec.ts @@ -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().toEqualTypeOf< + ( + | "id" + | "title" + | "variants.*" + | "variants.id" + | "variants.product_id" + | "variants.sku" + | "variants.prices.*" + | "variants.prices.amount" + )[] + >() + + expectTypeOf().toEqualTypeOf< + | { + id?: string | string[] | OperatorMap + title?: string | string[] | OperatorMap + variants?: { + id?: string | string[] | OperatorMap + product_id?: string | string[] | OperatorMap + sku?: string | string[] | OperatorMap + prices?: { + amount?: number | number[] | OperatorMap + } + } + } + | undefined + >() + + expectTypeOf().toEqualTypeOf< + | { + skip?: number + take?: number + order?: { + id?: OrderBy + title?: OrderBy + variants?: { + id?: OrderBy + product_id?: OrderBy + sku?: OrderBy + prices?: { + amount?: OrderBy + } + } + } + } + | undefined + >() + }) +}) diff --git a/packages/core/types/src/index/common.ts b/packages/core/types/src/index/common.ts new file mode 100644 index 0000000000..ff91295619 --- /dev/null +++ b/packages/core/types/src/index/common.ts @@ -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 + } diff --git a/packages/core/types/src/index/index-service-entry-points.ts b/packages/core/types/src/index/index-service-entry-points.ts new file mode 100644 index 0000000000..fb524e53f2 --- /dev/null +++ b/packages/core/types/src/index/index-service-entry-points.ts @@ -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 {} diff --git a/packages/core/types/src/index/index.ts b/packages/core/types/src/index/index.ts index 9376fea807..bfd5605b1c 100644 --- a/packages/core/types/src/index/index.ts +++ b/packages/core/types/src/index/index.ts @@ -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" diff --git a/packages/core/types/src/index/operator-map.ts b/packages/core/types/src/index/operator-map.ts new file mode 100644 index 0000000000..d1e0ddb8bf --- /dev/null +++ b/packages/core/types/src/index/operator-map.ts @@ -0,0 +1,12 @@ +export type OperatorMap = { + $eq: T + $lt: T + $lte: T + $gt: T + $gte: T + $ne: T + $in: T + $is: T + $like: T + $ilike: T +} diff --git a/packages/core/types/src/index/query-config/common.ts b/packages/core/types/src/index/query-config/common.ts new file mode 100644 index 0000000000..f357af3144 --- /dev/null +++ b/packages/core/types/src/index/query-config/common.ts @@ -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 = Prettify, ExcludedProps>> +export type OmitNever = { + [K in keyof T as TypeOnly extends never ? never : K]: T[K] +} +export type TypeOnly = Required> diff --git a/packages/core/types/src/index/query-config/index.ts b/packages/core/types/src/index/query-config/index.ts new file mode 100644 index 0000000000..f0add4110b --- /dev/null +++ b/packages/core/types/src/index/query-config/index.ts @@ -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" diff --git a/packages/core/types/src/index/query-config/query-input-config-fields.ts b/packages/core/types/src/index/query-config/query-input-config-fields.ts new file mode 100644 index 0000000000..5def2e1e49 --- /dev/null +++ b/packages/core/types/src/index/query-config/query-input-config-fields.ts @@ -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 + +/** + * 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 +> = Depth extends never + ? never + : T extends object + ? { + [K in keyof T]: K extends // handle big number + `${RawBigNumberPrefix}${string}` + ? Exclude + : // Special props that should be excluded + K extends ExcludedProps + ? never + : // Prevent recursive reference to itself + K extends Exclusion[number] + ? never + : TypeOnly extends Array + ? TypeOnly extends Date + ? Exclude + : TypeOnly extends { __typename: any } + ? `${Exclude}.${ExpandStarSelector< + TypeOnly, + Marker[Depth], + [K & string, ...Exclusion] + >}` + : TypeOnly extends object + ? Exclude + : never + : TypeOnly extends Date + ? Exclude + : TypeOnly extends { __typename: any } + ? `${Exclude}.${ExpandStarSelector< + TypeOnly, + Marker[Depth], + [K & string, ...Exclusion] + >}` + : T[K] extends object + ? Exclude + : Exclude + }[keyof T] + : never diff --git a/packages/core/types/src/index/query-config/query-input-config-filters.ts b/packages/core/types/src/index/query-config/query-input-config-filters.ts new file mode 100644 index 0000000000..d26a49a0f1 --- /dev/null +++ b/packages/core/types/src/index/query-config/query-input-config-filters.ts @@ -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 +> = { + [Key in keyof T]?: Key extends Exclusion[number] + ? never + : Key extends ExcludedProps + ? never + : TypeOnly extends string | number | boolean | Date + ? TypeOnly | TypeOnly[] | OperatorMap> + : TypeOnly extends Array + ? TypeOnly extends { __typename: any } + ? IndexFilters + : TypeOnly extends object + ? CleanupObject> + : never + : TypeOnly extends { __typename: any } + ? IndexFilters< + Key & string, + T[Key], + [Key & string, ...Exclusion], + Depth[Lim] + > + : TypeOnly extends object + ? CleanupObject> + : 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 extends Array + ? Prettify< + OmitNever> + > + : Prettify< + OmitNever< + ExtractFiltersOperators< + IndexEntryPointsLevel[TEntry], + Lim, + [TEntry, ...Exclusion] + > + > + > + : Record + : never diff --git a/packages/core/types/src/index/query-config/query-input-config-order-by.ts b/packages/core/types/src/index/query-config/query-input-config-order-by.ts new file mode 100644 index 0000000000..d106b7b49d --- /dev/null +++ b/packages/core/types/src/index/query-config/query-input-config-order-by.ts @@ -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 +> = { + [Key in keyof T]?: Key extends Exclusion[number] + ? never + : Key extends ExcludedProps + ? never + : TypeOnly extends string | number | boolean | Date + ? OrderBy + : TypeOnly extends Array + ? TypeOnly extends { __typename: any } + ? IndexOrderBy + : TypeOnly extends object + ? CleanupObject> + : never + : TypeOnly extends { __typename: any } + ? IndexOrderBy< + Key & string, + T[Key], + [Key & string, ...Exclusion], + Depth[Lim] + > + : TypeOnly extends object + ? CleanupObject> + : 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 extends Array + ? Prettify< + OmitNever> + > + : Prettify< + OmitNever< + ExtractOrderByOperators< + IndexEntryPointsLevel[TEntry], + Lim, + [TEntry, ...Exclusion] + > + > + > + : Record + : never diff --git a/packages/core/types/src/index/query-config/query-input-config.ts b/packages/core/types/src/index/query-config/query-input-config.ts new file mode 100644 index 0000000000..189ef338fc --- /dev/null +++ b/packages/core/types/src/index/query-config/query-input-config.ts @@ -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 = { + fields: ObjectToIndexFields< + IndexServiceEntryPoints[TEntry & keyof IndexServiceEntryPoints] + > extends never + ? string[] + : ObjectToIndexFields< + IndexServiceEntryPoints[TEntry & keyof IndexServiceEntryPoints] + >[] + filters?: IndexFilters + joinFilters?: IndexFilters + pagination?: { + skip?: number + take?: number + order?: IndexOrderBy + } + 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 = { + data: TEntry extends keyof IndexServiceEntryPoints + ? IndexServiceEntryPoints[TEntry][] + : any[] + metadata?: QueryFunctionReturnPagination +} diff --git a/packages/core/types/src/index/service.ts b/packages/core/types/src/index/service.ts index 0d96d37daf..dc505806ed 100644 --- a/packages/core/types/src/index/service.ts +++ b/packages/core/types/src/index/service.ts @@ -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 - queryAndCount(...args): Promise + query( + config: IndexQueryConfig + ): Promise> } diff --git a/packages/core/types/src/index/sotrage-provider.ts b/packages/core/types/src/index/sotrage-provider.ts new file mode 100644 index 0000000000..7c54f45220 --- /dev/null +++ b/packages/core/types/src/index/sotrage-provider.ts @@ -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, + options: { + schemaObjectRepresentation: SchemaObjectRepresentation + entityMap: Record + }, + moduleOptions: IndexModuleOptions + )*/ + + onApplicationStart?(): Promise + + query( + config: IndexQueryConfig + ): Promise> + + consumeEvent( + schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation + ): Subscriber +} diff --git a/packages/framework/framework/package.json b/packages/framework/framework/package.json index fa3288ea5e..de83acc01e 100644 --- a/packages/framework/framework/package.json +++ b/packages/framework/framework/package.json @@ -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" }, diff --git a/packages/medusa/src/commands/start.ts b/packages/medusa/src/commands/start.ts index bfdbdf312f..014bdcde67 100644 --- a/packages/medusa/src/commands/start.ts +++ b/packages/medusa/src/commands/start.ts @@ -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") diff --git a/packages/modules/index/.gitignore b/packages/modules/index/.gitignore index 874c6c69d3..dbbe8014f1 100644 --- a/packages/modules/index/.gitignore +++ b/packages/modules/index/.gitignore @@ -4,3 +4,4 @@ node_modules .env* .env *.sql +.medusa \ No newline at end of file diff --git a/packages/modules/index/integration-tests/__tests__/index-engine-module.spec.ts b/packages/modules/index/integration-tests/__tests__/index-engine-module.spec.ts index c240f6a3b8..1365303501 100644 --- a/packages/modules/index/integration-tests/__tests__/index-engine-module.spec.ts +++ b/packages/modules/index/integration-tests/__tests__/index-engine-module.spec.ts @@ -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, {}) diff --git a/packages/modules/index/integration-tests/__tests__/query-builder.spec.ts b/packages/modules/index/integration-tests/__tests__/query-builder.spec.ts index e6c5fd164c..807456f1c5 100644 --- a/packages/modules/index/integration-tests/__tests__/query-builder.spec.ts +++ b/packages/modules/index/integration-tests/__tests__/query-builder.spec.ts @@ -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", diff --git a/packages/modules/index/package.json b/packages/modules/index/package.json index 929c79a2a9..5d43ed7965 100644 --- a/packages/modules/index/package.json +++ b/packages/modules/index/package.json @@ -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", diff --git a/packages/modules/index/src/services/index-module-service.ts b/packages/modules/index/src/services/index-module-service.ts index e88b8558b9..61124e0970 100644 --- a/packages/modules/index/src/services/index-module-service.ts +++ b/packages/modules/index/src/services/index-module-service.ts @@ -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 + [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 - protected readonly storageProviderCtr_: StorageProvider + protected readonly storageProviderCtr_: Constructor 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( + config: IndexTypes.IndexQueryConfig + ): Promise> { + 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 diff --git a/packages/modules/index/src/services/postgres-provider.ts b/packages/modules/index/src/services/postgres-provider.ts index d93e0e11ba..6394b1369e 100644 --- a/packages/modules/index/src/services/postgres-provider.ts +++ b/packages/modules/index/src/services/postgres-provider.ts @@ -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 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 - 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 }, - 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[], 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( + config: IndexTypes.IndexQueryConfig, + @MedusaContext() sharedContext: Context = {} + ): Promise> { + 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( + { + fields, + joinFilters, + filters: { + [mainEntity]: { + id: ids, + }, + }, + pagination: undefined, + keepFilteredEntities: false, + } as IndexTypes.IndexQueryConfig, + sharedContext + ) + } + } + + return { + data: qb.buildObjectFromResultset( + resultSet + ) as IndexTypes.QueryResultSet["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 = {} ) { diff --git a/packages/modules/index/src/types/index.ts b/packages/modules/index/src/types/index.ts index 2b5f38cc8c..cd1e1c3f2a 100644 --- a/packages/modules/index/src/types/index.ts +++ b/packages/modules/index/src/types/index.ts @@ -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 - - 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 = Prev extends never - ? never - : { - [key in keyof Select]: Select[key] extends boolean - ? string - : Select[key] extends Select[] - ? Resultset[] - : Select[key] extends {} - ? Resultset - : unknown - } diff --git a/packages/modules/index/src/utils/__tests__/flatten-object-keys.spec.ts b/packages/modules/index/src/utils/__tests__/flatten-object-keys.spec.ts new file mode 100644 index 0000000000..1596e4ef21 --- /dev/null +++ b/packages/modules/index/src/utils/__tests__/flatten-object-keys.spec.ts @@ -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, + }) + }) +}) diff --git a/packages/modules/index/src/utils/__tests__/normalize-fields-selcetion.spec.ts b/packages/modules/index/src/utils/__tests__/normalize-fields-selcetion.spec.ts new file mode 100644 index 0000000000..730465230a --- /dev/null +++ b/packages/modules/index/src/utils/__tests__/normalize-fields-selcetion.spec.ts @@ -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, + }, + }, + }) + }) +}) diff --git a/packages/modules/index/src/utils/build-config.ts b/packages/modules/index/src/utils/build-config.ts index f62f2546a9..369b7955d0 100644 --- a/packages/modules/index/src/utils/build-config.ts +++ b/packages/modules/index/src/utils/build-config.ts @@ -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] { +): [IndexTypes.SchemaObjectRepresentation, Record] { 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) { diff --git a/packages/modules/index/src/utils/create-partitions.ts b/packages/modules/index/src/utils/create-partitions.ts index 61b1a50907..faedd38e23 100644 --- a/packages/modules/index/src/utils/create-partitions.ts +++ b/packages/modules/index/src/utils/create-partitions.ts @@ -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 { const activeSchema = manager.config.get("schema") diff --git a/packages/modules/index/src/utils/default-schema.ts b/packages/modules/index/src/utils/default-schema.ts index 5602901a53..d788375ff3 100644 --- a/packages/modules/index/src/utils/default-schema.ts +++ b/packages/modules/index/src/utils/default-schema.ts @@ -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] diff --git a/packages/modules/index/src/utils/flatten-object-keys.ts b/packages/modules/index/src/utils/flatten-object-keys.ts new file mode 100644 index 0000000000..3f7387e750 --- /dev/null +++ b/packages/modules/index/src/utils/flatten-object-keys.ts @@ -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) { + const result: Record = {} + + function flatten(obj: Record, 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 +} diff --git a/packages/modules/index/src/utils/gql-to-types.ts b/packages/modules/index/src/utils/gql-to-types.ts new file mode 100644 index 0000000000..51f996d0cd --- /dev/null +++ b/packages/modules/index/src/utils/gql-to-types.ts @@ -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 }[] +} diff --git a/packages/modules/index/src/utils/normalize-fields-selection.ts b/packages/modules/index/src/utils/normalize-fields-selection.ts new file mode 100644 index 0000000000..0036b4dcd4 --- /dev/null +++ b/packages/modules/index/src/utils/normalize-fields-selection.ts @@ -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 +} diff --git a/packages/modules/index/src/utils/query-builder.ts b/packages/modules/index/src/utils/query-builder.ts index ad52bcedb5..e75c28eecb 100644 --- a/packages/modules/index/src/utils/query-builder.ts +++ b/packages/modules/index/src/utils/query-builder.ts @@ -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 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) => { diff --git a/packages/modules/index/tsconfig.build.json b/packages/modules/index/tsconfig.build.json index 2ce34105bc..0b60e58d9e 100644 --- a/packages/modules/index/tsconfig.build.json +++ b/packages/modules/index/tsconfig.build.json @@ -7,5 +7,5 @@ "src/**/__mocks__", "src/**/__fixtures__", "node_modules" - ], + ] } diff --git a/packages/modules/index/tsconfig.json b/packages/modules/index/tsconfig.json index d2d0a73270..3965587692 100644 --- a/packages/modules/index/tsconfig.json +++ b/packages/modules/index/tsconfig.json @@ -27,9 +27,9 @@ "@types": ["./src/types"] } }, - "include": ["src", "integration-tests"], + "include": ["src"], "exclude": [ "dist", "node_modules" - ] + ], } diff --git a/packages/modules/index/tsconfig.spec.json b/packages/modules/index/tsconfig.spec.json new file mode 100644 index 0000000000..48e47e8cbb --- /dev/null +++ b/packages/modules/index/tsconfig.spec.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "include": ["src", "integration-tests"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "sourceMap": true + } +}