feat(index): add filterable fields to link definition (#11898)

* feat(index): add filterable fields to link definition

* rm comment

* break recursion

* validate read only links

* validate filterable

* gql schema array

* link parents

* isInverse

* push id when not present

* Fix ciruclar relationships and add tests to ensure proper behaviour (part 1)

* log and fallback to entity.alias

* cleanup and fixes

* cleanup and fixes

* cleanup and fixes

* fix get attributes

* gql type

* unit test

* array inference

* rm only

* package.json

* pacvkage.json

* fix link retrieval on duplicated entity type and aliases + tests

* link parents as array

* Match only parent entity

* rm comment

* remove hard coded schema

* extend types

* unit test

* test

* types

* pagination type

* type

* fix integration tests

* Improve performance of in selection

* use @@ to filter property

* escape jsonPath

* add Event Bus by default

* changeset

* rm postgres analyze

* estimate count

* new query

* parent aliases

* inner query w/ filter and sort relations

* address comments

---------

Co-authored-by: adrien2p <adrien.deperetti@gmail.com>
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Carlos R. L. Rodrigues
2025-04-29 12:10:31 +02:00
committed by GitHub
co-authored by adrien2p Oli Juhl
parent 8a3f639f01
commit b868a4ef4d
37 changed files with 3285 additions and 755 deletions
@@ -26,6 +26,7 @@ import {
} from "@medusajs/utils"
import { pgConnectionLoader } from "./database"
import type { Knex } from "@mikro-orm/knex"
import { aliasTo, asValue } from "awilix"
import { configManager } from "./config"
import {
@@ -33,7 +34,6 @@ import {
container as mainContainer,
MedusaContainer,
} from "./container"
import type { Knex } from "@mikro-orm/knex"
export class MedusaAppLoader {
/**
@@ -88,6 +88,7 @@ export class MedusaAppLoader {
const def = {} as ModuleDefinition
def.key ??= key
def.label ??= ModulesDefinition[key]?.label ?? upperCaseFirst(key)
def.dependencies ??= ModulesDefinition[key]?.dependencies
def.isQueryable = ModulesDefinition[key]?.isQueryable ?? true
const orignalDef = value?.definition ?? ModulesDefinition[key]
@@ -22,6 +22,7 @@ import {
isString,
MedusaModuleProviderType,
MedusaModuleType,
Modules,
ModulesSdkUtils,
toMikroOrmEntities,
} from "@medusajs/utils"
@@ -223,7 +224,8 @@ export async function loadInternalModule(args: {
ContainerRegistrationKeys.MANAGER,
ContainerRegistrationKeys.CONFIG_MODULE,
ContainerRegistrationKeys.LOGGER,
ContainerRegistrationKeys.PG_CONNECTION
ContainerRegistrationKeys.PG_CONNECTION,
Modules.EVENT_BUS
)
for (const dependency of dependencies) {
+4
View File
@@ -456,3 +456,7 @@ export type TransformObjectMethodToAsync<T extends object> = {
? TransformObjectMethodToAsync<T[K]>
: T[K]
}
export type QueryContextType = Record<string, any> & {
__type?: "QueryContext"
}
@@ -38,6 +38,10 @@ export type PaginatedResponse<T> = {
* The total number of items.
*/
count: number
/**
* The estimated number of items.
*/
estimate_count?: number
} & T
export type BatchResponse<T> = {
@@ -59,7 +63,7 @@ export type BatchResponse<T> = {
ids: string[]
/**
* The type of the items that were deleted.
*
*
* @example
* "product"
*/
@@ -9,6 +9,7 @@ describe("IndexQueryConfig", () => {
expectTypeOf<IndexConfig["fields"]>().toEqualTypeOf<
(
| "*"
| "id"
| "title"
| "variants.*"
@@ -31,10 +31,21 @@ export type SchemaObjectEntityRepresentation = {
*/
targetProp: string
/**
* The property the parent is assigned to in my side
*/
inverseSideProp: string
/**
* Are the data expected to be a list or not
*/
isList?: boolean
/**
* Whether the entity is the inverse of the link (not the owner.):
* e.g: order -> cart, order is the owner, cart is the inverse
*/
isInverse?: boolean
}[]
/**
@@ -69,6 +80,8 @@ export type SchemaPropertiesMap = {
[key: string]: {
shortCutOf?: string
ref: SchemaObjectEntityRepresentation
isInverse?: boolean
isList?: boolean
}
}
@@ -1,5 +1,4 @@
import { ExcludedProps, TypeOnly } from "./common"
type Marker = [never, 0, 1, 2, 3, 4]
type RawBigNumberPrefix = "raw_"
@@ -17,7 +16,7 @@ export type ObjectToIndexFields<
MaybeT,
Depth extends number = 2,
Exclusion extends string[] = [],
T = TypeOnly<MaybeT>
T = TypeOnly<MaybeT> & { "*": "*" }
> = Depth extends never
? never
: T extends object
@@ -1,7 +1,56 @@
import { RemoteQueryInput } from "../../modules-sdk/remote-query-object-from-string"
import { QueryContextType } from "../../common"
import { IndexServiceEntryPoints } from "../index-service-entry-points"
import { ObjectToIndexFields } from "./query-input-config-fields"
import { IndexFilters } from "./query-input-config-filters"
import { IndexOrderBy } from "./query-input-config-order-by"
export type IndexQueryInput<TEntry extends string> = {
/**
* The name of the entity to retrieve. For example, `product`.
*/
entity: TEntry | keyof IndexServiceEntryPoints
/**
* The fields and relations to retrieve in the entity.
*/
fields: ObjectToIndexFields<
IndexServiceEntryPoints[TEntry & keyof IndexServiceEntryPoints]
> extends never
? string[]
:
| ObjectToIndexFields<
IndexServiceEntryPoints[TEntry & keyof IndexServiceEntryPoints]
>[]
| string[]
/**
* Pagination configurations for the returned list of items.
*/
pagination?: {
/**
* The number of items to skip before retrieving the returned items.
*/
skip?: number
/**
* The maximum number of items to return.
*/
take?: number
/**
* Sort by field names in ascending or descending order.
*/
order?: IndexOrderBy<TEntry>
}
/**
* Filters to apply on the retrieved items.
*/
filters?: IndexFilters<TEntry>
/**
* Apply a query context on the retrieved data. For example, to retrieve product prices for a certain context.
*/
context?: QueryContextType
/**
* Apply a `withDeleted` flag on the retrieved data to retrieve soft deleted items.
*/
withDeleted?: boolean
}
export type IndexQueryConfig<TEntry extends string> = {
fields: ObjectToIndexFields<
@@ -13,14 +62,14 @@ export type IndexQueryConfig<TEntry extends string> = {
>[]
filters?: IndexFilters<TEntry>
joinFilters?: IndexFilters<TEntry>
pagination?: Partial<RemoteQueryInput<TEntry>["pagination"]>
pagination?: Partial<IndexQueryInput<TEntry>["pagination"]>
keepFilteredEntities?: boolean
}
export type QueryFunctionReturnPagination = {
skip?: number
take?: number
count?: number
skip: number
take: number
estimate_count: number
}
/**
+1
View File
@@ -21,6 +21,7 @@ export type JoinerRelationship = {
export interface JoinerServiceConfigAlias {
name: string | string[]
entity?: string
filterable?: string[]
/**
* Extra arguments to pass to the remoteFetchData callback
*/
@@ -248,6 +248,11 @@ export declare type ModuleJoinerRelationship = JoinerRelationship & {
* If true, the link joiner will cascade deleting the relationship
*/
deleteCascade?: boolean
/**
* The fields to be filterable by the Index module using query.index
*/
filterable?: string[]
/**
* Allow multiple relationships to exist for this
* entity
@@ -1,10 +1,10 @@
import { QueryContextType } from "../common"
import { IndexOrderBy } from "../index-data/query-config/query-input-config-order-by"
import { ObjectToRemoteQueryFields } from "./object-to-remote-query-fields"
import { RemoteQueryEntryPoints } from "./remote-query-entry-points"
import { RemoteQueryFilters } from "./to-remote-query"
export type RemoteQueryObjectConfig<TEntry extends string> = {
// service: string This property is still supported under the hood but part of the type due to types missmatch towards fields
entryPoint: TEntry | keyof RemoteQueryEntryPoints
variables?: any
fields: ObjectToRemoteQueryFields<
@@ -26,7 +26,6 @@ export type RemoteQueryObjectFromStringResult<
}
export type RemoteQueryInput<TEntry extends string> = {
// service: string This property is still supported under the hood but part of the type due to types missmatch towards fields
/**
* The name of the entity to retrieve. For example, `product`.
*/
@@ -67,7 +66,7 @@ export type RemoteQueryInput<TEntry extends string> = {
/**
* Apply a query context on the retrieved data. For example, to retrieve product prices for a certain context.
*/
context?: any
context?: QueryContextType
/**
* Apply a `withDeleted` flag on the retrieved data to retrieve soft deleted items.
*/
@@ -1,4 +1,8 @@
import { Prettify } from "../common"
import {
IndexQueryInput,
QueryResultSet,
} from "../index-data/query-config/query-input-config"
import { RemoteJoinerOptions, RemoteJoinerQuery } from "../joiner"
import { RemoteQueryEntryPoints } from "./remote-query-entry-points"
import {
@@ -6,7 +10,6 @@ import {
RemoteQueryObjectConfig,
RemoteQueryObjectFromStringResult,
} from "./remote-query-object-from-string"
import { RemoteQueryFilters } from "./to-remote-query"
/*type ExcludedProps = "__typename"*/
@@ -40,17 +43,14 @@ export type QueryGraphFunction = {
}
/**
* QueryIndexFunction is a wrapper on top of remoteQuery
* QueryIndexFunction is a wrapper on top of indexModule
* that simplifies the input it accepts and returns
* a normalized/consistent output.
*/
export type QueryIndexFunction = {
<const TEntry extends string>(
queryOptions: RemoteQueryInput<TEntry> & {
joinFilters?: RemoteQueryFilters<TEntry>
},
options?: RemoteJoinerOptions
): Promise<Prettify<GraphResultSet<TEntry>>>
<const TEntry extends string>(queryOptions: IndexQueryInput<TEntry>): Promise<
Prettify<QueryResultSet<TEntry>>
>
}
/*export type RemoteQueryReturnedData<TEntry extends string> =
@@ -69,7 +69,7 @@ describe("GraphQL builder", () => {
id: ID!
username: String!
email: Email!
spend_limit: String!
spend_limit: Float!
phones: [String]!
group_id:String!
group: Group!
@@ -9,7 +9,7 @@ const GRAPHQL_TYPES = {
boolean: "Boolean",
dateTime: "DateTime",
number: "Int",
bigNumber: "String",
bigNumber: "Float",
text: "String",
json: "JSON",
array: "[String]",
@@ -18,6 +18,7 @@ type InputSource = {
alias?: string
linkable: string
primaryKey: string
filterable?: string[]
}
type ReadOnlyInputSource = {
@@ -42,6 +43,7 @@ type InputOptions = {
field?: string
isList?: boolean
deleteCascade?: boolean
filterable?: string[]
}
type Shortcut = {
@@ -87,6 +89,7 @@ type ModuleLinkableKeyConfig = {
alias: string
hasMany?: boolean
shortcut?: Shortcut | Shortcut[]
filterable?: string[]
}
function isInputOptions(input: any): input is InputOptions {
@@ -141,6 +144,7 @@ function prepareServiceConfig(
isList: false,
hasMany: false,
deleteCascade: false,
filterable: source.filterable,
module: source.serviceName,
entity: source.entity,
}
@@ -159,6 +163,7 @@ function prepareServiceConfig(
isList: input.isList ?? false,
hasMany,
deleteCascade: input.deleteCascade ?? false,
filterable: input.filterable,
module: source.serviceName,
entity: source.entity,
}
@@ -192,6 +197,17 @@ export function defineLink(
const serviceBObj = prepareServiceConfig(rightService)
if (linkServiceOptions?.readOnly) {
if (!leftService.linkable || !leftService.field) {
throw new Error(
`ReadOnly link requires "linkable" and "field" to be defined for the left service.`
)
} else if (
(leftService as DefineLinkInputSource).filterable ||
(rightService as DefineLinkInputSource).filterable
) {
throw new Error(`ReadOnly link does not support filterable fields.`)
}
return defineReadOnlyLink(
serviceAObj,
serviceBObj,
@@ -378,6 +394,7 @@ ${serviceBObj.module}: {
methodSuffix: serviceAMethodSuffix,
},
deleteCascade: serviceAObj.deleteCascade,
filterable: serviceAObj.filterable,
hasMany: serviceAObj.hasMany,
},
{
@@ -390,6 +407,7 @@ ${serviceBObj.module}: {
methodSuffix: serviceBMethodSuffix,
},
deleteCascade: serviceBObj.deleteCascade,
filterable: serviceBObj.filterable,
hasMany: serviceBObj.hasMany,
},
],
@@ -1,11 +1,13 @@
type QueryContextType = {
import { QueryContextType } from "@medusajs/types"
type QueryContexFnType = {
(query: Record<string, unknown>): Record<string, unknown>
isQueryContext: (obj: any) => boolean
}
const __type = "QueryContext"
function QueryContextFn(query: Record<string, unknown>) {
function QueryContextFn(query: Record<string, unknown>): QueryContextType {
return {
...query,
__type,
@@ -16,4 +18,4 @@ QueryContextFn.isQueryContext = (obj: any) => {
return obj.__type === __type
}
export const QueryContext: QueryContextType = QueryContextFn
export const QueryContext: QueryContexFnType = QueryContextFn