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:
co-authored by
adrien2p
Oli Juhl
parent
8a3f639f01
commit
b868a4ef4d
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user