feat(index): Index module foundation (#9095)
**What** Index module foundation Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
1 parent
3cfcd075ae
commit
58167b5dfa
53 files changed
+4796
-1201
No files matched your search
@@ -32,6 +32,7 @@ packages/*
|
|||||||
!packages/workflow-engine-inmemory
|
!packages/workflow-engine-inmemory
|
||||||
!packages/fulfillment
|
!packages/fulfillment
|
||||||
!packages/fulfillment-manual
|
!packages/fulfillment-manual
|
||||||
|
!packages/index
|
||||||
|
|
||||||
!packages/framework
|
!packages/framework
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ module.exports = {
|
|||||||
"./packages/modules/auth/tsconfig.spec.json",
|
"./packages/modules/auth/tsconfig.spec.json",
|
||||||
"./packages/modules/cart/tsconfig.spec.json",
|
"./packages/modules/cart/tsconfig.spec.json",
|
||||||
"./packages/modules/currency/tsconfig.spec.json",
|
"./packages/modules/currency/tsconfig.spec.json",
|
||||||
|
"./packages/modules/index/tsconfig.spec.json",
|
||||||
"./packages/modules/customer/tsconfig.spec.json",
|
"./packages/modules/customer/tsconfig.spec.json",
|
||||||
"./packages/modules/file/tsconfig.spec.json",
|
"./packages/modules/file/tsconfig.spec.json",
|
||||||
"./packages/modules/inventory-next/tsconfig.spec.json",
|
"./packages/modules/inventory-next/tsconfig.spec.json",
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
import { IndexTypes } from "@medusajs/types"
|
||||||
|
import { defaultCurrencies, Modules } from "@medusajs/utils"
|
||||||
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
import { setTimeout } from "timers/promises"
|
||||||
|
import {
|
||||||
|
adminHeaders,
|
||||||
|
createAdminUser,
|
||||||
|
} from "../../../helpers/create-admin-user"
|
||||||
|
|
||||||
|
jest.setTimeout(120000)
|
||||||
|
|
||||||
|
process.env.ENABLE_INDEX_MODULE = "true"
|
||||||
|
|
||||||
|
medusaIntegrationTestRunner({
|
||||||
|
testSuite: ({ getContainer, dbConnection, api, dbConfig }) => {
|
||||||
|
let indexEngine: IndexTypes.IIndexService
|
||||||
|
let appContainer
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
appContainer = getContainer()
|
||||||
|
indexEngine = appContainer.resolve(Modules.INDEX)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
process.env.ENABLE_INDEX_MODULE = "false"
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createAdminUser(dbConnection, adminHeaders, appContainer)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Index engine", () => {
|
||||||
|
it("should search through the indexed data and return the correct results ordered and filtered [1]", async () => {
|
||||||
|
const payload = {
|
||||||
|
title: "Test Giftcard",
|
||||||
|
is_giftcard: true,
|
||||||
|
description: "test-giftcard-description",
|
||||||
|
options: [{ title: "Denominations", values: ["100"] }],
|
||||||
|
variants: new Array(10).fill(0).map((_, i) => ({
|
||||||
|
title: `Test variant ${i}`,
|
||||||
|
sku: `test-variant-${i}`,
|
||||||
|
prices: new Array(10).fill(0).map((_, j) => ({
|
||||||
|
currency_code: Object.values(defaultCurrencies)[j].code,
|
||||||
|
amount: 10 * j,
|
||||||
|
})),
|
||||||
|
options: {
|
||||||
|
Denominations: "100",
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
|
||||||
|
await api
|
||||||
|
.post("/admin/products", payload, adminHeaders)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Timeout to allow indexing to finish
|
||||||
|
await setTimeout(2000)
|
||||||
|
|
||||||
|
const [results, count] = await indexEngine.queryAndCount(
|
||||||
|
{
|
||||||
|
select: {
|
||||||
|
product: {
|
||||||
|
variants: {
|
||||||
|
prices: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
"product.variants.prices.amount": { $gt: 50 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orderBy: [{ "product.variants.prices.amount": "DESC" }],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(count).toBe(1)
|
||||||
|
|
||||||
|
const variants = results[0].variants
|
||||||
|
|
||||||
|
expect(variants.length).toBe(10)
|
||||||
|
|
||||||
|
for (const variant of variants) {
|
||||||
|
expect(variant.prices.length).toBe(4)
|
||||||
|
for (const price of variant.prices) {
|
||||||
|
expect(price.amount).toBeGreaterThan(50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should search through the indexed data and return the correct results ordered and filtered [2]", async () => {
|
||||||
|
const payload = {
|
||||||
|
title: "Test Giftcard",
|
||||||
|
is_giftcard: true,
|
||||||
|
description: "test-giftcard-description",
|
||||||
|
options: [{ title: "Denominations", values: ["100"] }],
|
||||||
|
variants: new Array(10).fill(0).map((_, i) => ({
|
||||||
|
title: `Test variant ${i}`,
|
||||||
|
sku: `test-variant-${i}`,
|
||||||
|
prices: new Array(10).fill(0).map((_, j) => ({
|
||||||
|
currency_code: Object.values(defaultCurrencies)[j].code,
|
||||||
|
amount: 10 * j,
|
||||||
|
})),
|
||||||
|
options: {
|
||||||
|
Denominations: "100",
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
|
||||||
|
await api
|
||||||
|
.post("/admin/products", payload, adminHeaders)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Timeout to allow indexing to finish
|
||||||
|
await setTimeout(2000)
|
||||||
|
|
||||||
|
const [results, count] = await indexEngine.queryAndCount(
|
||||||
|
{
|
||||||
|
select: {
|
||||||
|
product: {
|
||||||
|
variants: {
|
||||||
|
prices: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
"product.variants.prices.amount": { $gt: 50 },
|
||||||
|
"product.variants.prices.currency_code": { $eq: "AUD" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orderBy: [{ "product.variants.prices.amount": "DESC" }],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(count).toBe(1)
|
||||||
|
|
||||||
|
const variants = results[0].variants
|
||||||
|
|
||||||
|
expect(variants.length).toBe(10)
|
||||||
|
|
||||||
|
for (const variant of variants) {
|
||||||
|
expect(variant.prices.length).toBe(1)
|
||||||
|
expect(variant.prices[0].amount).toBeGreaterThan(50)
|
||||||
|
expect(variant.prices[0].currency_code).toBe("AUD")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it.skip("should search through the indexed data and return the correct results ordered and filtered [3]", async () => {
|
||||||
|
const payloads = new Array(50).fill(0).map((_, a) => ({
|
||||||
|
title: "Test Giftcard-" + a,
|
||||||
|
is_giftcard: true,
|
||||||
|
description: "test-giftcard-description" + a,
|
||||||
|
options: [{ title: "Denominations", values: ["100"] }],
|
||||||
|
variants: new Array(10).fill(0).map((_, i) => ({
|
||||||
|
title: `Test variant ${i}`,
|
||||||
|
sku: `test-variant-${i}${a}`,
|
||||||
|
prices: new Array(10).fill(0).map((_, j) => ({
|
||||||
|
currency_code: Object.values(defaultCurrencies)[j].code,
|
||||||
|
amount: 10 * j,
|
||||||
|
})),
|
||||||
|
options: {
|
||||||
|
Denominations: "100",
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
|
||||||
|
let i = 0
|
||||||
|
for (const payload of payloads) {
|
||||||
|
++i
|
||||||
|
await api.post("/admin/products", payload, adminHeaders).then(() => {
|
||||||
|
console.log(`Created ${i} products in ${payloads.length} payloads`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await setTimeout(5000)
|
||||||
|
|
||||||
|
const queryArgs = [
|
||||||
|
{
|
||||||
|
select: {
|
||||||
|
product: {
|
||||||
|
variants: {
|
||||||
|
prices: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
"product.variants.prices.amount": { $gt: 50 },
|
||||||
|
"product.variants.prices.currency_code": { $eq: "AUD" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orderBy: [{ "product.variants.prices.amount": "DESC" }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
await indexEngine.queryAndCount(...queryArgs)
|
||||||
|
|
||||||
|
const [results, count, perf] = await indexEngine.queryAndCount(
|
||||||
|
...queryArgs
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(perf)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -112,5 +112,10 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[Modules.INDEX]: process.env.ENABLE_INDEX_MODULE
|
||||||
|
? {
|
||||||
|
resolve: "@medusajs/index",
|
||||||
|
}
|
||||||
|
: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,25 @@
|
|||||||
import { asValue } from "awilix"
|
import { MedusaAppOutput } from "@medusajs/modules-sdk"
|
||||||
import { ContainerLike, MedusaContainer } from "@medusajs/types"
|
import { ContainerLike, MedusaContainer } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
ContainerRegistrationKeys,
|
ContainerRegistrationKeys,
|
||||||
createMedusaContainer,
|
createMedusaContainer,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
|
import { asValue } from "awilix"
|
||||||
import { createDatabase, dropDatabase } from "pg-god"
|
import { createDatabase, dropDatabase } from "pg-god"
|
||||||
import { getDatabaseURL } from "./database"
|
import { getDatabaseURL } from "./database"
|
||||||
import { startApp } from "./medusa-test-runner-utils/bootstrap-app"
|
import { startApp } from "./medusa-test-runner-utils/bootstrap-app"
|
||||||
|
import { clearInstances } from "./medusa-test-runner-utils/clear-instances"
|
||||||
|
import { configLoaderOverride } from "./medusa-test-runner-utils/config"
|
||||||
import {
|
import {
|
||||||
initDb,
|
initDb,
|
||||||
migrateDatabase,
|
migrateDatabase,
|
||||||
syncLinks,
|
syncLinks,
|
||||||
} from "./medusa-test-runner-utils/use-db"
|
} from "./medusa-test-runner-utils/use-db"
|
||||||
import { configLoaderOverride } from "./medusa-test-runner-utils/config"
|
|
||||||
import { applyEnvVarsToProcess } from "./medusa-test-runner-utils/utils"
|
import { applyEnvVarsToProcess } from "./medusa-test-runner-utils/utils"
|
||||||
import { clearInstances } from "./medusa-test-runner-utils/clear-instances"
|
|
||||||
|
|
||||||
const DB_HOST = process.env.DB_HOST
|
const DB_HOST = process.env.DB_HOST
|
||||||
const DB_USERNAME = process.env.DB_USERNAME
|
const DB_USERNAME = process.env.DB_USERNAME ?? ''
|
||||||
const DB_PASSWORD = process.env.DB_PASSWORD
|
const DB_PASSWORD = process.env.DB_PASSWORD ?? ''
|
||||||
|
|
||||||
const pgGodCredentials = {
|
const pgGodCredentials = {
|
||||||
user: DB_USERNAME,
|
user: DB_USERNAME,
|
||||||
@@ -26,11 +27,14 @@ const pgGodCredentials = {
|
|||||||
host: DB_HOST,
|
host: DB_HOST,
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbTestUtilFactory = (): any => ({
|
export const dbTestUtilFactory = (): any => ({
|
||||||
pgConnection_: null,
|
pgConnection_: null,
|
||||||
|
|
||||||
create: async function (dbName: string) {
|
create: async function (dbName: string) {
|
||||||
await createDatabase({ databaseName: dbName }, pgGodCredentials)
|
await createDatabase(
|
||||||
|
{ databaseName: dbName, errorIfExist: false },
|
||||||
|
pgGodCredentials
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
teardown: async function ({ schema }: { schema?: string } = {}) {
|
teardown: async function ({ schema }: { schema?: string } = {}) {
|
||||||
@@ -80,11 +84,14 @@ export interface MedusaSuiteOptions<TService = unknown> {
|
|||||||
schema: string
|
schema: string
|
||||||
clientUrl: string
|
clientUrl: string
|
||||||
}
|
}
|
||||||
|
getMedusaApp: () => MedusaAppOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
export function medusaIntegrationTestRunner({
|
export function medusaIntegrationTestRunner({
|
||||||
moduleName,
|
moduleName,
|
||||||
dbName,
|
dbName,
|
||||||
|
medusaConfigFile,
|
||||||
|
loadApplication,
|
||||||
schema = "public",
|
schema = "public",
|
||||||
env = {},
|
env = {},
|
||||||
debug = false,
|
debug = false,
|
||||||
@@ -94,6 +101,8 @@ export function medusaIntegrationTestRunner({
|
|||||||
moduleName?: string
|
moduleName?: string
|
||||||
env?: Record<string, any>
|
env?: Record<string, any>
|
||||||
dbName?: string
|
dbName?: string
|
||||||
|
medusaConfigFile?: string
|
||||||
|
loadApplication?: boolean
|
||||||
schema?: string
|
schema?: string
|
||||||
debug?: boolean
|
debug?: boolean
|
||||||
inApp?: boolean
|
inApp?: boolean
|
||||||
@@ -110,12 +119,13 @@ export function medusaIntegrationTestRunner({
|
|||||||
debug,
|
debug,
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = process.cwd()
|
const cwd = medusaConfigFile ?? process.cwd()
|
||||||
|
|
||||||
let shutdown = async () => void 0
|
let shutdown = async () => void 0
|
||||||
const dbUtils = dbTestUtilFactory()
|
const dbUtils = dbTestUtilFactory()
|
||||||
let globalContainer: ContainerLike
|
let globalContainer: ContainerLike
|
||||||
let apiUtils: any
|
let apiUtils: any
|
||||||
|
let loadedApplication: any
|
||||||
|
|
||||||
let options = {
|
let options = {
|
||||||
api: new Proxy(
|
api: new Proxy(
|
||||||
@@ -134,6 +144,7 @@ export function medusaIntegrationTestRunner({
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
getMedusaApp: () => loadedApplication,
|
||||||
getContainer: () => globalContainer,
|
getContainer: () => globalContainer,
|
||||||
dbConfig: {
|
dbConfig: {
|
||||||
dbName,
|
dbName,
|
||||||
@@ -175,6 +186,10 @@ export function medusaIntegrationTestRunner({
|
|||||||
let serverShutdownRes: () => any
|
let serverShutdownRes: () => any
|
||||||
let portRes: number
|
let portRes: number
|
||||||
|
|
||||||
|
if (loadApplication) {
|
||||||
|
loadedApplication = await appLoader.load()
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
shutdown = () => void 0,
|
shutdown = () => void 0,
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { mergeTypeDefs } from "@graphql-tools/merge"
|
||||||
|
import { makeExecutableSchema } from "@graphql-tools/schema"
|
||||||
|
import { gqlGetFieldsAndRelations } from "../../utils"
|
||||||
|
|
||||||
|
const userModule = `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
blabla: WHATEVER
|
||||||
|
}
|
||||||
|
|
||||||
|
type Post {
|
||||||
|
author: User!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const postModule = `
|
||||||
|
type Post {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
date: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type User {
|
||||||
|
posts: [Post!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type WHATEVER {
|
||||||
|
random_field: String
|
||||||
|
post: Post
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const mergedSchema = mergeTypeDefs([userModule, postModule])
|
||||||
|
const schema = makeExecutableSchema({
|
||||||
|
typeDefs: mergedSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
const types = schema.getTypeMap()
|
||||||
|
|
||||||
|
describe("gqlGetFieldsAndRelations", function () {
|
||||||
|
it("Should get all fields of a given entity", async function () {
|
||||||
|
const fields = gqlGetFieldsAndRelations(types, "User")
|
||||||
|
expect(fields).toEqual(expect.arrayContaining(["id", "name"]))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should get all fields of a given entity and a relation", async function () {
|
||||||
|
const fields = gqlGetFieldsAndRelations(types, "User", ["posts"])
|
||||||
|
expect(fields).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"posts.id",
|
||||||
|
"posts.title",
|
||||||
|
"posts.date",
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should get all fields of a given entity and many relations", async function () {
|
||||||
|
const fields = gqlGetFieldsAndRelations(types, "User", [
|
||||||
|
"posts",
|
||||||
|
"blabla",
|
||||||
|
"blabla.post",
|
||||||
|
])
|
||||||
|
expect(fields).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"posts.id",
|
||||||
|
"posts.title",
|
||||||
|
"posts.date",
|
||||||
|
"blabla.random_field",
|
||||||
|
"blabla.post.id",
|
||||||
|
"blabla.post.title",
|
||||||
|
"blabla.post.date",
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("Should get all fields of a given entity and many relations limited to the relations given", async function () {
|
||||||
|
const fields = gqlGetFieldsAndRelations(types, "User", ["posts", "blabla"])
|
||||||
|
expect(fields).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"posts.id",
|
||||||
|
"posts.title",
|
||||||
|
"posts.date",
|
||||||
|
"blabla.random_field",
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import { ModuleDefinition } from "@medusajs/types"
|
import { ModuleDefinition } from "@medusajs/types"
|
||||||
import { Modules, upperCaseFirst } from "@medusajs/utils"
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
Modules,
|
||||||
|
upperCaseFirst,
|
||||||
|
} from "@medusajs/utils"
|
||||||
import { MODULE_RESOURCE_TYPE, MODULE_SCOPE } from "./types"
|
import { MODULE_RESOURCE_TYPE, MODULE_SCOPE } from "./types"
|
||||||
|
|
||||||
export const MODULE_PACKAGE_NAMES = {
|
export const MODULE_PACKAGE_NAMES = {
|
||||||
@@ -27,6 +31,7 @@ export const MODULE_PACKAGE_NAMES = {
|
|||||||
[Modules.CURRENCY]: "@medusajs/currency",
|
[Modules.CURRENCY]: "@medusajs/currency",
|
||||||
[Modules.FILE]: "@medusajs/file",
|
[Modules.FILE]: "@medusajs/file",
|
||||||
[Modules.NOTIFICATION]: "@medusajs/notification",
|
[Modules.NOTIFICATION]: "@medusajs/notification",
|
||||||
|
[Modules.INDEX]: "@medusajs/index",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModulesDefinition: {
|
export const ModulesDefinition: {
|
||||||
@@ -308,6 +313,22 @@ export const ModulesDefinition: {
|
|||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[Modules.INDEX]: {
|
||||||
|
key: Modules.INDEX,
|
||||||
|
defaultPackage: false,
|
||||||
|
label: upperCaseFirst(Modules.INDEX),
|
||||||
|
isRequired: false,
|
||||||
|
isQueryable: false,
|
||||||
|
dependencies: [
|
||||||
|
Modules.EVENT_BUS,
|
||||||
|
"logger",
|
||||||
|
ContainerRegistrationKeys.REMOTE_QUERY,
|
||||||
|
],
|
||||||
|
defaultModuleDeclaration: {
|
||||||
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MODULE_DEFINITIONS: ModuleDefinition[] =
|
export const MODULE_DEFINITIONS: ModuleDefinition[] =
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ export * from "./medusa-module"
|
|||||||
export * from "./remote-link"
|
export * from "./remote-link"
|
||||||
export * from "./remote-query"
|
export * from "./remote-query"
|
||||||
export * from "./types"
|
export * from "./types"
|
||||||
export { gqlSchemaToTypes } from "./utils"
|
export * from "./utils"
|
||||||
@@ -102,6 +102,11 @@ export async function loadModules(
|
|||||||
let declaration: any = {}
|
let declaration: any = {}
|
||||||
let definition: Partial<ModuleDefinition> | undefined = undefined
|
let definition: Partial<ModuleDefinition> | undefined = undefined
|
||||||
|
|
||||||
|
// Skip disabled modules
|
||||||
|
if (mod === false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (isObject(mod)) {
|
if (isObject(mod)) {
|
||||||
const mod_ = mod as unknown as InternalModuleDeclaration
|
const mod_ = mod as unknown as InternalModuleDeclaration
|
||||||
path = mod_.resolve ?? MODULE_PACKAGE_NAMES[moduleName]
|
path = mod_.resolve ?? MODULE_PACKAGE_NAMES[moduleName]
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { GraphQLNamedType, GraphQLObjectType, isObjectType } from "graphql"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a list of fields and fields relations for a given type with the requested relations
|
||||||
|
* @param schemaTypeMap
|
||||||
|
* @param typeName
|
||||||
|
* @param relations
|
||||||
|
*/
|
||||||
|
export function gqlGetFieldsAndRelations(
|
||||||
|
schemaTypeMap: { [key: string]: GraphQLNamedType },
|
||||||
|
typeName: string,
|
||||||
|
relations: string[] = []
|
||||||
|
) {
|
||||||
|
const result: string[] = []
|
||||||
|
|
||||||
|
function traverseFields(typeName, prefix = "") {
|
||||||
|
const type = schemaTypeMap[typeName]
|
||||||
|
|
||||||
|
if (!(type instanceof GraphQLObjectType)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = type.getFields()
|
||||||
|
|
||||||
|
for (const fieldName in fields) {
|
||||||
|
const field = fields[fieldName]
|
||||||
|
let fieldType = field.type as any
|
||||||
|
|
||||||
|
while (fieldType.ofType) {
|
||||||
|
fieldType = fieldType.ofType
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isObjectType(fieldType)) {
|
||||||
|
result.push(`${prefix}${fieldName}`)
|
||||||
|
} else if (relations.includes(prefix + fieldName)) {
|
||||||
|
traverseFields(fieldType.name, `${prefix}${fieldName}.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverseFields(typeName)
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./clean-graphql-schema"
|
export * from "./clean-graphql-schema"
|
||||||
export * from "./graphql-schema-to-fields"
|
export * from "./graphql-schema-to-fields"
|
||||||
export * from "./gql-schema-to-types"
|
export * from "./gql-schema-to-types"
|
||||||
|
export * from "./gql-get-fields-and-relations"
|
||||||
@@ -31,3 +31,4 @@ export * as UserTypes from "./user"
|
|||||||
export * as WorkflowTypes from "./workflow"
|
export * as WorkflowTypes from "./workflow"
|
||||||
export * as WorkflowsSdkTypes from "./workflows-sdk"
|
export * as WorkflowsSdkTypes from "./workflows-sdk"
|
||||||
export * as DmlTypes from "./dml"
|
export * as DmlTypes from "./dml"
|
||||||
|
export * as IndexTypes from "./index/index"
|
||||||
@@ -41,3 +41,4 @@ export * from "./workflow"
|
|||||||
export * from "./workflows"
|
export * from "./workflows"
|
||||||
export * from "./workflows-sdk"
|
export * from "./workflows-sdk"
|
||||||
export * from "./dml"
|
export * from "./dml"
|
||||||
|
export * from "./index"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./service"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { IModuleService } from "../modules-sdk"
|
||||||
|
|
||||||
|
export interface IIndexService extends IModuleService {
|
||||||
|
query(...args): Promise<any>
|
||||||
|
queryAndCount(...args): Promise<any>
|
||||||
|
}
|
||||||
@@ -29,7 +29,6 @@ export const MedusaErrorCodes = {
|
|||||||
* @extends Error
|
* @extends Error
|
||||||
*/
|
*/
|
||||||
export class MedusaError extends Error {
|
export class MedusaError extends Error {
|
||||||
// Symbol is not serializable
|
|
||||||
__isMedusaError = true
|
__isMedusaError = true
|
||||||
|
|
||||||
public type: string
|
public type: string
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -49,6 +49,7 @@ export enum Modules {
|
|||||||
CURRENCY = "Currency",
|
CURRENCY = "Currency",
|
||||||
FILE = "File",
|
FILE = "File",
|
||||||
NOTIFICATION = "Notification",
|
NOTIFICATION = "Notification",
|
||||||
|
INDEX = "Index",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModuleRegistrationName = Modules
|
export const ModuleRegistrationName = Modules
|
||||||
@@ -78,5 +79,6 @@ declare module "@medusajs/types" {
|
|||||||
[Modules.CURRENCY]: ICurrencyModuleService
|
[Modules.CURRENCY]: ICurrencyModuleService
|
||||||
[Modules.FILE]: IFileModuleService
|
[Modules.FILE]: IFileModuleService
|
||||||
[Modules.NOTIFICATION]: INotificationModuleService
|
[Modules.NOTIFICATION]: INotificationModuleService
|
||||||
|
[Modules.INDEX]: any // TODO: define index module interface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/dist
|
||||||
|
node_modules
|
||||||
|
.DS_store
|
||||||
|
.env*
|
||||||
|
.env
|
||||||
|
*.sql
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# @medusajs/index
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Index Module
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import {
|
||||||
|
EventBusTypes,
|
||||||
|
IEventBusModuleService,
|
||||||
|
Message,
|
||||||
|
Subscriber,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
|
||||||
|
export class EventBusServiceMock implements IEventBusModuleService {
|
||||||
|
protected readonly subscribers_: Map<string | symbol, Set<Subscriber>> =
|
||||||
|
new Map()
|
||||||
|
|
||||||
|
async emit<T>(
|
||||||
|
messages: Message<T> | Message<T>[],
|
||||||
|
options?: Record<string, unknown>
|
||||||
|
): Promise<void> {
|
||||||
|
const messages_ = Array.isArray(messages) ? messages : [messages]
|
||||||
|
|
||||||
|
for (const message of messages_) {
|
||||||
|
const subscribers = this.subscribers_.get(message.name)
|
||||||
|
|
||||||
|
for (const subscriber of subscribers ?? []) {
|
||||||
|
const { options, ...payload } = message
|
||||||
|
await subscriber(payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(event: string | symbol, subscriber: Subscriber): this {
|
||||||
|
this.subscribers_.set(event, new Set([subscriber]))
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe(
|
||||||
|
event: string | symbol,
|
||||||
|
subscriber: Subscriber,
|
||||||
|
context?: EventBusTypes.SubscriberContext
|
||||||
|
): this {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseGroupedEvents(eventGroupId: string): Promise<void> {
|
||||||
|
throw new Error("Method not implemented.")
|
||||||
|
}
|
||||||
|
clearGroupedEvents(eventGroupId: string): Promise<void> {
|
||||||
|
throw new Error("Method not implemented.")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./event-bus"
|
||||||
|
export * from "./schema"
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
const { defineConfig, Modules } = require("@medusajs/utils")
|
||||||
|
const { schema } = require("./schema")
|
||||||
|
|
||||||
|
export const dbName = "medusa-index-integration-2024"
|
||||||
|
const DB_HOST = process.env.DB_HOST ?? "localhost:5432"
|
||||||
|
const DB_USERNAME = process.env.DB_USERNAME ?? ""
|
||||||
|
const DB_PASSWORD = process.env.DB_PASSWORD ?? ""
|
||||||
|
const DB_URL = `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${dbName}`
|
||||||
|
|
||||||
|
const config = defineConfig({
|
||||||
|
admin: {
|
||||||
|
disable: true,
|
||||||
|
},
|
||||||
|
projectConfig: {
|
||||||
|
databaseUrl: DB_URL,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.keys(config.modules).forEach((key) => {
|
||||||
|
if ([Modules.EVENT_BUS].includes(key)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config.modules[key] = false
|
||||||
|
})
|
||||||
|
|
||||||
|
config.modules[Modules.INDEX] = {
|
||||||
|
resolve: "@medusajs/index",
|
||||||
|
dependencies: [Modules.EVENT_BUS],
|
||||||
|
options: {
|
||||||
|
schema,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
config.modules[Modules.PRODUCT] = true
|
||||||
|
config.modules[Modules.PRICING] = true
|
||||||
|
|
||||||
|
export default config
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export const schema = `
|
||||||
|
type Product @Listeners(values: ["product.created", "product.updated", "product.deleted"]) {
|
||||||
|
id: String
|
||||||
|
title: String
|
||||||
|
deep: InternalNested
|
||||||
|
variants: [ProductVariant]
|
||||||
|
}
|
||||||
|
|
||||||
|
type InternalNested {
|
||||||
|
a: Int
|
||||||
|
obj: InternalObject
|
||||||
|
}
|
||||||
|
|
||||||
|
type InternalObject {
|
||||||
|
b: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductVariant @Listeners(values: ["variant.created", "variant.updated", "variant.deleted"]) {
|
||||||
|
id: String
|
||||||
|
product_id: String
|
||||||
|
sku: String
|
||||||
|
prices: [Price]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Price @Listeners(values: ["price.created", "price.updated", "price.deleted"]) {
|
||||||
|
amount: Int
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -0,0 +1,747 @@
|
|||||||
|
import {
|
||||||
|
MedusaAppLoader,
|
||||||
|
configLoader,
|
||||||
|
container,
|
||||||
|
logger,
|
||||||
|
} from "@medusajs/framework"
|
||||||
|
import { MedusaAppOutput, MedusaModule } from "@medusajs/modules-sdk"
|
||||||
|
import { EventBusTypes } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
ModuleRegistrationName,
|
||||||
|
Modules,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
import { EntityManager } from "@mikro-orm/postgresql"
|
||||||
|
import { IndexData, IndexRelation } from "@models"
|
||||||
|
import { asValue } from "awilix"
|
||||||
|
import { dbTestUtilFactory } from "medusa-test-utils"
|
||||||
|
import { initDb } from "medusa-test-utils/dist/medusa-test-runner-utils/use-db"
|
||||||
|
import * as path from "path"
|
||||||
|
import { EventBusServiceMock } from "../__fixtures__"
|
||||||
|
import { dbName } from "../__fixtures__/medusa-config"
|
||||||
|
|
||||||
|
const eventBusMock = new EventBusServiceMock()
|
||||||
|
const remoteQueryMock = jest.fn()
|
||||||
|
const dbUtils = dbTestUtilFactory()
|
||||||
|
|
||||||
|
jest.setTimeout(300000)
|
||||||
|
|
||||||
|
const productId = "prod_1"
|
||||||
|
const variantId = "var_1"
|
||||||
|
const priceSetId = "price_set_1"
|
||||||
|
const priceId = "money_amount_1"
|
||||||
|
const linkId = "link_id_1"
|
||||||
|
|
||||||
|
const sendEvents = async (eventDataToEmit) => {
|
||||||
|
let a = 0
|
||||||
|
remoteQueryMock.mockImplementation((query) => {
|
||||||
|
query = query.__value
|
||||||
|
if (query.product) {
|
||||||
|
return {
|
||||||
|
id: a++ > 0 ? "aaaa" : productId,
|
||||||
|
}
|
||||||
|
} else if (query.product_variant) {
|
||||||
|
return {
|
||||||
|
id: variantId,
|
||||||
|
sku: "aaa test aaa",
|
||||||
|
product: {
|
||||||
|
id: productId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if (query.price_set) {
|
||||||
|
return {
|
||||||
|
id: priceSetId,
|
||||||
|
}
|
||||||
|
} else if (query.price) {
|
||||||
|
return {
|
||||||
|
id: priceId,
|
||||||
|
amount: 100,
|
||||||
|
price_set: [
|
||||||
|
{
|
||||||
|
id: priceSetId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
} else if (query.product_variant_price_set) {
|
||||||
|
return {
|
||||||
|
id: linkId,
|
||||||
|
variant_id: variantId,
|
||||||
|
price_set_id: priceSetId,
|
||||||
|
variant: [
|
||||||
|
{
|
||||||
|
id: variantId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
await eventBusMock.emit(eventDataToEmit)
|
||||||
|
}
|
||||||
|
|
||||||
|
let isFirstTime = true
|
||||||
|
let medusaAppLoader!: MedusaAppLoader
|
||||||
|
|
||||||
|
const beforeAll_ = async () => {
|
||||||
|
try {
|
||||||
|
configLoader(path.join(__dirname, "./../__fixtures__"), "medusa-config.js")
|
||||||
|
|
||||||
|
console.log(`Creating database ${dbName}`)
|
||||||
|
await dbUtils.create(dbName)
|
||||||
|
dbUtils.pgConnection_ = await initDb()
|
||||||
|
|
||||||
|
container.register({
|
||||||
|
[ContainerRegistrationKeys.LOGGER]: asValue(logger),
|
||||||
|
[ContainerRegistrationKeys.REMOTE_QUERY]: asValue(null),
|
||||||
|
[ContainerRegistrationKeys.PG_CONNECTION]: asValue(dbUtils.pgConnection_),
|
||||||
|
})
|
||||||
|
|
||||||
|
medusaAppLoader = new MedusaAppLoader(container as any)
|
||||||
|
|
||||||
|
// Migrations
|
||||||
|
await medusaAppLoader.runModulesMigrations()
|
||||||
|
const linkPlanner = await medusaAppLoader.getLinksExecutionPlanner()
|
||||||
|
const plan = await linkPlanner.createPlan()
|
||||||
|
await linkPlanner.executePlan(plan)
|
||||||
|
|
||||||
|
// Clear partially loaded instances
|
||||||
|
MedusaModule.clearInstances()
|
||||||
|
|
||||||
|
// Bootstrap modules
|
||||||
|
const globalApp = await medusaAppLoader.load()
|
||||||
|
|
||||||
|
const 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)
|
||||||
|
|
||||||
|
return globalApp
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error initializing", error?.message)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeEach_ = async (eventDataToEmit) => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
|
if (isFirstTime) {
|
||||||
|
isFirstTime = false
|
||||||
|
await sendEvents(eventDataToEmit)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await medusaAppLoader.runModulesLoader()
|
||||||
|
|
||||||
|
await sendEvents(eventDataToEmit)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error runner modules loaders", error?.message)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const afterEach_ = async () => {
|
||||||
|
try {
|
||||||
|
await dbUtils.teardown({ schema: "public" })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error tearing down database:", error?.message)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("IndexModuleService", function () {
|
||||||
|
let medusaApp: MedusaAppOutput
|
||||||
|
let onApplicationPrepareShutdown!: () => Promise<void>
|
||||||
|
let onApplicationShutdown!: () => Promise<void>
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
medusaApp = await beforeAll_()
|
||||||
|
onApplicationPrepareShutdown = medusaApp.onApplicationPrepareShutdown
|
||||||
|
onApplicationShutdown = medusaApp.onApplicationShutdown
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await onApplicationPrepareShutdown()
|
||||||
|
await onApplicationShutdown()
|
||||||
|
await dbUtils.shutdown(dbName)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("on created or attached events", function () {
|
||||||
|
let manager
|
||||||
|
|
||||||
|
const eventDataToEmit: EventBusTypes.Event[] = [
|
||||||
|
{
|
||||||
|
name: "product.created",
|
||||||
|
data: {
|
||||||
|
id: productId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "product.created",
|
||||||
|
data: {
|
||||||
|
id: "PRODUCTASDASDAS",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "variant.created",
|
||||||
|
data: {
|
||||||
|
id: variantId,
|
||||||
|
product: {
|
||||||
|
id: productId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PriceSet.created",
|
||||||
|
data: {
|
||||||
|
id: priceSetId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "price.created",
|
||||||
|
data: {
|
||||||
|
id: priceId,
|
||||||
|
price_set: {
|
||||||
|
id: priceSetId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "LinkProductVariantPriceSet.attached",
|
||||||
|
data: {
|
||||||
|
id: linkId,
|
||||||
|
variant_id: variantId,
|
||||||
|
price_set_id: priceSetId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await beforeEach_(eventDataToEmit)
|
||||||
|
|
||||||
|
manager = (
|
||||||
|
medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX) as any
|
||||||
|
).container_.manager as EntityManager
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
|
||||||
|
const indexEntries: IndexData[] = await manager.find(IndexData, {})
|
||||||
|
|
||||||
|
const productIndexEntries = indexEntries.filter((entry) => {
|
||||||
|
return entry.name === "Product"
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(productIndexEntries).toHaveLength(2)
|
||||||
|
|
||||||
|
const variantIndexEntries = indexEntries.filter((entry) => {
|
||||||
|
return entry.name === "ProductVariant"
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(variantIndexEntries).toHaveLength(1)
|
||||||
|
|
||||||
|
const priceSetIndexEntries = indexEntries.filter((entry) => {
|
||||||
|
return entry.name === "PriceSet"
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(priceSetIndexEntries).toHaveLength(1)
|
||||||
|
|
||||||
|
const priceIndexEntries = indexEntries.filter((entry) => {
|
||||||
|
return entry.name === "Price"
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(priceIndexEntries).toHaveLength(1)
|
||||||
|
|
||||||
|
const linkIndexEntries = indexEntries.filter((entry) => {
|
||||||
|
return entry.name === "LinkProductVariantPriceSet"
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(linkIndexEntries).toHaveLength(1)
|
||||||
|
|
||||||
|
const indexRelationEntries: IndexRelation[] = await manager.find(
|
||||||
|
IndexRelation,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(indexRelationEntries).toHaveLength(4)
|
||||||
|
|
||||||
|
const productVariantIndexRelationEntries = indexRelationEntries.filter(
|
||||||
|
(entry) => {
|
||||||
|
return (
|
||||||
|
entry.parent_id === productId &&
|
||||||
|
entry.parent_name === "Product" &&
|
||||||
|
entry.child_id === variantId &&
|
||||||
|
entry.child_name === "ProductVariant"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(productVariantIndexRelationEntries).toHaveLength(1)
|
||||||
|
|
||||||
|
const variantLinkIndexRelationEntries = indexRelationEntries.filter(
|
||||||
|
(entry) => {
|
||||||
|
return (
|
||||||
|
entry.parent_id === variantId &&
|
||||||
|
entry.parent_name === "ProductVariant" &&
|
||||||
|
entry.child_id === linkId &&
|
||||||
|
entry.child_name === "LinkProductVariantPriceSet"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(variantLinkIndexRelationEntries).toHaveLength(1)
|
||||||
|
|
||||||
|
const linkPriceSetIndexRelationEntries = indexRelationEntries.filter(
|
||||||
|
(entry) => {
|
||||||
|
return (
|
||||||
|
entry.parent_id === linkId &&
|
||||||
|
entry.parent_name === "LinkProductVariantPriceSet" &&
|
||||||
|
entry.child_id === priceSetId &&
|
||||||
|
entry.child_name === "PriceSet"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(linkPriceSetIndexRelationEntries).toHaveLength(1)
|
||||||
|
|
||||||
|
const priceSetPriceIndexRelationEntries = indexRelationEntries.filter(
|
||||||
|
(entry) => {
|
||||||
|
return (
|
||||||
|
entry.parent_id === priceSetId &&
|
||||||
|
entry.parent_name === "PriceSet" &&
|
||||||
|
entry.child_id === priceId &&
|
||||||
|
entry.child_name === "Price"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(priceSetPriceIndexRelationEntries).toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("on unordered created or attached events", function () {
|
||||||
|
let manager
|
||||||
|
|
||||||
|
const eventDataToEmit: EventBusTypes.Event[] = [
|
||||||
|
{
|
||||||
|
name: "variant.created",
|
||||||
|
data: {
|
||||||
|
id: variantId,
|
||||||
|
product: {
|
||||||
|
id: productId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "product.created",
|
||||||
|
data: {
|
||||||
|
id: productId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "product.created",
|
||||||
|
data: {
|
||||||
|
id: "PRODUCTASDASDAS",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PriceSet.created",
|
||||||
|
data: {
|
||||||
|
id: priceSetId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "price.created",
|
||||||
|
data: {
|
||||||
|
id: priceId,
|
||||||
|
price_set: {
|
||||||
|
id: priceSetId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "LinkProductVariantPriceSet.attached",
|
||||||
|
data: {
|
||||||
|
id: linkId,
|
||||||
|
variant_id: variantId,
|
||||||
|
price_set_id: priceSetId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await beforeEach_(eventDataToEmit)
|
||||||
|
|
||||||
|
manager = (
|
||||||
|
medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX) as any
|
||||||
|
).container_.manager as EntityManager
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
|
||||||
|
const indexEntries: IndexData[] = await manager.find(IndexData, {})
|
||||||
|
|
||||||
|
const productIndexEntries = indexEntries.filter((entry) => {
|
||||||
|
return entry.name === "Product"
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(productIndexEntries).toHaveLength(2)
|
||||||
|
expect(productIndexEntries[0].id).toEqual(productId)
|
||||||
|
|
||||||
|
const variantIndexEntries = indexEntries.filter((entry) => {
|
||||||
|
return entry.name === "ProductVariant"
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(variantIndexEntries).toHaveLength(1)
|
||||||
|
expect(variantIndexEntries[0].id).toEqual(variantId)
|
||||||
|
|
||||||
|
const priceSetIndexEntries = indexEntries.filter((entry) => {
|
||||||
|
return entry.name === "PriceSet"
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(priceSetIndexEntries).toHaveLength(1)
|
||||||
|
expect(priceSetIndexEntries[0].id).toEqual(priceSetId)
|
||||||
|
|
||||||
|
const priceIndexEntries = indexEntries.filter((entry) => {
|
||||||
|
return entry.name === "Price"
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(priceIndexEntries).toHaveLength(1)
|
||||||
|
expect(priceIndexEntries[0].id).toEqual(priceId)
|
||||||
|
|
||||||
|
const linkIndexEntries = indexEntries.filter((entry) => {
|
||||||
|
return entry.name === "LinkProductVariantPriceSet"
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(linkIndexEntries).toHaveLength(1)
|
||||||
|
expect(linkIndexEntries[0].id).toEqual(linkId)
|
||||||
|
|
||||||
|
const indexRelationEntries: IndexRelation[] = await manager.find(
|
||||||
|
IndexRelation,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(indexRelationEntries).toHaveLength(4)
|
||||||
|
|
||||||
|
const productVariantIndexRelationEntries = indexRelationEntries.filter(
|
||||||
|
(entry) => {
|
||||||
|
return (
|
||||||
|
entry.parent_id === productId &&
|
||||||
|
entry.parent_name === "Product" &&
|
||||||
|
entry.child_id === variantId &&
|
||||||
|
entry.child_name === "ProductVariant"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(productVariantIndexRelationEntries).toHaveLength(1)
|
||||||
|
|
||||||
|
const variantLinkIndexRelationEntries = indexRelationEntries.filter(
|
||||||
|
(entry) => {
|
||||||
|
return (
|
||||||
|
entry.parent_id === variantId &&
|
||||||
|
entry.parent_name === "ProductVariant" &&
|
||||||
|
entry.child_id === linkId &&
|
||||||
|
entry.child_name === "LinkProductVariantPriceSet"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(variantLinkIndexRelationEntries).toHaveLength(1)
|
||||||
|
|
||||||
|
const linkPriceSetIndexRelationEntries = indexRelationEntries.filter(
|
||||||
|
(entry) => {
|
||||||
|
return (
|
||||||
|
entry.parent_id === linkId &&
|
||||||
|
entry.parent_name === "LinkProductVariantPriceSet" &&
|
||||||
|
entry.child_id === priceSetId &&
|
||||||
|
entry.child_name === "PriceSet"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(linkPriceSetIndexRelationEntries).toHaveLength(1)
|
||||||
|
|
||||||
|
const priceSetPriceIndexRelationEntries = indexRelationEntries.filter(
|
||||||
|
(entry) => {
|
||||||
|
return (
|
||||||
|
entry.parent_id === priceSetId &&
|
||||||
|
entry.parent_name === "PriceSet" &&
|
||||||
|
entry.child_id === priceId &&
|
||||||
|
entry.child_name === "Price"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(priceSetPriceIndexRelationEntries).toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("on updated events", function () {
|
||||||
|
let manager
|
||||||
|
|
||||||
|
const updateData = async (manager) => {
|
||||||
|
const indexRepository = manager.getRepository(IndexData)
|
||||||
|
await indexRepository.upsertMany([
|
||||||
|
{
|
||||||
|
id: productId,
|
||||||
|
name: "Product",
|
||||||
|
data: {
|
||||||
|
id: productId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: variantId,
|
||||||
|
name: "ProductVariant",
|
||||||
|
data: {
|
||||||
|
id: variantId,
|
||||||
|
sku: "aaa test aaa",
|
||||||
|
product: {
|
||||||
|
id: productId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventDataToEmit: EventBusTypes.Event[] = [
|
||||||
|
{
|
||||||
|
name: "product.updated",
|
||||||
|
data: {
|
||||||
|
id: productId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "variant.updated",
|
||||||
|
data: {
|
||||||
|
id: variantId,
|
||||||
|
product: {
|
||||||
|
id: productId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await beforeEach_(eventDataToEmit)
|
||||||
|
|
||||||
|
manager = (
|
||||||
|
medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX) as any
|
||||||
|
).container_.manager as EntityManager
|
||||||
|
|
||||||
|
await updateData(manager)
|
||||||
|
|
||||||
|
remoteQueryMock.mockImplementation((query) => {
|
||||||
|
query = query.__value
|
||||||
|
if (query.product) {
|
||||||
|
return {
|
||||||
|
id: productId,
|
||||||
|
title: "updated Title",
|
||||||
|
}
|
||||||
|
} else if (query.product_variant) {
|
||||||
|
return {
|
||||||
|
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)
|
||||||
|
|
||||||
|
const productEntry = updatedIndexEntries.find((entry) => {
|
||||||
|
return entry.name === "Product" && entry.id === productId
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(productEntry?.data?.title).toEqual("updated Title")
|
||||||
|
|
||||||
|
const variantEntry = updatedIndexEntries.find((entry) => {
|
||||||
|
return entry.name === "ProductVariant" && entry.id === variantId
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(variantEntry?.data?.sku).toEqual("updated sku")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("on deleted events", function () {
|
||||||
|
let manager
|
||||||
|
|
||||||
|
const eventDataToEmit: EventBusTypes.Event[] = [
|
||||||
|
{
|
||||||
|
name: "product.created",
|
||||||
|
data: {
|
||||||
|
id: productId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "variant.created",
|
||||||
|
data: {
|
||||||
|
id: variantId,
|
||||||
|
product: {
|
||||||
|
id: productId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PriceSet.created",
|
||||||
|
data: {
|
||||||
|
id: priceSetId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "price.created",
|
||||||
|
data: {
|
||||||
|
id: priceId,
|
||||||
|
price_set: {
|
||||||
|
id: priceSetId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "LinkProductVariantPriceSet.attached",
|
||||||
|
data: {
|
||||||
|
id: linkId,
|
||||||
|
variant_id: variantId,
|
||||||
|
price_set_id: priceSetId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const deleteEventDataToEmit: EventBusTypes.Event[] = [
|
||||||
|
{
|
||||||
|
name: "product.deleted",
|
||||||
|
data: {
|
||||||
|
id: productId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "variant.deleted",
|
||||||
|
data: {
|
||||||
|
id: variantId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await beforeEach_(eventDataToEmit)
|
||||||
|
|
||||||
|
manager = (
|
||||||
|
medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX) as any
|
||||||
|
).container_.manager as EntityManager
|
||||||
|
|
||||||
|
remoteQueryMock.mockImplementation((query) => {
|
||||||
|
query = query.__value
|
||||||
|
if (query.product) {
|
||||||
|
return {
|
||||||
|
id: productId,
|
||||||
|
}
|
||||||
|
} else if (query.product_variant) {
|
||||||
|
return {
|
||||||
|
id: variantId,
|
||||||
|
product: [
|
||||||
|
{
|
||||||
|
id: productId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
await eventBusMock.emit(deleteEventDataToEmit)
|
||||||
|
})
|
||||||
|
|
||||||
|
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, {})
|
||||||
|
|
||||||
|
expect(indexEntries).toHaveLength(3)
|
||||||
|
expect(indexRelationEntries).toHaveLength(2)
|
||||||
|
|
||||||
|
const linkIndexEntry = indexEntries.find((entry) => {
|
||||||
|
return (
|
||||||
|
entry.name === "LinkProductVariantPriceSet" && entry.id === linkId
|
||||||
|
)
|
||||||
|
})!
|
||||||
|
|
||||||
|
const priceSetIndexEntry = indexEntries.find((entry) => {
|
||||||
|
return entry.name === "PriceSet" && entry.id === priceSetId
|
||||||
|
})!
|
||||||
|
|
||||||
|
const priceIndexEntry = indexEntries.find((entry) => {
|
||||||
|
return entry.name === "Price" && entry.id === priceId
|
||||||
|
})!
|
||||||
|
|
||||||
|
const linkPriceSetIndexRelationEntry = indexRelationEntries.find(
|
||||||
|
(entry) => {
|
||||||
|
return (
|
||||||
|
entry.parent_id === linkId &&
|
||||||
|
entry.parent_name === "LinkProductVariantPriceSet" &&
|
||||||
|
entry.child_id === priceSetId &&
|
||||||
|
entry.child_name === "PriceSet"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)!
|
||||||
|
|
||||||
|
expect(linkPriceSetIndexRelationEntry.parent).toEqual(linkIndexEntry)
|
||||||
|
expect(linkPriceSetIndexRelationEntry.child).toEqual(priceSetIndexEntry)
|
||||||
|
|
||||||
|
const priceSetPriceIndexRelationEntry = indexRelationEntries.find(
|
||||||
|
(entry) => {
|
||||||
|
return (
|
||||||
|
entry.parent_id === priceSetId &&
|
||||||
|
entry.parent_name === "PriceSet" &&
|
||||||
|
entry.child_id === priceId &&
|
||||||
|
entry.child_name === "Price"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)!
|
||||||
|
|
||||||
|
expect(priceSetPriceIndexRelationEntry.parent).toEqual(priceSetIndexEntry)
|
||||||
|
expect(priceSetPriceIndexRelationEntry.child).toEqual(priceIndexEntry)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,562 @@
|
|||||||
|
import {
|
||||||
|
MedusaAppLoader,
|
||||||
|
configLoader,
|
||||||
|
container,
|
||||||
|
logger,
|
||||||
|
} from "@medusajs/framework"
|
||||||
|
import { MedusaAppOutput, MedusaModule } from "@medusajs/modules-sdk"
|
||||||
|
import { IndexTypes } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
ModuleRegistrationName,
|
||||||
|
Modules,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
import { EntityManager } from "@mikro-orm/postgresql"
|
||||||
|
import { IndexData, IndexRelation } from "@models"
|
||||||
|
import { asValue } from "awilix"
|
||||||
|
import { dbTestUtilFactory } from "medusa-test-utils"
|
||||||
|
import { initDb } from "medusa-test-utils/dist/medusa-test-runner-utils/use-db"
|
||||||
|
import path from "path"
|
||||||
|
import { EventBusServiceMock } from "../__fixtures__"
|
||||||
|
import { dbName } from "../__fixtures__/medusa-config"
|
||||||
|
|
||||||
|
const eventBusMock = new EventBusServiceMock()
|
||||||
|
const remoteQueryMock = jest.fn()
|
||||||
|
const dbUtils = dbTestUtilFactory()
|
||||||
|
|
||||||
|
jest.setTimeout(300000)
|
||||||
|
|
||||||
|
let isFirstTime = true
|
||||||
|
let medusaAppLoader!: MedusaAppLoader
|
||||||
|
|
||||||
|
const beforeAll_ = async () => {
|
||||||
|
try {
|
||||||
|
configLoader(path.join(__dirname, "./../__fixtures__"), "medusa-config.js")
|
||||||
|
|
||||||
|
console.log(`Creating database ${dbName}`)
|
||||||
|
await dbUtils.create(dbName)
|
||||||
|
dbUtils.pgConnection_ = await initDb()
|
||||||
|
|
||||||
|
container.register({
|
||||||
|
[ContainerRegistrationKeys.LOGGER]: asValue(logger),
|
||||||
|
[ContainerRegistrationKeys.REMOTE_QUERY]: asValue(null),
|
||||||
|
[ContainerRegistrationKeys.PG_CONNECTION]: asValue(dbUtils.pgConnection_),
|
||||||
|
})
|
||||||
|
|
||||||
|
medusaAppLoader = new MedusaAppLoader(container as any)
|
||||||
|
|
||||||
|
// Migrations
|
||||||
|
await medusaAppLoader.runModulesMigrations()
|
||||||
|
const linkPlanner = await medusaAppLoader.getLinksExecutionPlanner()
|
||||||
|
const plan = await linkPlanner.createPlan()
|
||||||
|
await linkPlanner.executePlan(plan)
|
||||||
|
|
||||||
|
// Clear partially loaded instances
|
||||||
|
MedusaModule.clearInstances()
|
||||||
|
|
||||||
|
// Bootstrap modules
|
||||||
|
const globalApp = await medusaAppLoader.load()
|
||||||
|
|
||||||
|
const 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)
|
||||||
|
|
||||||
|
return globalApp
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error initializing", error?.message)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeEach_ = async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
|
if (isFirstTime) {
|
||||||
|
isFirstTime = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await medusaAppLoader.runModulesLoader()
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error runner modules loaders", error?.message)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const afterEach_ = async () => {
|
||||||
|
try {
|
||||||
|
await dbUtils.teardown({ schema: "public" })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error tearing down database:", error?.message)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("IndexModuleService query", function () {
|
||||||
|
let medusaApp: MedusaAppOutput
|
||||||
|
let module: IndexTypes.IIndexService
|
||||||
|
let onApplicationPrepareShutdown!: () => Promise<void>
|
||||||
|
let onApplicationShutdown!: () => Promise<void>
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
medusaApp = await beforeAll_()
|
||||||
|
onApplicationPrepareShutdown = medusaApp.onApplicationPrepareShutdown
|
||||||
|
onApplicationShutdown = medusaApp.onApplicationShutdown
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await onApplicationPrepareShutdown()
|
||||||
|
await onApplicationShutdown()
|
||||||
|
await dbUtils.shutdown(dbName)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await beforeEach_()
|
||||||
|
|
||||||
|
module = medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX)
|
||||||
|
|
||||||
|
const manager = (
|
||||||
|
(medusaApp.sharedContainer!.resolve(ModuleRegistrationName.INDEX) as any)
|
||||||
|
.container_.manager as EntityManager
|
||||||
|
).fork()
|
||||||
|
|
||||||
|
const indexRepository = manager.getRepository(IndexData)
|
||||||
|
|
||||||
|
await manager.persistAndFlush(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "prod_1",
|
||||||
|
name: "Product",
|
||||||
|
data: {
|
||||||
|
id: "prod_1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "prod_2",
|
||||||
|
name: "Product",
|
||||||
|
data: {
|
||||||
|
id: "prod_2",
|
||||||
|
title: "Product 2 title",
|
||||||
|
deep: {
|
||||||
|
a: 1,
|
||||||
|
obj: {
|
||||||
|
b: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "var_1",
|
||||||
|
name: "ProductVariant",
|
||||||
|
data: {
|
||||||
|
id: "var_1",
|
||||||
|
sku: "aaa test aaa",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "var_2",
|
||||||
|
name: "ProductVariant",
|
||||||
|
data: {
|
||||||
|
id: "var_2",
|
||||||
|
sku: "sku 123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "link_id_1",
|
||||||
|
name: "LinkProductVariantPriceSet",
|
||||||
|
data: {
|
||||||
|
id: "link_id_1",
|
||||||
|
variant_id: "var_1",
|
||||||
|
price_set_id: "price_set_1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "link_id_2",
|
||||||
|
name: "LinkProductVariantPriceSet",
|
||||||
|
data: {
|
||||||
|
id: "link_id_2",
|
||||||
|
variant_id: "var_2",
|
||||||
|
price_set_id: "price_set_2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "price_set_1",
|
||||||
|
name: "PriceSet",
|
||||||
|
data: {
|
||||||
|
id: "price_set_1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "price_set_2",
|
||||||
|
name: "PriceSet",
|
||||||
|
data: {
|
||||||
|
id: "price_set_2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "money_amount_1",
|
||||||
|
name: "Price",
|
||||||
|
data: {
|
||||||
|
id: "money_amount_1",
|
||||||
|
amount: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "money_amount_2",
|
||||||
|
name: "Price",
|
||||||
|
data: {
|
||||||
|
id: "money_amount_2",
|
||||||
|
amount: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
].map((data) => indexRepository.create(data))
|
||||||
|
)
|
||||||
|
|
||||||
|
const indexRelationRepository = manager.getRepository(IndexRelation)
|
||||||
|
|
||||||
|
await manager.persistAndFlush(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
parent_id: "prod_1",
|
||||||
|
parent_name: "Product",
|
||||||
|
child_id: "var_1",
|
||||||
|
child_name: "ProductVariant",
|
||||||
|
pivot: "Product-ProductVariant",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent_id: "prod_1",
|
||||||
|
parent_name: "Product",
|
||||||
|
child_id: "var_2",
|
||||||
|
child_name: "ProductVariant",
|
||||||
|
pivot: "Product-ProductVariant",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent_id: "var_1",
|
||||||
|
parent_name: "ProductVariant",
|
||||||
|
child_id: "link_id_1",
|
||||||
|
child_name: "LinkProductVariantPriceSet",
|
||||||
|
pivot: "ProductVariant-LinkProductVariantPriceSet",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent_id: "var_2",
|
||||||
|
parent_name: "ProductVariant",
|
||||||
|
child_id: "link_id_2",
|
||||||
|
child_name: "LinkProductVariantPriceSet",
|
||||||
|
pivot: "ProductVariant-LinkProductVariantPriceSet",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent_id: "link_id_1",
|
||||||
|
parent_name: "LinkProductVariantPriceSet",
|
||||||
|
child_id: "price_set_1",
|
||||||
|
child_name: "PriceSet",
|
||||||
|
pivot: "LinkProductVariantPriceSet-PriceSet",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent_id: "link_id_2",
|
||||||
|
parent_name: "LinkProductVariantPriceSet",
|
||||||
|
child_id: "price_set_2",
|
||||||
|
child_name: "PriceSet",
|
||||||
|
pivot: "LinkProductVariantPriceSet-PriceSet",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent_id: "price_set_1",
|
||||||
|
parent_name: "PriceSet",
|
||||||
|
child_id: "money_amount_1",
|
||||||
|
child_name: "Price",
|
||||||
|
pivot: "PriceSet-Price",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parent_id: "price_set_2",
|
||||||
|
parent_name: "PriceSet",
|
||||||
|
child_id: "money_amount_2",
|
||||||
|
child_name: "Price",
|
||||||
|
pivot: "PriceSet-Price",
|
||||||
|
},
|
||||||
|
].map((data) => indexRelationRepository.create(data))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(afterEach_)
|
||||||
|
|
||||||
|
it("should query all products ordered by sku DESC", async () => {
|
||||||
|
const [result, count] = await module.queryAndCount(
|
||||||
|
{
|
||||||
|
select: {
|
||||||
|
product: {
|
||||||
|
variants: {
|
||||||
|
prices: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orderBy: [{ "product.variants.sku": "DESC" }],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(count).toEqual(2)
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
id: "prod_2",
|
||||||
|
title: "Product 2 title",
|
||||||
|
deep: {
|
||||||
|
a: 1,
|
||||||
|
obj: {
|
||||||
|
b: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variants: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "prod_1",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: "var_2",
|
||||||
|
sku: "sku 123",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
id: "money_amount_2",
|
||||||
|
amount: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "var_1",
|
||||||
|
sku: "aaa test aaa",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
id: "money_amount_1",
|
||||||
|
amount: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should query products filtering by variant sku", async () => {
|
||||||
|
const result = await module.query({
|
||||||
|
select: {
|
||||||
|
product: {
|
||||||
|
variants: {
|
||||||
|
prices: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
"product.variants.sku": { $like: "aaa%" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
id: "prod_1",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: "var_1",
|
||||||
|
sku: "aaa test aaa",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
id: "money_amount_1",
|
||||||
|
amount: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should query products filtering by price and returning the complete entity", async () => {
|
||||||
|
const [result] = await module.queryAndCount(
|
||||||
|
{
|
||||||
|
select: {
|
||||||
|
product: {
|
||||||
|
variants: {
|
||||||
|
prices: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
"product.variants.prices.amount": { $gt: "50" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepFilteredEntities: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
id: "prod_1",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: "var_1",
|
||||||
|
sku: "aaa test aaa",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
id: "money_amount_1",
|
||||||
|
amount: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "var_2",
|
||||||
|
sku: "sku 123",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
id: "money_amount_2",
|
||||||
|
amount: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should query all products", async () => {
|
||||||
|
const [result, count] = await module.queryAndCount({
|
||||||
|
select: {
|
||||||
|
product: {
|
||||||
|
variants: {
|
||||||
|
prices: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(count).toEqual(2)
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
id: "prod_1",
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: "var_1",
|
||||||
|
sku: "aaa test aaa",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
id: "money_amount_1",
|
||||||
|
amount: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "var_2",
|
||||||
|
sku: "sku 123",
|
||||||
|
prices: [
|
||||||
|
{
|
||||||
|
id: "money_amount_2",
|
||||||
|
amount: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "prod_2",
|
||||||
|
title: "Product 2 title",
|
||||||
|
deep: {
|
||||||
|
a: 1,
|
||||||
|
obj: {
|
||||||
|
b: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variants: [],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should paginate products", async () => {
|
||||||
|
const result = await module.query(
|
||||||
|
{
|
||||||
|
select: {
|
||||||
|
product: {
|
||||||
|
variants: {
|
||||||
|
prices: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
take: 1,
|
||||||
|
skip: 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
id: "prod_2",
|
||||||
|
title: "Product 2 title",
|
||||||
|
deep: {
|
||||||
|
a: 1,
|
||||||
|
obj: {
|
||||||
|
b: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variants: [],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should handle null values on where clause", async () => {
|
||||||
|
const result = await module.query({
|
||||||
|
select: {
|
||||||
|
product: {
|
||||||
|
variants: {
|
||||||
|
prices: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
"product.variants.sku": null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
id: "prod_2",
|
||||||
|
title: "Product 2 title",
|
||||||
|
deep: {
|
||||||
|
a: 1,
|
||||||
|
obj: {
|
||||||
|
b: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variants: [],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should query products filtering by deep nested levels", async () => {
|
||||||
|
const [result] = await module.queryAndCount({
|
||||||
|
select: {
|
||||||
|
product: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
"product.deep.obj.b": 15,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
id: "prod_2",
|
||||||
|
title: "Product 2 title",
|
||||||
|
deep: {
|
||||||
|
a: 1,
|
||||||
|
obj: {
|
||||||
|
b: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
module.exports = {
|
||||||
|
moduleNameMapper: {
|
||||||
|
"^@models": "<rootDir>/src/models",
|
||||||
|
"^@services": "<rootDir>/src/services",
|
||||||
|
"^@repositories": "<rootDir>/src/repositories",
|
||||||
|
"^@types": "<rootDir>/src/types",
|
||||||
|
},
|
||||||
|
transform: {
|
||||||
|
"^.+\\.[jt]s$": [
|
||||||
|
"@swc/jest",
|
||||||
|
{
|
||||||
|
jsc: {
|
||||||
|
parser: { syntax: "typescript", decorators: true },
|
||||||
|
transform: { decoratorMetadata: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
testEnvironment: `node`,
|
||||||
|
moduleFileExtensions: [`js`, `ts`],
|
||||||
|
modulePathIgnorePatterns: ["dist/"],
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { defineMikroOrmCliConfig, Modules } from "@medusajs/utils"
|
||||||
|
import * as entities from "./src/models"
|
||||||
|
|
||||||
|
export default defineMikroOrmCliConfig(Modules.INDEX, {
|
||||||
|
entities: Object.values(entities),
|
||||||
|
})
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"name": "@medusajs/index",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Medusa Index module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/medusajs/medusa",
|
||||||
|
"directory": "packages/index"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"author": "Medusa",
|
||||||
|
"license": "MIT",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create --initial",
|
||||||
|
"migration:create": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create",
|
||||||
|
"migration:up": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:up",
|
||||||
|
"orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@medusajs/types": "^1.11.16",
|
||||||
|
"@mikro-orm/cli": "5.9.7",
|
||||||
|
"cross-env": "^5.2.1",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"medusa-test-utils": "^1.1.44",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tsc-alias": "^1.8.6",
|
||||||
|
"typescript": "^5.1.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@medusajs/utils": "^1.11.9"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@mikro-orm/core": "5.9.7",
|
||||||
|
"@mikro-orm/migrations": "5.9.7",
|
||||||
|
"@mikro-orm/postgresql": "5.9.7",
|
||||||
|
"awilix": "^8.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { IndexModuleService } from "@services"
|
||||||
|
import { Module, Modules } from "@medusajs/utils"
|
||||||
|
import containerLoader from "./loaders/index"
|
||||||
|
|
||||||
|
export default Module(Modules.INDEX, {
|
||||||
|
service: IndexModuleService,
|
||||||
|
loaders: [containerLoader],
|
||||||
|
})
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { asClass, asValue } from "awilix"
|
||||||
|
import { PostgresProvider } from "../services/postgres-provider"
|
||||||
|
import { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"
|
||||||
|
import { IndexModuleService } from "@services"
|
||||||
|
import { LoaderOptions } from "@medusajs/types"
|
||||||
|
|
||||||
|
export default async ({ container, options }: LoaderOptions): Promise<void> => {
|
||||||
|
container.register({
|
||||||
|
baseRepository: asClass(BaseRepository).singleton(),
|
||||||
|
searchModuleService: asClass(IndexModuleService).singleton(),
|
||||||
|
})
|
||||||
|
|
||||||
|
container.register("storageProviderCtrOptions", asValue(undefined))
|
||||||
|
|
||||||
|
container.register("storageProviderCtr", asValue(PostgresProvider))
|
||||||
|
|
||||||
|
/*if (!options?.customAdapter) {
|
||||||
|
container.register("storageProviderCtr", asValue(PostgresProvider))
|
||||||
|
} else {
|
||||||
|
container.register(
|
||||||
|
"storageProviderCtr",
|
||||||
|
asValue(options.customAdapter.constructor)
|
||||||
|
)
|
||||||
|
container.register(
|
||||||
|
"storageProviderCtrOptions",
|
||||||
|
asValue(options.customAdapter.options)
|
||||||
|
)
|
||||||
|
}*/
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
{
|
||||||
|
"namespaces": ["public"],
|
||||||
|
"name": "public",
|
||||||
|
"tables": [
|
||||||
|
{
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"mappedType": "text"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"mappedType": "text"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"name": "data",
|
||||||
|
"type": "jsonb",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"default": "'{}'",
|
||||||
|
"mappedType": "json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "index_data",
|
||||||
|
"schema": "public",
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"columnNames": ["id"],
|
||||||
|
"composite": false,
|
||||||
|
"keyName": "IDX_index_data_id",
|
||||||
|
"primary": false,
|
||||||
|
"unique": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columnNames": ["name"],
|
||||||
|
"composite": false,
|
||||||
|
"keyName": "IDX_index_data_name",
|
||||||
|
"primary": false,
|
||||||
|
"unique": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keyName": "IDX_index_data_gin",
|
||||||
|
"columnNames": ["data"],
|
||||||
|
"composite": false,
|
||||||
|
"primary": false,
|
||||||
|
"unique": false,
|
||||||
|
"type": "GIN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keyName": "index_data_pkey",
|
||||||
|
"columnNames": ["id", "name"],
|
||||||
|
"composite": true,
|
||||||
|
"primary": true,
|
||||||
|
"unique": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"checks": [],
|
||||||
|
"foreignKeys": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"unsigned": true,
|
||||||
|
"autoincrement": true,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"mappedType": "text"
|
||||||
|
},
|
||||||
|
"pivot": {
|
||||||
|
"name": "pivot",
|
||||||
|
"type": "text",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"mappedType": "text"
|
||||||
|
},
|
||||||
|
"parent_id": {
|
||||||
|
"name": "parent_id",
|
||||||
|
"type": "text",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"mappedType": "text"
|
||||||
|
},
|
||||||
|
"parent_name": {
|
||||||
|
"name": "parent_name",
|
||||||
|
"type": "text",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"mappedType": "text"
|
||||||
|
},
|
||||||
|
"child_id": {
|
||||||
|
"name": "child_id",
|
||||||
|
"type": "text",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"mappedType": "text"
|
||||||
|
},
|
||||||
|
"child_name": {
|
||||||
|
"name": "child_name",
|
||||||
|
"type": "text",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"mappedType": "text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "index_relation",
|
||||||
|
"schema": "public",
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"keyName": "IDX_index_relation_child_id",
|
||||||
|
"columnNames": ["child_id"],
|
||||||
|
"composite": false,
|
||||||
|
"primary": false,
|
||||||
|
"unique": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"keyName": "index_relation_pkey",
|
||||||
|
"columnNames": ["id", "pivot"],
|
||||||
|
"composite": true,
|
||||||
|
"primary": true,
|
||||||
|
"unique": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"checks": [],
|
||||||
|
"foreignKeys": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Migration } from "@mikro-orm/migrations"
|
||||||
|
|
||||||
|
export class Migration20231019174230 extends Migration {
|
||||||
|
async up(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
`create table "index_data" ("id" text not null, "name" text not null, "data" jsonb not null default '{}', constraint "index_data_pkey" primary key ("id", "name")) PARTITION BY LIST("name");`
|
||||||
|
)
|
||||||
|
this.addSql(`create index "IDX_index_data_id" on "index_data" ("id");`)
|
||||||
|
this.addSql(`create index "IDX_index_data_name" on "index_data" ("name");`)
|
||||||
|
this.addSql(
|
||||||
|
`create index "IDX_index_data_gin" on "index_data" using GIN ("data");`
|
||||||
|
)
|
||||||
|
|
||||||
|
this.addSql(
|
||||||
|
`create table "index_relation" ("id" bigserial, "pivot" text not null, "parent_id" text not null, "parent_name" text not null, "child_id" text not null, "child_name" text not null, constraint "index_relation_pkey" primary key ("id", "pivot")) PARTITION BY LIST("pivot");`
|
||||||
|
)
|
||||||
|
this.addSql(
|
||||||
|
`create index "IDX_index_relation_child_id" on "index_relation" ("child_id");`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
Cascade,
|
||||||
|
Collection,
|
||||||
|
Entity,
|
||||||
|
Index,
|
||||||
|
ManyToMany,
|
||||||
|
OptionalProps,
|
||||||
|
PrimaryKey,
|
||||||
|
PrimaryKeyType,
|
||||||
|
Property,
|
||||||
|
} from "@mikro-orm/core"
|
||||||
|
import { IndexRelation } from "./index-relation"
|
||||||
|
|
||||||
|
type OptionalRelations = "parents"
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
tableName: "index_data",
|
||||||
|
})
|
||||||
|
export class IndexData {
|
||||||
|
[OptionalProps]: OptionalRelations
|
||||||
|
|
||||||
|
@PrimaryKey({ columnType: "text" })
|
||||||
|
@Index({ name: "IDX_index_data_id" })
|
||||||
|
id!: string
|
||||||
|
|
||||||
|
@PrimaryKey({ columnType: "text" })
|
||||||
|
@Index({ name: "IDX_index_data_name" })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
[PrimaryKeyType]?: [string, string]
|
||||||
|
|
||||||
|
@Index({ name: "IDX_index_data_gin", type: "GIN" })
|
||||||
|
@Property({ columnType: "jsonb", default: "{}" })
|
||||||
|
data: Record<string, unknown>
|
||||||
|
|
||||||
|
@ManyToMany({
|
||||||
|
owner: true,
|
||||||
|
entity: () => IndexData,
|
||||||
|
pivotEntity: () => IndexRelation,
|
||||||
|
cascade: [Cascade.REMOVE],
|
||||||
|
inverseJoinColumns: ["parent_id", "parent_name"],
|
||||||
|
joinColumns: ["child_id", "child_name"],
|
||||||
|
})
|
||||||
|
parents = new Collection<IndexData>(this)
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
Index,
|
||||||
|
ManyToOne,
|
||||||
|
OptionalProps,
|
||||||
|
PrimaryKey,
|
||||||
|
Property,
|
||||||
|
Ref,
|
||||||
|
} from "@mikro-orm/core"
|
||||||
|
import { IndexData } from "./index-data"
|
||||||
|
|
||||||
|
type OptionalRelations =
|
||||||
|
| "parent"
|
||||||
|
| "child"
|
||||||
|
| "parent_id"
|
||||||
|
| "child_id"
|
||||||
|
| "parent_name"
|
||||||
|
| "child_name"
|
||||||
|
|
||||||
|
@Entity({
|
||||||
|
tableName: "index_relation",
|
||||||
|
})
|
||||||
|
@Index({
|
||||||
|
name: "IDX_index_relation_child_id",
|
||||||
|
properties: ["child_id"],
|
||||||
|
})
|
||||||
|
export class IndexRelation {
|
||||||
|
[OptionalProps]: OptionalRelations
|
||||||
|
|
||||||
|
@PrimaryKey({ columnType: "integer", autoincrement: true })
|
||||||
|
id!: string
|
||||||
|
|
||||||
|
// if added as PK, BeforeCreate value isn't set
|
||||||
|
@Property({
|
||||||
|
columnType: "text",
|
||||||
|
})
|
||||||
|
pivot: string
|
||||||
|
|
||||||
|
@Property({ columnType: "text" })
|
||||||
|
parent_id?: string
|
||||||
|
|
||||||
|
@Property({ columnType: "text" })
|
||||||
|
parent_name?: string
|
||||||
|
|
||||||
|
@Property({ columnType: "text" })
|
||||||
|
child_id?: string
|
||||||
|
|
||||||
|
@Property({ columnType: "text" })
|
||||||
|
child_name?: string
|
||||||
|
|
||||||
|
@ManyToOne({
|
||||||
|
entity: () => IndexData,
|
||||||
|
onDelete: "cascade",
|
||||||
|
persist: false,
|
||||||
|
})
|
||||||
|
parent?: Ref<IndexData>
|
||||||
|
|
||||||
|
@ManyToOne({
|
||||||
|
entity: () => IndexData,
|
||||||
|
onDelete: "cascade",
|
||||||
|
persist: false,
|
||||||
|
})
|
||||||
|
child?: Ref<IndexData>
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { IndexData } from "./index-data"
|
||||||
|
export { IndexRelation } from "./index-relation"
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import {
|
||||||
|
IEventBusModuleService,
|
||||||
|
IndexTypes,
|
||||||
|
InternalModuleDeclaration,
|
||||||
|
RemoteQueryFunction,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
MikroOrmBaseRepository as BaseRepository,
|
||||||
|
Modules,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
IndexModuleOptions,
|
||||||
|
SchemaObjectRepresentation,
|
||||||
|
schemaObjectRepresentationPropertiesToOmit,
|
||||||
|
StorageProvider,
|
||||||
|
} from "@types"
|
||||||
|
import { buildSchemaObjectRepresentation } from "../utils/build-config"
|
||||||
|
import { defaultSchema } from "../utils/default-schema"
|
||||||
|
|
||||||
|
type InjectedDependencies = {
|
||||||
|
[Modules.EVENT_BUS]: IEventBusModuleService
|
||||||
|
storageProviderCtr: StorageProvider
|
||||||
|
storageProviderCtrOptions: unknown
|
||||||
|
[ContainerRegistrationKeys.REMOTE_QUERY]: RemoteQueryFunction
|
||||||
|
baseRepository: BaseRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class IndexModuleService implements IndexTypes.IIndexService {
|
||||||
|
private readonly container_: InjectedDependencies
|
||||||
|
private readonly moduleOptions_: IndexModuleOptions
|
||||||
|
|
||||||
|
protected readonly eventBusModuleService_: IEventBusModuleService
|
||||||
|
|
||||||
|
protected schemaObjectRepresentation_: SchemaObjectRepresentation
|
||||||
|
protected schemaEntitiesMap_: Record<string, any>
|
||||||
|
|
||||||
|
protected readonly storageProviderCtr_: StorageProvider
|
||||||
|
protected readonly storageProviderCtrOptions_: unknown
|
||||||
|
|
||||||
|
protected storageProvider_: StorageProvider
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
container: InjectedDependencies,
|
||||||
|
protected readonly moduleDeclaration: InternalModuleDeclaration
|
||||||
|
) {
|
||||||
|
this.container_ = container
|
||||||
|
this.moduleOptions_ = (moduleDeclaration.options ??
|
||||||
|
moduleDeclaration) as unknown as IndexModuleOptions
|
||||||
|
|
||||||
|
const {
|
||||||
|
[Modules.EVENT_BUS]: eventBusModuleService,
|
||||||
|
storageProviderCtr,
|
||||||
|
storageProviderCtrOptions,
|
||||||
|
} = container
|
||||||
|
|
||||||
|
this.eventBusModuleService_ = eventBusModuleService
|
||||||
|
this.storageProviderCtr_ = storageProviderCtr
|
||||||
|
this.storageProviderCtrOptions_ = storageProviderCtrOptions
|
||||||
|
|
||||||
|
if (!this.eventBusModuleService_) {
|
||||||
|
throw new Error(
|
||||||
|
"EventBusModuleService is required for the IndexModule to work"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__joinerConfig() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
__hooks = {
|
||||||
|
onApplicationStart(this: IndexModuleService) {
|
||||||
|
return this.onApplicationStart_()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onApplicationStart_() {
|
||||||
|
try {
|
||||||
|
this.buildSchemaObjectRepresentation_()
|
||||||
|
|
||||||
|
this.storageProvider_ = new this.storageProviderCtr_(
|
||||||
|
this.container_,
|
||||||
|
Object.assign(this.storageProviderCtrOptions_ ?? {}, {
|
||||||
|
schemaObjectRepresentation: this.schemaObjectRepresentation_,
|
||||||
|
entityMap: this.schemaEntitiesMap_,
|
||||||
|
}),
|
||||||
|
this.moduleOptions_
|
||||||
|
)
|
||||||
|
|
||||||
|
this.registerListeners()
|
||||||
|
|
||||||
|
if (this.storageProvider_.onApplicationStart) {
|
||||||
|
await this.storageProvider_.onApplicationStart()
|
||||||
|
}
|
||||||
|
} 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected registerListeners() {
|
||||||
|
const schemaObjectRepresentation = this.schemaObjectRepresentation_ ?? {}
|
||||||
|
|
||||||
|
for (const [entityName, schemaEntityObjectRepresentation] of Object.entries(
|
||||||
|
schemaObjectRepresentation
|
||||||
|
)) {
|
||||||
|
if (schemaObjectRepresentationPropertiesToOmit.includes(entityName)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaEntityObjectRepresentation.listeners.forEach((listener) => {
|
||||||
|
this.eventBusModuleService_.subscribe(
|
||||||
|
listener,
|
||||||
|
this.storageProvider_.consumeEvent(schemaEntityObjectRepresentation)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildSchemaObjectRepresentation_() {
|
||||||
|
if (this.schemaObjectRepresentation_) {
|
||||||
|
return this.schemaObjectRepresentation_
|
||||||
|
}
|
||||||
|
|
||||||
|
const [objectRepresentation, entityMap] = buildSchemaObjectRepresentation(
|
||||||
|
this.moduleOptions_.schema ?? defaultSchema
|
||||||
|
)
|
||||||
|
this.schemaObjectRepresentation_ = objectRepresentation
|
||||||
|
this.schemaEntitiesMap_ = entityMap
|
||||||
|
|
||||||
|
return this.schemaObjectRepresentation_
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as IndexModuleService } from "./index-module-service"
|
||||||
@@ -0,0 +1,726 @@
|
|||||||
|
import {
|
||||||
|
Context,
|
||||||
|
Event,
|
||||||
|
RemoteQueryFunction,
|
||||||
|
Subscriber,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
InjectManager,
|
||||||
|
InjectTransactionManager,
|
||||||
|
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"
|
||||||
|
|
||||||
|
type InjectedDependencies = {
|
||||||
|
manager: EntityManager
|
||||||
|
[ContainerRegistrationKeys.REMOTE_QUERY]: RemoteQueryFunction
|
||||||
|
baseRepository: BaseRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PostgresProvider {
|
||||||
|
#isReady_: Promise<boolean>
|
||||||
|
|
||||||
|
protected readonly eventActionToMethodMap_ = {
|
||||||
|
created: "onCreate",
|
||||||
|
updated: "onUpdate",
|
||||||
|
deleted: "onDelete",
|
||||||
|
attached: "onAttach",
|
||||||
|
detached: "onDetach",
|
||||||
|
}
|
||||||
|
|
||||||
|
protected container_: InjectedDependencies
|
||||||
|
protected readonly schemaObjectRepresentation_: SchemaObjectRepresentation
|
||||||
|
protected readonly schemaEntitiesMap_: Record<string, any>
|
||||||
|
protected readonly moduleOptions_: IndexModuleOptions
|
||||||
|
protected readonly manager_: SqlEntityManager
|
||||||
|
protected readonly remoteQuery_: RemoteQueryFunction
|
||||||
|
protected baseRepository_: BaseRepository
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
{
|
||||||
|
manager,
|
||||||
|
[ContainerRegistrationKeys.REMOTE_QUERY]: remoteQuery,
|
||||||
|
baseRepository,
|
||||||
|
}: InjectedDependencies,
|
||||||
|
options: {
|
||||||
|
schemaObjectRepresentation: SchemaObjectRepresentation
|
||||||
|
entityMap: Record<string, any>
|
||||||
|
},
|
||||||
|
moduleOptions: IndexModuleOptions
|
||||||
|
) {
|
||||||
|
this.manager_ = manager
|
||||||
|
this.remoteQuery_ = remoteQuery
|
||||||
|
this.moduleOptions_ = moduleOptions
|
||||||
|
this.baseRepository_ = baseRepository
|
||||||
|
|
||||||
|
this.schemaObjectRepresentation_ = options.schemaObjectRepresentation
|
||||||
|
this.schemaEntitiesMap_ = options.entityMap
|
||||||
|
|
||||||
|
// Add a new column for each key that can be found in the jsonb data column to perform indexes and query on it.
|
||||||
|
// So far, the execution time is about the same
|
||||||
|
/*;(async () => {
|
||||||
|
const query = [
|
||||||
|
...new Set(
|
||||||
|
Object.keys(this.schemaObjectRepresentation_)
|
||||||
|
.filter(
|
||||||
|
(key) =>
|
||||||
|
![
|
||||||
|
"_serviceNameModuleConfigMap",
|
||||||
|
"_schemaPropertiesMap",
|
||||||
|
].includes(key)
|
||||||
|
)
|
||||||
|
.map((key) => {
|
||||||
|
return this.schemaObjectRepresentation_[key].fields.filter(
|
||||||
|
(field) => !field.includes(".")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.flat()
|
||||||
|
),
|
||||||
|
].map(
|
||||||
|
(field) =>
|
||||||
|
"ALTER TABLE index_data ADD IF NOT EXISTS " +
|
||||||
|
field +
|
||||||
|
" text GENERATED ALWAYS AS (NEW.data->>'" +
|
||||||
|
field +
|
||||||
|
"') STORED"
|
||||||
|
)
|
||||||
|
await this.manager_.execute(query.join(";"))
|
||||||
|
})()*/
|
||||||
|
}
|
||||||
|
|
||||||
|
async onApplicationStart() {
|
||||||
|
let initalizedOk: (value: any) => void = () => {}
|
||||||
|
let initalizedNok: (value: any) => void = () => {}
|
||||||
|
this.#isReady_ = new Promise((resolve, reject) => {
|
||||||
|
initalizedOk = resolve
|
||||||
|
initalizedNok = reject
|
||||||
|
})
|
||||||
|
|
||||||
|
await createPartitions(
|
||||||
|
this.schemaObjectRepresentation_,
|
||||||
|
this.manager_.fork()
|
||||||
|
)
|
||||||
|
.then(initalizedOk)
|
||||||
|
.catch(initalizedNok)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static parseData<
|
||||||
|
TData extends { id: string; [key: string]: unknown }
|
||||||
|
>(
|
||||||
|
data: TData | TData[],
|
||||||
|
schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation
|
||||||
|
) {
|
||||||
|
const data_ = Array.isArray(data) ? data : [data]
|
||||||
|
|
||||||
|
// Always keep the id in the entity properties
|
||||||
|
const entityProperties: string[] = ["id"]
|
||||||
|
const parentsProperties: { [entity: string]: string[] } = {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split fields into entity properties and parents properties
|
||||||
|
*/
|
||||||
|
|
||||||
|
schemaEntityObjectRepresentation.fields.forEach((field) => {
|
||||||
|
if (field.includes(".")) {
|
||||||
|
const parentAlias = field.split(".")[0]
|
||||||
|
const parentSchemaObjectRepresentation =
|
||||||
|
schemaEntityObjectRepresentation.parents.find(
|
||||||
|
(parent) => parent.ref.alias === parentAlias
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!parentSchemaObjectRepresentation) {
|
||||||
|
throw new Error(
|
||||||
|
`IndexModule error, unable to parse data for ${schemaEntityObjectRepresentation.entity}. The parent schema object representation could not be found for the alias ${parentAlias} for the entity ${schemaEntityObjectRepresentation.entity}.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
parentsProperties[parentSchemaObjectRepresentation.ref.entity] ??= []
|
||||||
|
parentsProperties[parentSchemaObjectRepresentation.ref.entity].push(
|
||||||
|
field
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
entityProperties.push(field)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data_,
|
||||||
|
entityProperties,
|
||||||
|
parentsProperties,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static parseMessageData<T>(message?: Event): {
|
||||||
|
action: string
|
||||||
|
data: { id: string }[]
|
||||||
|
ids: string[]
|
||||||
|
} | void {
|
||||||
|
const isExpectedFormat =
|
||||||
|
isDefined(message?.data) && isDefined(message?.metadata?.action)
|
||||||
|
|
||||||
|
if (!isExpectedFormat) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: {
|
||||||
|
action: string
|
||||||
|
data: { id: string }[]
|
||||||
|
ids: string[]
|
||||||
|
} = {
|
||||||
|
action: "",
|
||||||
|
data: [],
|
||||||
|
ids: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
result.action = message!.metadata!.action as string
|
||||||
|
result.data = message!.data as { id: string }[]
|
||||||
|
result.data = Array.isArray(result.data) ? result.data : [result.data]
|
||||||
|
result.ids = result.data.map((d) => d.id)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@InjectManager("baseRepository_")
|
||||||
|
async query(
|
||||||
|
selection: QueryFormat,
|
||||||
|
options?: QueryOptions,
|
||||||
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
) {
|
||||||
|
await this.#isReady_
|
||||||
|
|
||||||
|
const { manager } = sharedContext as { manager: SqlEntityManager }
|
||||||
|
let hasPagination = false
|
||||||
|
if (
|
||||||
|
typeof options?.take === "number" ||
|
||||||
|
typeof options?.skip === "number"
|
||||||
|
) {
|
||||||
|
hasPagination = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = manager.getConnection()
|
||||||
|
const qb = new QueryBuilder({
|
||||||
|
schema: this.schemaObjectRepresentation_,
|
||||||
|
entityMap: this.schemaEntitiesMap_,
|
||||||
|
knex: connection.getKnex(),
|
||||||
|
selector: selection,
|
||||||
|
options,
|
||||||
|
})
|
||||||
|
|
||||||
|
const sql = qb.buildQuery(hasPagination, !!options?.keepFilteredEntities)
|
||||||
|
|
||||||
|
let resultset = await manager.execute(sql)
|
||||||
|
|
||||||
|
if (options?.keepFilteredEntities) {
|
||||||
|
const mainEntity = Object.keys(selection.select)[0]
|
||||||
|
|
||||||
|
const ids = resultset.map((r) => r[`${mainEntity}.id`])
|
||||||
|
if (ids.length) {
|
||||||
|
const selection_ = {
|
||||||
|
select: selection.select,
|
||||||
|
joinWhere: selection.joinWhere,
|
||||||
|
where: {
|
||||||
|
[`${mainEntity}.id`]: ids,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return await this.query(selection_, undefined, sharedContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return qb.buildObjectFromResultset(resultset)
|
||||||
|
}
|
||||||
|
|
||||||
|
@InjectManager("baseRepository_")
|
||||||
|
async queryAndCount(
|
||||||
|
selection: QueryFormat,
|
||||||
|
options?: QueryOptions,
|
||||||
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
): Promise<[Record<string, any>[], number, PerformanceEntry]> {
|
||||||
|
await this.#isReady_
|
||||||
|
|
||||||
|
const { manager } = sharedContext as { manager: SqlEntityManager }
|
||||||
|
const connection = manager.getConnection()
|
||||||
|
const qb = new QueryBuilder({
|
||||||
|
schema: this.schemaObjectRepresentation_,
|
||||||
|
entityMap: this.schemaEntitiesMap_,
|
||||||
|
knex: connection.getKnex(),
|
||||||
|
selector: selection,
|
||||||
|
options,
|
||||||
|
})
|
||||||
|
|
||||||
|
const sql = qb.buildQuery(true, !!options?.keepFilteredEntities)
|
||||||
|
performance.mark("index-query-start")
|
||||||
|
let resultset = await connection.execute(sql)
|
||||||
|
performance.mark("index-query-end")
|
||||||
|
|
||||||
|
const performanceMesurements = performance.measure(
|
||||||
|
"index-query-end",
|
||||||
|
"index-query-start"
|
||||||
|
)
|
||||||
|
|
||||||
|
const count = +(resultset[0]?.count ?? 0)
|
||||||
|
|
||||||
|
if (options?.keepFilteredEntities) {
|
||||||
|
const mainEntity = Object.keys(selection.select)[0]
|
||||||
|
|
||||||
|
const ids = resultset.map((r) => r[`${mainEntity}.id`])
|
||||||
|
if (ids.length) {
|
||||||
|
const selection_ = {
|
||||||
|
select: selection.select,
|
||||||
|
joinWhere: selection.joinWhere,
|
||||||
|
where: {
|
||||||
|
[`${mainEntity}.id`]: ids,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
performance.mark("index-query-start")
|
||||||
|
resultset = await this.query(selection_, undefined, sharedContext)
|
||||||
|
performance.mark("index-query-end")
|
||||||
|
|
||||||
|
const performanceMesurements = performance.measure(
|
||||||
|
"index-query-end",
|
||||||
|
"index-query-start"
|
||||||
|
)
|
||||||
|
|
||||||
|
return [resultset, count, performanceMesurements]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
qb.buildObjectFromResultset(resultset),
|
||||||
|
count,
|
||||||
|
performanceMesurements,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeEvent(
|
||||||
|
schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation
|
||||||
|
): Subscriber<{ id: string }> {
|
||||||
|
return async (data: Event) => {
|
||||||
|
await this.#isReady_
|
||||||
|
|
||||||
|
const data_: { id: string }[] = Array.isArray(data.data)
|
||||||
|
? data.data
|
||||||
|
: [data.data]
|
||||||
|
let ids: string[] = data_.map((d) => d.id)
|
||||||
|
let action = data.name.split(".").pop() || ""
|
||||||
|
|
||||||
|
const parsedMessage = PostgresProvider.parseMessageData(data)
|
||||||
|
if (parsedMessage) {
|
||||||
|
action = parsedMessage.action
|
||||||
|
ids = parsedMessage.ids
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fields, alias } = schemaEntityObjectRepresentation
|
||||||
|
const entityData = await this.remoteQuery_(
|
||||||
|
remoteQueryObjectFromString({
|
||||||
|
entryPoint: alias,
|
||||||
|
variables: {
|
||||||
|
filters: {
|
||||||
|
id: ids,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fields: [...new Set(["id", ...fields])],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const argument = {
|
||||||
|
entity: schemaEntityObjectRepresentation.entity,
|
||||||
|
data: entityData,
|
||||||
|
schemaEntityObjectRepresentation,
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetMethod = this.eventActionToMethodMap_[action]
|
||||||
|
|
||||||
|
if (!targetMethod) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await this[targetMethod](argument)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the index entry and the index relation entry when this event is emitted.
|
||||||
|
* @param entity
|
||||||
|
* @param data
|
||||||
|
* @param schemaEntityObjectRepresentation
|
||||||
|
* @param sharedContext
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
@InjectTransactionManager("baseRepository_")
|
||||||
|
protected async onCreate<
|
||||||
|
TData extends { id: string; [key: string]: unknown }
|
||||||
|
>(
|
||||||
|
{
|
||||||
|
entity,
|
||||||
|
data,
|
||||||
|
schemaEntityObjectRepresentation,
|
||||||
|
}: {
|
||||||
|
entity: string
|
||||||
|
data: TData | TData[]
|
||||||
|
schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation
|
||||||
|
},
|
||||||
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
) {
|
||||||
|
const { transactionManager: em } = sharedContext as {
|
||||||
|
transactionManager: SqlEntityManager
|
||||||
|
}
|
||||||
|
const indexRepository = em.getRepository(IndexData)
|
||||||
|
const indexRelationRepository = em.getRepository(IndexRelation)
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: data_,
|
||||||
|
entityProperties,
|
||||||
|
parentsProperties,
|
||||||
|
} = PostgresProvider.parseData(data, schemaEntityObjectRepresentation)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loop through the data and create index entries for each entity as well as the
|
||||||
|
* index relation entries if the entity has parents
|
||||||
|
*/
|
||||||
|
|
||||||
|
for (const entityData of data_) {
|
||||||
|
/**
|
||||||
|
* Clean the entity data to only keep the properties that are defined in the schema
|
||||||
|
*/
|
||||||
|
|
||||||
|
const cleanedEntityData = entityProperties.reduce((acc, property) => {
|
||||||
|
acc[property] = entityData[property]
|
||||||
|
return acc
|
||||||
|
}, {}) as TData
|
||||||
|
|
||||||
|
await indexRepository.upsert({
|
||||||
|
id: cleanedEntityData.id,
|
||||||
|
name: entity,
|
||||||
|
data: cleanedEntityData,
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the parents to attach it to the index entry.
|
||||||
|
*/
|
||||||
|
|
||||||
|
for (const [parentEntity, parentProperties] of Object.entries(
|
||||||
|
parentsProperties
|
||||||
|
)) {
|
||||||
|
const parentAlias = parentProperties[0].split(".")[0]
|
||||||
|
const parentData = entityData[parentAlias] as TData[]
|
||||||
|
|
||||||
|
if (!parentData) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentDataCollection = Array.isArray(parentData)
|
||||||
|
? parentData
|
||||||
|
: [parentData]
|
||||||
|
|
||||||
|
for (const parentData_ of parentDataCollection) {
|
||||||
|
await indexRepository.upsert({
|
||||||
|
id: (parentData_ as any).id,
|
||||||
|
name: parentEntity,
|
||||||
|
data: parentData_,
|
||||||
|
})
|
||||||
|
|
||||||
|
const parentIndexRelationEntry = indexRelationRepository.create({
|
||||||
|
parent_id: (parentData_ as any).id,
|
||||||
|
parent_name: parentEntity,
|
||||||
|
child_id: cleanedEntityData.id,
|
||||||
|
child_name: entity,
|
||||||
|
pivot: `${parentEntity}-${entity}`,
|
||||||
|
})
|
||||||
|
indexRelationRepository.persist(parentIndexRelationEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the index entry when this event is emitted.
|
||||||
|
* @param entity
|
||||||
|
* @param data
|
||||||
|
* @param schemaEntityObjectRepresentation
|
||||||
|
* @param sharedContext
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
@InjectTransactionManager("baseRepository_")
|
||||||
|
protected async onUpdate<
|
||||||
|
TData extends { id: string; [key: string]: unknown }
|
||||||
|
>(
|
||||||
|
{
|
||||||
|
entity,
|
||||||
|
data,
|
||||||
|
schemaEntityObjectRepresentation,
|
||||||
|
}: {
|
||||||
|
entity: string
|
||||||
|
data: TData | TData[]
|
||||||
|
schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation
|
||||||
|
},
|
||||||
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
) {
|
||||||
|
const { transactionManager: em } = sharedContext as {
|
||||||
|
transactionManager: SqlEntityManager
|
||||||
|
}
|
||||||
|
const indexRepository = em.getRepository(IndexData)
|
||||||
|
|
||||||
|
const { data: data_, entityProperties } = PostgresProvider.parseData(
|
||||||
|
data,
|
||||||
|
schemaEntityObjectRepresentation
|
||||||
|
)
|
||||||
|
|
||||||
|
await indexRepository.upsertMany(
|
||||||
|
data_.map((entityData) => {
|
||||||
|
return {
|
||||||
|
id: entityData.id,
|
||||||
|
name: entity,
|
||||||
|
data: entityProperties.reduce((acc, property) => {
|
||||||
|
acc[property] = entityData[property]
|
||||||
|
return acc
|
||||||
|
}, {}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the index entry when this event is emitted.
|
||||||
|
* @param entity
|
||||||
|
* @param data
|
||||||
|
* @param schemaEntityObjectRepresentation
|
||||||
|
* @param sharedContext
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
@InjectTransactionManager("baseRepository_")
|
||||||
|
protected async onDelete<
|
||||||
|
TData extends { id: string; [key: string]: unknown }
|
||||||
|
>(
|
||||||
|
{
|
||||||
|
entity,
|
||||||
|
data,
|
||||||
|
schemaEntityObjectRepresentation,
|
||||||
|
}: {
|
||||||
|
entity: string
|
||||||
|
data: TData | TData[]
|
||||||
|
schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation
|
||||||
|
},
|
||||||
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
) {
|
||||||
|
const { transactionManager: em } = sharedContext as {
|
||||||
|
transactionManager: SqlEntityManager
|
||||||
|
}
|
||||||
|
const indexRepository = em.getRepository(IndexData)
|
||||||
|
const indexRelationRepository = em.getRepository(IndexRelation)
|
||||||
|
|
||||||
|
const { data: data_ } = PostgresProvider.parseData(
|
||||||
|
data,
|
||||||
|
schemaEntityObjectRepresentation
|
||||||
|
)
|
||||||
|
|
||||||
|
const ids = data_.map((entityData) => entityData.id)
|
||||||
|
|
||||||
|
await indexRepository.nativeDelete({
|
||||||
|
id: { $in: ids },
|
||||||
|
name: entity,
|
||||||
|
})
|
||||||
|
|
||||||
|
await indexRelationRepository.nativeDelete({
|
||||||
|
$or: [
|
||||||
|
{
|
||||||
|
parent_id: { $in: ids },
|
||||||
|
parent_name: entity,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
child_id: { $in: ids },
|
||||||
|
child_name: entity,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* event emitted from the link modules to attach a link entity to its parent and child entities from the linked modules.
|
||||||
|
* @param entity
|
||||||
|
* @param data
|
||||||
|
* @param schemaEntityObjectRepresentation
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
@InjectTransactionManager("baseRepository_")
|
||||||
|
protected async onAttach<
|
||||||
|
TData extends { id: string; [key: string]: unknown }
|
||||||
|
>(
|
||||||
|
{
|
||||||
|
entity,
|
||||||
|
data,
|
||||||
|
schemaEntityObjectRepresentation,
|
||||||
|
}: {
|
||||||
|
entity: string
|
||||||
|
data: TData | TData[]
|
||||||
|
schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation
|
||||||
|
},
|
||||||
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
) {
|
||||||
|
const { transactionManager: em } = sharedContext as {
|
||||||
|
transactionManager: SqlEntityManager
|
||||||
|
}
|
||||||
|
const indexRepository = em.getRepository(IndexData)
|
||||||
|
const indexRelationRepository = em.getRepository(IndexRelation)
|
||||||
|
|
||||||
|
const { data: data_, entityProperties } = PostgresProvider.parseData(
|
||||||
|
data,
|
||||||
|
schemaEntityObjectRepresentation
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the property that represent the foreign key related to the parent entity of the link entity.
|
||||||
|
* Then from the service name of the parent entity, retrieve the entity name using the linkable keys.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const parentPropertyId =
|
||||||
|
schemaEntityObjectRepresentation.moduleConfig.relationships![0].foreignKey
|
||||||
|
const parentServiceName =
|
||||||
|
schemaEntityObjectRepresentation.moduleConfig.relationships![0]
|
||||||
|
.serviceName
|
||||||
|
const parentEntityName = (
|
||||||
|
this.schemaObjectRepresentation_._serviceNameModuleConfigMap[
|
||||||
|
parentServiceName
|
||||||
|
] as EntityNameModuleConfigMap[0]
|
||||||
|
).linkableKeys?.[parentPropertyId]
|
||||||
|
|
||||||
|
if (!parentEntityName) {
|
||||||
|
throw new Error(
|
||||||
|
`IndexModule error, unable to handle attach event for ${entity}. The parent entity name could not be found using the linkable keys from the module ${parentServiceName}.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the property that represent the foreign key related to the child entity of the link entity.
|
||||||
|
* Then from the service name of the child entity, retrieve the entity name using the linkable keys.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const childPropertyId =
|
||||||
|
schemaEntityObjectRepresentation.moduleConfig.relationships![1].foreignKey
|
||||||
|
const childServiceName =
|
||||||
|
schemaEntityObjectRepresentation.moduleConfig.relationships![1]
|
||||||
|
.serviceName
|
||||||
|
const childEntityName = (
|
||||||
|
this.schemaObjectRepresentation_._serviceNameModuleConfigMap[
|
||||||
|
childServiceName
|
||||||
|
] as EntityNameModuleConfigMap[0]
|
||||||
|
).linkableKeys?.[childPropertyId]
|
||||||
|
|
||||||
|
if (!childEntityName) {
|
||||||
|
throw new Error(
|
||||||
|
`IndexModule error, unable to handle attach event for ${entity}. The child entity name could not be found using the linkable keys from the module ${childServiceName}.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entityData of data_) {
|
||||||
|
/**
|
||||||
|
* Clean the link entity data to only keep the properties that are defined in the schema
|
||||||
|
*/
|
||||||
|
|
||||||
|
const cleanedEntityData = entityProperties.reduce((acc, property) => {
|
||||||
|
acc[property] = entityData[property]
|
||||||
|
return acc
|
||||||
|
}, {}) as TData
|
||||||
|
|
||||||
|
await indexRepository.upsert({
|
||||||
|
id: cleanedEntityData.id,
|
||||||
|
name: entity,
|
||||||
|
data: cleanedEntityData,
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the index relation entries for the parent entity and the child entity
|
||||||
|
*/
|
||||||
|
|
||||||
|
const parentIndexRelationEntry = indexRelationRepository.create({
|
||||||
|
parent_id: entityData[parentPropertyId] as string,
|
||||||
|
parent_name: parentEntityName,
|
||||||
|
child_id: cleanedEntityData.id,
|
||||||
|
child_name: entity,
|
||||||
|
pivot: `${parentEntityName}-${entity}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
const childIndexRelationEntry = indexRelationRepository.create({
|
||||||
|
parent_id: cleanedEntityData.id,
|
||||||
|
parent_name: entity,
|
||||||
|
child_id: entityData[childPropertyId] as string,
|
||||||
|
child_name: childEntityName,
|
||||||
|
pivot: `${entity}-${childEntityName}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
indexRelationRepository.persist([
|
||||||
|
parentIndexRelationEntry,
|
||||||
|
childIndexRelationEntry,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitted from the link modules to detach a link entity from its parent and child entities from the linked modules.
|
||||||
|
* @param entity
|
||||||
|
* @param data
|
||||||
|
* @param schemaEntityObjectRepresentation
|
||||||
|
* @param sharedContext
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
@InjectTransactionManager("baseRepository_")
|
||||||
|
protected async onDetach<
|
||||||
|
TData extends { id: string; [key: string]: unknown }
|
||||||
|
>(
|
||||||
|
{
|
||||||
|
entity,
|
||||||
|
data,
|
||||||
|
schemaEntityObjectRepresentation,
|
||||||
|
}: {
|
||||||
|
entity: string
|
||||||
|
data: TData | TData[]
|
||||||
|
schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation
|
||||||
|
},
|
||||||
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
) {
|
||||||
|
const { transactionManager: em } = sharedContext as {
|
||||||
|
transactionManager: SqlEntityManager
|
||||||
|
}
|
||||||
|
const indexRepository = em.getRepository(IndexData)
|
||||||
|
const indexRelationRepository = em.getRepository(IndexRelation)
|
||||||
|
|
||||||
|
const { data: data_ } = PostgresProvider.parseData(
|
||||||
|
data,
|
||||||
|
schemaEntityObjectRepresentation
|
||||||
|
)
|
||||||
|
|
||||||
|
const ids = data_.map((entityData) => entityData.id)
|
||||||
|
|
||||||
|
await indexRepository.nativeDelete({
|
||||||
|
id: { $in: ids },
|
||||||
|
name: entity,
|
||||||
|
})
|
||||||
|
|
||||||
|
await indexRelationRepository.nativeDelete({
|
||||||
|
$or: [
|
||||||
|
{
|
||||||
|
parent_id: { $in: ids },
|
||||||
|
parent_name: entity,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
child_id: { $in: ids },
|
||||||
|
child_name: entity,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import {
|
||||||
|
ModuleJoinerConfig,
|
||||||
|
ModulesSdkTypes,
|
||||||
|
Subscriber,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the module options that can be provided
|
||||||
|
*/
|
||||||
|
export interface IndexModuleOptions {
|
||||||
|
customAdapter?: {
|
||||||
|
constructor: new (...args: any[]) => any
|
||||||
|
options: any
|
||||||
|
}
|
||||||
|
defaultAdapterOptions?: ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||||
|
schema: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SchemaObjectEntityRepresentation = {
|
||||||
|
/**
|
||||||
|
* The name of the type/entity in the schema
|
||||||
|
*/
|
||||||
|
entity: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All parents a type/entity refers to in the schema
|
||||||
|
* or through links
|
||||||
|
*/
|
||||||
|
parents: {
|
||||||
|
/**
|
||||||
|
* The reference to the schema object representation
|
||||||
|
* of the parent
|
||||||
|
*/
|
||||||
|
ref: SchemaObjectEntityRepresentation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a link is inferred between two types/entities
|
||||||
|
* we are configuring the link tree, and therefore we are
|
||||||
|
* storing the reference to the parent type/entity within the
|
||||||
|
* schema which defer from the true parent from a pure entity
|
||||||
|
* point of view
|
||||||
|
*/
|
||||||
|
inSchemaRef?: SchemaObjectEntityRepresentation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The property the data should be assigned to in the parent
|
||||||
|
*/
|
||||||
|
targetProp: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Are the data expected to be a list or not
|
||||||
|
*/
|
||||||
|
isList?: boolean
|
||||||
|
}[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default fields to query for the type/entity
|
||||||
|
*/
|
||||||
|
fields: string[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Listerners directive is required and all listeners found
|
||||||
|
* for the type will be stored here
|
||||||
|
*/
|
||||||
|
listeners: string[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The alias for the type/entity retrieved in the corresponding
|
||||||
|
* module
|
||||||
|
*/
|
||||||
|
alias: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The module joiner config corresponding to the module the type/entity
|
||||||
|
* refers to
|
||||||
|
*/
|
||||||
|
moduleConfig: ModuleJoinerConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SchemaPropertiesMap = {
|
||||||
|
[key: string]: {
|
||||||
|
shortCutOf?: string
|
||||||
|
ref: SchemaObjectEntityRepresentation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EntityNameModuleConfigMap = {
|
||||||
|
[key: string]: ModuleJoinerConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the schema objects representation once the schema has been processed
|
||||||
|
*/
|
||||||
|
export type SchemaObjectRepresentation =
|
||||||
|
| {
|
||||||
|
[key: string]: SchemaObjectEntityRepresentation
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
_schemaPropertiesMap: SchemaPropertiesMap
|
||||||
|
_serviceNameModuleConfigMap: EntityNameModuleConfigMap
|
||||||
|
}
|
||||||
|
|
||||||
|
export const schemaObjectRepresentationPropertiesToOmit = [
|
||||||
|
"_schemaPropertiesMap",
|
||||||
|
"_serviceNameModuleConfigMap",
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the storage provider interface,
|
||||||
|
*/
|
||||||
|
export interface StorageProvider {
|
||||||
|
new (
|
||||||
|
container: { [key: string]: any },
|
||||||
|
storageProviderOptions: unknown & {
|
||||||
|
schemaObjectRepresentation: SchemaObjectRepresentation
|
||||||
|
},
|
||||||
|
moduleOptions: IndexModuleOptions
|
||||||
|
): StorageProvider
|
||||||
|
|
||||||
|
onApplicationStart?(): Promise<void>
|
||||||
|
|
||||||
|
query(...args): unknown
|
||||||
|
|
||||||
|
queryAndCount(...args): unknown
|
||||||
|
|
||||||
|
consumeEvent(
|
||||||
|
schemaEntityObjectRepresentation: SchemaObjectEntityRepresentation
|
||||||
|
): Subscriber
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Select = {
|
||||||
|
[key: string]: boolean | Select | Select[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Where = {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OrderBy = {
|
||||||
|
[path: string]: OrderBy | "ASC" | "DESC" | 1 | -1 | true | false
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QueryFormat = {
|
||||||
|
select: Select
|
||||||
|
where?: Where
|
||||||
|
joinWhere?: Where
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QueryOptions = {
|
||||||
|
skip?: number
|
||||||
|
take?: number
|
||||||
|
orderBy?: OrderBy | OrderBy[]
|
||||||
|
keepFilteredEntities?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preventing infinite depth
|
||||||
|
type ResultSetLimit = [never | 0 | 1 | 2]
|
||||||
|
|
||||||
|
export type Resultset<Select, Prev extends number = 3> = Prev extends never
|
||||||
|
? never
|
||||||
|
: {
|
||||||
|
[key in keyof Select]: Select[key] extends boolean
|
||||||
|
? string
|
||||||
|
: Select[key] extends Select[]
|
||||||
|
? Resultset<Select[key][0], ResultSetLimit[Prev]>[]
|
||||||
|
: Select[key] extends {}
|
||||||
|
? Resultset<Select[key], ResultSetLimit[Prev]>
|
||||||
|
: unknown
|
||||||
|
}
|
||||||
@@ -0,0 +1,734 @@
|
|||||||
|
import { makeExecutableSchema } from "@graphql-tools/schema"
|
||||||
|
import {
|
||||||
|
cleanGraphQLSchema,
|
||||||
|
gqlGetFieldsAndRelations,
|
||||||
|
MedusaModule,
|
||||||
|
} from "@medusajs/modules-sdk"
|
||||||
|
import {
|
||||||
|
JoinerServiceConfigAlias,
|
||||||
|
ModuleJoinerConfig,
|
||||||
|
ModuleJoinerRelationship,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import { CommonEvents } from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
SchemaObjectEntityRepresentation,
|
||||||
|
SchemaObjectRepresentation,
|
||||||
|
schemaObjectRepresentationPropertiesToOmit,
|
||||||
|
} from "@types"
|
||||||
|
import { Kind, ObjectTypeDefinitionNode } from "graphql/index"
|
||||||
|
|
||||||
|
export const CustomDirectives = {
|
||||||
|
Listeners: {
|
||||||
|
configurationPropertyName: "listeners",
|
||||||
|
isRequired: true,
|
||||||
|
name: "Listeners",
|
||||||
|
directive: "@Listeners",
|
||||||
|
definition: "directive @Listeners (values: [String!]) on OBJECT",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeSchemaExecutable(inputSchema: string) {
|
||||||
|
const { schema: cleanedSchema } = cleanGraphQLSchema(inputSchema)
|
||||||
|
|
||||||
|
return makeExecutableSchema({ typeDefs: cleanedSchema })
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractNameFromAlias(
|
||||||
|
alias: JoinerServiceConfigAlias | JoinerServiceConfigAlias[]
|
||||||
|
) {
|
||||||
|
const alias_ = Array.isArray(alias) ? alias[0] : alias
|
||||||
|
const names = Array.isArray(alias_?.name) ? alias_?.name : [alias_?.name]
|
||||||
|
return names[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
function retrieveAliasForEntity(entityName: string, aliases) {
|
||||||
|
aliases = aliases ? (Array.isArray(aliases) ? aliases : [aliases]) : []
|
||||||
|
|
||||||
|
for (const alias of aliases) {
|
||||||
|
const names = Array.isArray(alias.name) ? alias.name : [alias.name]
|
||||||
|
|
||||||
|
if (alias.entity === entityName) {
|
||||||
|
return names[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const name of names) {
|
||||||
|
if (name.toLowerCase() === entityName.toLowerCase()) {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function retrieveModuleAndAlias(entityName, moduleJoinerConfigs) {
|
||||||
|
let relatedModule
|
||||||
|
let alias
|
||||||
|
|
||||||
|
for (const moduleJoinerConfig of moduleJoinerConfigs) {
|
||||||
|
const moduleSchema = moduleJoinerConfig.schema
|
||||||
|
const moduleAliases = moduleJoinerConfig.alias
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the entity exist in the module schema, then the current module is the
|
||||||
|
* one we are looking for.
|
||||||
|
*
|
||||||
|
* If the module does not have any schema, then we need to base the search
|
||||||
|
* on the provided aliases. in any case, we try to get both
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (moduleSchema) {
|
||||||
|
const executableSchema = makeSchemaExecutable(moduleSchema)
|
||||||
|
const entitiesMap = executableSchema.getTypeMap()
|
||||||
|
|
||||||
|
if (entitiesMap[entityName]) {
|
||||||
|
relatedModule = moduleJoinerConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relatedModule && moduleAliases) {
|
||||||
|
alias = retrieveAliasForEntity(entityName, moduleJoinerConfig.alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relatedModule) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!relatedModule) {
|
||||||
|
return { relatedModule: null, alias: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alias) {
|
||||||
|
throw new Error(
|
||||||
|
`Index Module error, the module ${relatedModule?.serviceName} has a schema but does not have any alias for the entity ${entityName}. Please add an alias to the module configuration and the entity it correspond to in the args under the entity property.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { relatedModule, alias }
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: rename util
|
||||||
|
function retrieveLinkModuleAndAlias({
|
||||||
|
primaryEntity,
|
||||||
|
primaryModuleConfig,
|
||||||
|
foreignEntity,
|
||||||
|
foreignModuleConfig,
|
||||||
|
moduleJoinerConfigs,
|
||||||
|
}: {
|
||||||
|
primaryEntity: string
|
||||||
|
primaryModuleConfig: ModuleJoinerConfig
|
||||||
|
foreignEntity: string
|
||||||
|
foreignModuleConfig: ModuleJoinerConfig
|
||||||
|
moduleJoinerConfigs: ModuleJoinerConfig[]
|
||||||
|
}): {
|
||||||
|
entityName: string
|
||||||
|
alias: string
|
||||||
|
linkModuleConfig: ModuleJoinerConfig
|
||||||
|
intermediateEntityNames: string[]
|
||||||
|
}[] {
|
||||||
|
const linkModulesMetadata: {
|
||||||
|
entityName: string
|
||||||
|
alias: string
|
||||||
|
linkModuleConfig: ModuleJoinerConfig
|
||||||
|
intermediateEntityNames: string[]
|
||||||
|
}[] = []
|
||||||
|
|
||||||
|
for (const linkModuleJoinerConfig of moduleJoinerConfigs.filter(
|
||||||
|
(config) => config.isLink && !config.isReadOnlyLink
|
||||||
|
)) {
|
||||||
|
const linkPrimary =
|
||||||
|
linkModuleJoinerConfig.relationships![0] as ModuleJoinerRelationship
|
||||||
|
const linkForeign =
|
||||||
|
linkModuleJoinerConfig.relationships![1] as ModuleJoinerRelationship
|
||||||
|
|
||||||
|
if (
|
||||||
|
linkPrimary.serviceName === primaryModuleConfig.serviceName &&
|
||||||
|
linkForeign.serviceName === foreignModuleConfig.serviceName
|
||||||
|
) {
|
||||||
|
const primaryEntityLinkableKey = linkPrimary.foreignKey
|
||||||
|
const isTheForeignKeyEntityEqualPrimaryEntity =
|
||||||
|
primaryModuleConfig.linkableKeys?.[primaryEntityLinkableKey] ===
|
||||||
|
primaryEntity
|
||||||
|
|
||||||
|
const foreignEntityLinkableKey = linkForeign.foreignKey
|
||||||
|
const isTheForeignKeyEntityEqualForeignEntity =
|
||||||
|
foreignModuleConfig.linkableKeys?.[foreignEntityLinkableKey] ===
|
||||||
|
foreignEntity
|
||||||
|
|
||||||
|
const linkName = linkModuleJoinerConfig.extends?.find((extend) => {
|
||||||
|
return (
|
||||||
|
extend.serviceName === primaryModuleConfig.serviceName &&
|
||||||
|
extend.relationship.primaryKey === primaryEntityLinkableKey
|
||||||
|
)
|
||||||
|
})?.relationship.serviceName
|
||||||
|
|
||||||
|
if (!linkName) {
|
||||||
|
throw new Error(
|
||||||
|
`Index Module error, unable to retrieve the link module name for the services ${primaryModuleConfig.serviceName} - ${foreignModuleConfig.serviceName}. Please be sure that the extend relationship service name is set correctly`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!linkModuleJoinerConfig.alias?.[0]?.entity) {
|
||||||
|
throw new Error(
|
||||||
|
`Index Module error, unable to retrieve the link module entity name for the services ${primaryModuleConfig.serviceName} - ${foreignModuleConfig.serviceName}. Please be sure that the link module alias has an entity property in the args.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isTheForeignKeyEntityEqualPrimaryEntity &&
|
||||||
|
isTheForeignKeyEntityEqualForeignEntity
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* The link will become the parent of the foreign entity, that is why the alias must be the one that correspond to the extended foreign module
|
||||||
|
*/
|
||||||
|
|
||||||
|
linkModulesMetadata.push({
|
||||||
|
entityName: linkModuleJoinerConfig.alias[0].entity,
|
||||||
|
alias: extractNameFromAlias(linkModuleJoinerConfig.alias),
|
||||||
|
linkModuleConfig: linkModuleJoinerConfig,
|
||||||
|
intermediateEntityNames: [],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const intermediateEntityName =
|
||||||
|
foreignModuleConfig.linkableKeys![foreignEntityLinkableKey]
|
||||||
|
|
||||||
|
if (!foreignModuleConfig.schema) {
|
||||||
|
throw new Error(
|
||||||
|
`Index Module error, unable to retrieve the intermediate entity name for the services ${primaryModuleConfig.serviceName} - ${foreignModuleConfig.serviceName}. Please be sure that the foreign module ${foreignModuleConfig.serviceName} has a schema.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const executableSchema = makeSchemaExecutable(
|
||||||
|
foreignModuleConfig.schema
|
||||||
|
)
|
||||||
|
const entitiesMap = executableSchema.getTypeMap()
|
||||||
|
|
||||||
|
let intermediateEntities: string[] = []
|
||||||
|
let foundCount = 0
|
||||||
|
|
||||||
|
const isForeignEntityChildOfIntermediateEntity = (
|
||||||
|
entityName
|
||||||
|
): boolean => {
|
||||||
|
for (const entityType of Object.values(entitiesMap)) {
|
||||||
|
if (
|
||||||
|
entityType.astNode?.kind === "ObjectTypeDefinition" &&
|
||||||
|
entityType.astNode?.fields?.some((field) => {
|
||||||
|
return (field.type as any)?.type?.name?.value === entityName
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
if (entityType.name === intermediateEntityName) {
|
||||||
|
++foundCount
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
const test = isForeignEntityChildOfIntermediateEntity(
|
||||||
|
entityType.name
|
||||||
|
)
|
||||||
|
if (test) {
|
||||||
|
intermediateEntities.push(entityType.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
isForeignEntityChildOfIntermediateEntity(foreignEntity)
|
||||||
|
|
||||||
|
if (foundCount !== 1) {
|
||||||
|
throw new Error(
|
||||||
|
`Index Module error, unable to retrieve the intermediate entities for the services ${primaryModuleConfig.serviceName} - ${foreignModuleConfig.serviceName} between ${foreignEntity} and ${intermediateEntityName}. Multiple paths or no path found. Please check your schema in ${foreignModuleConfig.serviceName}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
intermediateEntities.push(intermediateEntityName!)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The link will become the parent of the foreign entity, that is why the alias must be the one that correspond to the extended foreign module
|
||||||
|
*/
|
||||||
|
|
||||||
|
linkModulesMetadata.push({
|
||||||
|
entityName: linkModuleJoinerConfig.alias[0].entity,
|
||||||
|
alias: extractNameFromAlias(linkModuleJoinerConfig.alias),
|
||||||
|
linkModuleConfig: linkModuleJoinerConfig,
|
||||||
|
intermediateEntityNames: intermediateEntities,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!linkModulesMetadata.length) {
|
||||||
|
// TODO: change to use the logger
|
||||||
|
console.warn(
|
||||||
|
`Index Module warning, unable to retrieve the link module that correspond to the entities ${primaryEntity} - ${foreignEntity}.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return linkModulesMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
function getObjectRepresentationRef(
|
||||||
|
entityName,
|
||||||
|
{ objectRepresentationRef }
|
||||||
|
): SchemaObjectEntityRepresentation {
|
||||||
|
return (objectRepresentationRef[entityName] ??= {
|
||||||
|
entity: entityName,
|
||||||
|
parents: [],
|
||||||
|
alias: "",
|
||||||
|
listeners: [],
|
||||||
|
moduleConfig: null,
|
||||||
|
fields: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCustomDirectives(currentObjectRepresentationRef, directives) {
|
||||||
|
for (const customDirectiveConfiguration of Object.values(CustomDirectives)) {
|
||||||
|
const directive = directives.find(
|
||||||
|
(typeDirective) =>
|
||||||
|
typeDirective.name.value === customDirectiveConfiguration.name
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!directive) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only support array directive value for now
|
||||||
|
currentObjectRepresentationRef[
|
||||||
|
customDirectiveConfiguration.configurationPropertyName
|
||||||
|
] = ((directive.arguments[0].value as any)?.values ?? []).map(
|
||||||
|
(v) => v.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processEntity(
|
||||||
|
entityName: string,
|
||||||
|
{
|
||||||
|
entitiesMap,
|
||||||
|
moduleJoinerConfigs,
|
||||||
|
objectRepresentationRef,
|
||||||
|
}: {
|
||||||
|
entitiesMap: any
|
||||||
|
moduleJoinerConfigs: ModuleJoinerConfig[]
|
||||||
|
objectRepresentationRef: SchemaObjectRepresentation
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Get the reference to the object representation for the current entity.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const currentObjectRepresentationRef = getObjectRepresentationRef(
|
||||||
|
entityName,
|
||||||
|
{
|
||||||
|
objectRepresentationRef,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve and set the custom directives for the current entity.
|
||||||
|
*/
|
||||||
|
|
||||||
|
setCustomDirectives(
|
||||||
|
currentObjectRepresentationRef,
|
||||||
|
entitiesMap[entityName].astNode?.directives ?? []
|
||||||
|
)
|
||||||
|
|
||||||
|
currentObjectRepresentationRef.fields =
|
||||||
|
gqlGetFieldsAndRelations(entitiesMap, entityName) ?? []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the module and alias for the current entity.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { relatedModule: currentEntityModule, alias } = retrieveModuleAndAlias(
|
||||||
|
entityName,
|
||||||
|
moduleJoinerConfigs
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!currentEntityModule &&
|
||||||
|
currentObjectRepresentationRef.listeners.length > 0
|
||||||
|
) {
|
||||||
|
const example = JSON.stringify({
|
||||||
|
alias: [
|
||||||
|
{
|
||||||
|
name: "entity-alias",
|
||||||
|
entity: entityName,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
throw new Error(
|
||||||
|
`Index Module error, unable to retrieve the module that corresponds to the entity ${entityName}.\nPlease add the entity to the module schema or add an alias to the joiner config like the example below:\n${example}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentEntityModule) {
|
||||||
|
objectRepresentationRef._serviceNameModuleConfigMap[
|
||||||
|
currentEntityModule.serviceName
|
||||||
|
] = currentEntityModule
|
||||||
|
currentObjectRepresentationRef.moduleConfig = currentEntityModule
|
||||||
|
currentObjectRepresentationRef.alias = alias
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Retrieve the parent entities for the current entity.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const schemaParentEntity = Object.values(entitiesMap).filter((value: any) => {
|
||||||
|
return (
|
||||||
|
value.astNode &&
|
||||||
|
(value.astNode as ObjectTypeDefinitionNode).fields?.some((field: any) => {
|
||||||
|
let currentType = field.type
|
||||||
|
while (currentType.type) {
|
||||||
|
currentType = currentType.type
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentType.name?.value === entityName
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!schemaParentEntity.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the current entity has parent entities, then we need to process them.
|
||||||
|
*/
|
||||||
|
const parentEntityNames = schemaParentEntity.map((parent: any) => {
|
||||||
|
return parent.name
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const parent of parentEntityNames) {
|
||||||
|
/**
|
||||||
|
* Retrieve the parent entity field in the schema
|
||||||
|
*/
|
||||||
|
|
||||||
|
const entityFieldInParent = (
|
||||||
|
entitiesMap[parent].astNode as any
|
||||||
|
)?.fields?.find((field) => {
|
||||||
|
let currentType = field.type
|
||||||
|
while (currentType.type) {
|
||||||
|
currentType = currentType.type
|
||||||
|
}
|
||||||
|
return currentType.name?.value === entityName
|
||||||
|
})
|
||||||
|
|
||||||
|
const isEntityListInParent =
|
||||||
|
entityFieldInParent.type.kind === Kind.LIST_TYPE
|
||||||
|
const entityTargetPropertyNameInParent = entityFieldInParent.name.value
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the parent entity object representation reference.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const parentObjectRepresentationRef = getObjectRepresentationRef(parent, {
|
||||||
|
objectRepresentationRef,
|
||||||
|
})
|
||||||
|
const parentModuleConfig = parentObjectRepresentationRef.moduleConfig
|
||||||
|
|
||||||
|
// If the entity is not part of any module, just set the parent and continue
|
||||||
|
if (!currentObjectRepresentationRef.moduleConfig) {
|
||||||
|
currentObjectRepresentationRef.parents.push({
|
||||||
|
ref: parentObjectRepresentationRef,
|
||||||
|
targetProp: entityTargetPropertyNameInParent,
|
||||||
|
isList: isEntityListInParent,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the parent entity and the current entity are part of the same servive then configure the parent and
|
||||||
|
* add the parent id as a field to the current entity.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentObjectRepresentationRef.moduleConfig.serviceName ===
|
||||||
|
parentModuleConfig.serviceName ||
|
||||||
|
parentModuleConfig.isLink
|
||||||
|
) {
|
||||||
|
currentObjectRepresentationRef.parents.push({
|
||||||
|
ref: parentObjectRepresentationRef,
|
||||||
|
targetProp: entityTargetPropertyNameInParent,
|
||||||
|
isList: isEntityListInParent,
|
||||||
|
})
|
||||||
|
|
||||||
|
currentObjectRepresentationRef.fields.push(
|
||||||
|
parentObjectRepresentationRef.alias + ".id"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* If the parent entity and the current entity are not part of the same service then we need to
|
||||||
|
* find the link module that join them.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const linkModuleMetadatas = retrieveLinkModuleAndAlias({
|
||||||
|
primaryEntity: parentObjectRepresentationRef.entity,
|
||||||
|
primaryModuleConfig: parentModuleConfig,
|
||||||
|
foreignEntity: currentObjectRepresentationRef.entity,
|
||||||
|
foreignModuleConfig: currentEntityModule,
|
||||||
|
moduleJoinerConfigs,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const linkModuleMetadata of linkModuleMetadatas) {
|
||||||
|
const linkObjectRepresentationRef = getObjectRepresentationRef(
|
||||||
|
linkModuleMetadata.entityName,
|
||||||
|
{ objectRepresentationRef }
|
||||||
|
)
|
||||||
|
|
||||||
|
objectRepresentationRef._serviceNameModuleConfigMap[
|
||||||
|
linkModuleMetadata.linkModuleConfig.serviceName ||
|
||||||
|
linkModuleMetadata.entityName
|
||||||
|
] = currentEntityModule
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the schema parent entity as a parent to the link module and configure it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
linkObjectRepresentationRef.parents = [
|
||||||
|
{
|
||||||
|
ref: parentObjectRepresentationRef,
|
||||||
|
targetProp: linkModuleMetadata.alias,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
linkObjectRepresentationRef.alias = linkModuleMetadata.alias
|
||||||
|
linkObjectRepresentationRef.listeners = [
|
||||||
|
`${linkModuleMetadata.entityName}.${CommonEvents.ATTACHED}`,
|
||||||
|
`${linkModuleMetadata.entityName}.${CommonEvents.DETACHED}`,
|
||||||
|
]
|
||||||
|
linkObjectRepresentationRef.moduleConfig =
|
||||||
|
linkModuleMetadata.linkModuleConfig
|
||||||
|
|
||||||
|
linkObjectRepresentationRef.fields = [
|
||||||
|
"id",
|
||||||
|
...linkModuleMetadata.linkModuleConfig
|
||||||
|
.relationships!.map(
|
||||||
|
(relationship) =>
|
||||||
|
[
|
||||||
|
parentModuleConfig.serviceName,
|
||||||
|
currentEntityModule.serviceName,
|
||||||
|
].includes(relationship.serviceName) && relationship.foreignKey
|
||||||
|
)
|
||||||
|
.filter((v): v is string => Boolean(v)),
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the current entity is not the entity that is used to join the link module and the parent entity
|
||||||
|
* then we need to add the new entity that join them and then add the link as its parent
|
||||||
|
* before setting the new entity as the true parent of the current entity.
|
||||||
|
*/
|
||||||
|
|
||||||
|
for (
|
||||||
|
let i = linkModuleMetadata.intermediateEntityNames.length - 1;
|
||||||
|
i >= 0;
|
||||||
|
--i
|
||||||
|
) {
|
||||||
|
const intermediateEntityName =
|
||||||
|
linkModuleMetadata.intermediateEntityNames[i]
|
||||||
|
|
||||||
|
const isLastIntermediateEntity =
|
||||||
|
i === linkModuleMetadata.intermediateEntityNames.length - 1
|
||||||
|
|
||||||
|
const parentIntermediateEntityRef = isLastIntermediateEntity
|
||||||
|
? linkObjectRepresentationRef
|
||||||
|
: objectRepresentationRef[
|
||||||
|
linkModuleMetadata.intermediateEntityNames[i + 1]
|
||||||
|
]
|
||||||
|
|
||||||
|
const {
|
||||||
|
relatedModule: intermediateEntityModule,
|
||||||
|
alias: intermediateEntityAlias,
|
||||||
|
} = retrieveModuleAndAlias(
|
||||||
|
intermediateEntityName,
|
||||||
|
moduleJoinerConfigs
|
||||||
|
)
|
||||||
|
|
||||||
|
const intermediateEntityObjectRepresentationRef =
|
||||||
|
getObjectRepresentationRef(intermediateEntityName, {
|
||||||
|
objectRepresentationRef,
|
||||||
|
})
|
||||||
|
|
||||||
|
objectRepresentationRef._serviceNameModuleConfigMap[
|
||||||
|
intermediateEntityModule.serviceName
|
||||||
|
] = intermediateEntityModule
|
||||||
|
|
||||||
|
intermediateEntityObjectRepresentationRef.parents.push({
|
||||||
|
ref: parentIntermediateEntityRef,
|
||||||
|
targetProp: intermediateEntityAlias,
|
||||||
|
isList: true, // TODO: check if it is a list in retrieveLinkModuleAndAlias and return the intermediate entity names + isList for each
|
||||||
|
})
|
||||||
|
|
||||||
|
intermediateEntityObjectRepresentationRef.alias =
|
||||||
|
intermediateEntityAlias
|
||||||
|
intermediateEntityObjectRepresentationRef.listeners = [
|
||||||
|
intermediateEntityName + "." + CommonEvents.CREATED,
|
||||||
|
intermediateEntityName + "." + CommonEvents.UPDATED,
|
||||||
|
]
|
||||||
|
intermediateEntityObjectRepresentationRef.moduleConfig =
|
||||||
|
intermediateEntityModule
|
||||||
|
intermediateEntityObjectRepresentationRef.fields = ["id"]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We push the parent id only between intermediate entities but not between intermediate and link
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!isLastIntermediateEntity) {
|
||||||
|
intermediateEntityObjectRepresentationRef.fields.push(
|
||||||
|
parentIntermediateEntityRef.alias + ".id"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there is any intermediate entity then we need to set the last one as the parent field for the current entity.
|
||||||
|
* otherwise there is not need to set the link id field into the current entity.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let currentParentIntermediateRef = linkObjectRepresentationRef
|
||||||
|
if (linkModuleMetadata.intermediateEntityNames.length) {
|
||||||
|
currentParentIntermediateRef =
|
||||||
|
objectRepresentationRef[
|
||||||
|
linkModuleMetadata.intermediateEntityNames[0]
|
||||||
|
]
|
||||||
|
currentObjectRepresentationRef.fields.push(
|
||||||
|
currentParentIntermediateRef.alias + ".id"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentObjectRepresentationRef.parents.push({
|
||||||
|
ref: currentParentIntermediateRef,
|
||||||
|
inSchemaRef: parentObjectRepresentationRef,
|
||||||
|
targetProp: entityTargetPropertyNameInParent,
|
||||||
|
isList: isEntityListInParent,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a special object which will be used to retrieve the correct
|
||||||
|
* object representation using path tree
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* {
|
||||||
|
* _schemaPropertiesMap: {
|
||||||
|
* "product": <ProductRef>
|
||||||
|
* "product.variants": <ProductVariantRef>
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
function buildAliasMap(objectRepresentation: SchemaObjectRepresentation) {
|
||||||
|
const aliasMap: SchemaObjectRepresentation["_schemaPropertiesMap"] = {}
|
||||||
|
|
||||||
|
function recursivelyBuildAliasPath(
|
||||||
|
current,
|
||||||
|
alias = "",
|
||||||
|
aliases: { alias: string; shortCutOf?: string }[] = []
|
||||||
|
): { alias: string; shortCutOf?: string }[] {
|
||||||
|
if (current.parents?.length) {
|
||||||
|
for (const parentEntity of current.parents) {
|
||||||
|
/**
|
||||||
|
* Here we build the alias from child to parent to get it as parent to child
|
||||||
|
*/
|
||||||
|
|
||||||
|
const _aliases = recursivelyBuildAliasPath(
|
||||||
|
parentEntity.ref,
|
||||||
|
`${parentEntity.targetProp}${alias ? "." + alias : ""}`
|
||||||
|
).map((alias) => ({ alias: alias.alias }))
|
||||||
|
|
||||||
|
aliases.push(..._aliases)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Now if there is a inSchemaRef it means that we had inferred a link module
|
||||||
|
* and we want to get the alias path as it would be in the schema provided
|
||||||
|
* and it become the short cut path of the full path above
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (parentEntity.inSchemaRef) {
|
||||||
|
const shortCutOf = _aliases.map((a) => a.alias)[0]
|
||||||
|
const _aliasesShortCut = recursivelyBuildAliasPath(
|
||||||
|
parentEntity.inSchemaRef,
|
||||||
|
`${parentEntity.targetProp}${alias ? "." + alias : ""}`
|
||||||
|
).map((alias_) => {
|
||||||
|
return {
|
||||||
|
alias: alias_.alias,
|
||||||
|
// It has to be the same entry point
|
||||||
|
shortCutOf:
|
||||||
|
shortCutOf.split(".")[0] === alias_.alias.split(".")[0]
|
||||||
|
? shortCutOf
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
aliases.push(..._aliasesShortCut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aliases.push({ alias: current.alias + (alias ? "." + alias : "") })
|
||||||
|
|
||||||
|
return aliases
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const objectRepresentationKey of Object.keys(
|
||||||
|
objectRepresentation
|
||||||
|
).filter(
|
||||||
|
(key) => !schemaObjectRepresentationPropertiesToOmit.includes(key)
|
||||||
|
)) {
|
||||||
|
const entityRepresentationRef =
|
||||||
|
objectRepresentation[objectRepresentationKey]
|
||||||
|
|
||||||
|
const aliases = recursivelyBuildAliasPath(entityRepresentationRef)
|
||||||
|
|
||||||
|
for (const alias of aliases) {
|
||||||
|
aliasMap[alias.alias] = {
|
||||||
|
ref: entityRepresentationRef,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alias.shortCutOf) {
|
||||||
|
aliasMap[alias.alias]["shortCutOf"] = alias.shortCutOf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return aliasMap
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This util build an internal representation object from the provided schema.
|
||||||
|
* It will resolve all modules, fields, link module representation to build
|
||||||
|
* the appropriate representation for the index module.
|
||||||
|
*
|
||||||
|
* This representation will be used to re construct the expected output object from a search
|
||||||
|
* but can also be used for anything since the relation tree is available through ref.
|
||||||
|
*
|
||||||
|
* @param schema
|
||||||
|
*/
|
||||||
|
export function buildSchemaObjectRepresentation(
|
||||||
|
schema
|
||||||
|
): [SchemaObjectRepresentation, Record<string, any>] {
|
||||||
|
const moduleJoinerConfigs = MedusaModule.getAllJoinerConfigs()
|
||||||
|
const augmentedSchema = CustomDirectives.Listeners.definition + schema
|
||||||
|
const executableSchema = makeSchemaExecutable(augmentedSchema)
|
||||||
|
const entitiesMap = executableSchema.getTypeMap()
|
||||||
|
|
||||||
|
const objectRepresentation = {
|
||||||
|
_serviceNameModuleConfigMap: {},
|
||||||
|
} as SchemaObjectRepresentation
|
||||||
|
|
||||||
|
Object.entries(entitiesMap).forEach(([entityName, entityMapValue]) => {
|
||||||
|
if (!entityMapValue.astNode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
processEntity(entityName, {
|
||||||
|
entitiesMap,
|
||||||
|
moduleJoinerConfigs,
|
||||||
|
objectRepresentationRef: objectRepresentation,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
objectRepresentation._schemaPropertiesMap =
|
||||||
|
buildAliasMap(objectRepresentation)
|
||||||
|
|
||||||
|
return [objectRepresentation, entitiesMap]
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||||
|
import {
|
||||||
|
SchemaObjectRepresentation,
|
||||||
|
schemaObjectRepresentationPropertiesToOmit,
|
||||||
|
} from "../types"
|
||||||
|
|
||||||
|
export async function createPartitions(
|
||||||
|
schemaObjectRepresentation: SchemaObjectRepresentation,
|
||||||
|
manager: SqlEntityManager
|
||||||
|
): Promise<void> {
|
||||||
|
const activeSchema = manager.config.get("schema")
|
||||||
|
? `"${manager.config.get("schema")}".`
|
||||||
|
: ""
|
||||||
|
const partitions = Object.keys(schemaObjectRepresentation)
|
||||||
|
.filter(
|
||||||
|
(key) =>
|
||||||
|
!schemaObjectRepresentationPropertiesToOmit.includes(key) &&
|
||||||
|
schemaObjectRepresentation[key].listeners.length > 0
|
||||||
|
)
|
||||||
|
.map((key) => {
|
||||||
|
const cName = key.toLowerCase()
|
||||||
|
const part: string[] = []
|
||||||
|
part.push(
|
||||||
|
`CREATE TABLE IF NOT EXISTS ${activeSchema}cat_${cName} PARTITION OF ${activeSchema}index_data FOR VALUES IN ('${key}')`
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const parent of schemaObjectRepresentation[key].parents) {
|
||||||
|
const pKey = `${parent.ref.entity}-${key}`
|
||||||
|
const pName = `${parent.ref.entity}${key}`.toLowerCase()
|
||||||
|
part.push(
|
||||||
|
`CREATE TABLE IF NOT EXISTS ${activeSchema}cat_pivot_${pName} PARTITION OF ${activeSchema}index_relation FOR VALUES IN ('${pKey}')`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return part
|
||||||
|
})
|
||||||
|
.flat()
|
||||||
|
|
||||||
|
if (!partitions.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
partitions.push(`analyse ${activeSchema}index_data`)
|
||||||
|
partitions.push(`analyse ${activeSchema}index_relation`)
|
||||||
|
|
||||||
|
await manager.execute(partitions.join("; "))
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export const defaultSchema = `
|
||||||
|
type Product @Listeners(values: ["Product.product.created", "Product.product.updated", "Product.product.deleted"]) {
|
||||||
|
id: String
|
||||||
|
title: String
|
||||||
|
variants: [ProductVariant]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductVariant @Listeners(values: ["Product.product-variant.created", "Product.product-variant.updated", "Product.product-variant.deleted"]) {
|
||||||
|
id: String
|
||||||
|
product_id: String
|
||||||
|
sku: String
|
||||||
|
prices: [Price]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Price @Listeners(values: ["Pricing.price.created", "Pricing.price.updated", "Pricing.price.deleted"]) {
|
||||||
|
amount: Int
|
||||||
|
currency_code: String
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./query-builder"
|
||||||
|
export * from "./create-partitions"
|
||||||
|
export * from "./build-config"
|
||||||
@@ -0,0 +1,628 @@
|
|||||||
|
import { isObject, isString } from "@medusajs/utils"
|
||||||
|
import { GraphQLList } from "graphql"
|
||||||
|
import { Knex } from "knex"
|
||||||
|
import {
|
||||||
|
OrderBy,
|
||||||
|
QueryFormat,
|
||||||
|
QueryOptions,
|
||||||
|
SchemaObjectRepresentation,
|
||||||
|
SchemaPropertiesMap,
|
||||||
|
Select,
|
||||||
|
} from "../types"
|
||||||
|
|
||||||
|
export class QueryBuilder {
|
||||||
|
private readonly structure: Select
|
||||||
|
private readonly entityMap: Record<string, any>
|
||||||
|
private readonly knex: Knex
|
||||||
|
private readonly selector: QueryFormat
|
||||||
|
private readonly options?: QueryOptions
|
||||||
|
private readonly schema: SchemaObjectRepresentation
|
||||||
|
|
||||||
|
constructor(args: {
|
||||||
|
schema: SchemaObjectRepresentation
|
||||||
|
entityMap: Record<string, any>
|
||||||
|
knex: Knex
|
||||||
|
selector: QueryFormat
|
||||||
|
options?: QueryOptions
|
||||||
|
}) {
|
||||||
|
this.schema = args.schema
|
||||||
|
this.entityMap = args.entityMap
|
||||||
|
this.selector = args.selector
|
||||||
|
this.options = args.options
|
||||||
|
this.knex = args.knex
|
||||||
|
this.structure = this.selector.select
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStructureKeys(structure) {
|
||||||
|
return Object.keys(structure ?? {}).filter((key) => key !== "entity")
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEntity(path): SchemaPropertiesMap[0] {
|
||||||
|
if (!this.schema._schemaPropertiesMap[path]) {
|
||||||
|
throw new Error(`Could not find entity for path: ${path}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.schema._schemaPropertiesMap[path]
|
||||||
|
}
|
||||||
|
|
||||||
|
private getGraphQLType(path, field) {
|
||||||
|
const entity = this.getEntity(path)?.ref?.entity
|
||||||
|
const fieldRef = this.entityMap[entity]._fields[field]
|
||||||
|
if (!fieldRef) {
|
||||||
|
throw new Error(`Field ${field} not found in the entityMap.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentType = fieldRef.type
|
||||||
|
let isArray = false
|
||||||
|
while (currentType.ofType) {
|
||||||
|
if (currentType instanceof GraphQLList) {
|
||||||
|
isArray = true
|
||||||
|
}
|
||||||
|
|
||||||
|
currentType = currentType.ofType
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentType.name + (isArray ? "[]" : "")
|
||||||
|
}
|
||||||
|
|
||||||
|
private transformValueToType(path, field, value) {
|
||||||
|
if (value === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeToFn = {
|
||||||
|
Int: (val) => parseInt(val, 10),
|
||||||
|
Float: (val) => parseFloat(val),
|
||||||
|
String: (val) => String(val),
|
||||||
|
Boolean: (val) => Boolean(val),
|
||||||
|
ID: (val) => String(val),
|
||||||
|
Date: (val) => new Date(val).toISOString(),
|
||||||
|
Time: (val) => new Date(`1970-01-01T${val}Z`).toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPath = [path, ...field]
|
||||||
|
const prop = fullPath.pop()
|
||||||
|
const fieldPath = fullPath.join(".")
|
||||||
|
const graphqlType = this.getGraphQLType(fieldPath, prop).replace("[]", "")
|
||||||
|
|
||||||
|
const fn = typeToFn[graphqlType]
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((v) => (!fn ? v : fn(v)))
|
||||||
|
}
|
||||||
|
return !fn ? value : fn(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPostgresCastType(path, field) {
|
||||||
|
const graphqlToPostgresTypeMap = {
|
||||||
|
Int: "::int",
|
||||||
|
Float: "::double precision",
|
||||||
|
Boolean: "::boolean",
|
||||||
|
Date: "::timestamp",
|
||||||
|
Time: "::time",
|
||||||
|
"": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPath = [path, ...field]
|
||||||
|
const prop = fullPath.pop()
|
||||||
|
const fieldPath = fullPath.join(".")
|
||||||
|
let graphqlType = this.getGraphQLType(fieldPath, prop)
|
||||||
|
const isList = graphqlType.endsWith("[]")
|
||||||
|
graphqlType = graphqlType.replace("[]", "")
|
||||||
|
|
||||||
|
return (
|
||||||
|
(graphqlToPostgresTypeMap[graphqlType] ?? "") + (isList ? "[]" : "") ?? ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseWhere(
|
||||||
|
aliasMapping: { [path: string]: string },
|
||||||
|
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) => {
|
||||||
|
const path = key.split(".")
|
||||||
|
const field = [path.pop()]
|
||||||
|
|
||||||
|
while (!aliasMapping[path.join(".")] && path.length > 0) {
|
||||||
|
field.unshift(path.pop())
|
||||||
|
}
|
||||||
|
|
||||||
|
const attr = path.join(".")
|
||||||
|
|
||||||
|
return { field, attr }
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPathOperation = (
|
||||||
|
attr: string,
|
||||||
|
path: string[],
|
||||||
|
value: unknown
|
||||||
|
): string => {
|
||||||
|
const partialPath = path.length > 1 ? path.slice(0, -1) : path
|
||||||
|
const val = this.transformValueToType(attr, partialPath, value)
|
||||||
|
const result = path.reduceRight((acc, key) => {
|
||||||
|
return { [key]: acc }
|
||||||
|
}, val)
|
||||||
|
|
||||||
|
return JSON.stringify(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.forEach((key) => {
|
||||||
|
let value = obj[key]
|
||||||
|
|
||||||
|
if ((key === "$and" || key === "$or") && !Array.isArray(value)) {
|
||||||
|
value = [value]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "$and" && Array.isArray(value)) {
|
||||||
|
builder.where((qb) => {
|
||||||
|
value.forEach((cond) => {
|
||||||
|
qb.andWhere((subBuilder) =>
|
||||||
|
this.parseWhere(aliasMapping, cond, subBuilder)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else if (key === "$or" && Array.isArray(value)) {
|
||||||
|
builder.where((qb) => {
|
||||||
|
value.forEach((cond) => {
|
||||||
|
qb.orWhere((subBuilder) =>
|
||||||
|
this.parseWhere(aliasMapping, cond, subBuilder)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else if (isObject(value) && !Array.isArray(value)) {
|
||||||
|
const subKeys = Object.keys(value)
|
||||||
|
subKeys.forEach((subKey) => {
|
||||||
|
let operator = OPERATOR_MAP[subKey]
|
||||||
|
if (operator) {
|
||||||
|
const { field, attr } = getPathAndField(key)
|
||||||
|
const nested = new Array(field.length).join("->?")
|
||||||
|
|
||||||
|
const subValue = this.transformValueToType(
|
||||||
|
attr,
|
||||||
|
field,
|
||||||
|
value[subKey]
|
||||||
|
)
|
||||||
|
const castType = this.getPostgresCastType(attr, field)
|
||||||
|
|
||||||
|
const val = operator === "IN" ? subValue : [subValue]
|
||||||
|
if (operator === "=" && subValue === null) {
|
||||||
|
operator = "IS"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operator === "=") {
|
||||||
|
builder.whereRaw(
|
||||||
|
`${aliasMapping[attr]}.data @> '${getPathOperation(
|
||||||
|
attr,
|
||||||
|
field as string[],
|
||||||
|
subValue
|
||||||
|
)}'::jsonb`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
builder.whereRaw(
|
||||||
|
`(${aliasMapping[attr]}.data${nested}->>?)${castType} ${operator} ?`,
|
||||||
|
[...field, ...val]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unsupported operator: ${subKey}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const { field, attr } = getPathAndField(key)
|
||||||
|
const nested = new Array(field.length).join("->?")
|
||||||
|
|
||||||
|
value = this.transformValueToType(attr, field, value)
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const castType = this.getPostgresCastType(attr, field)
|
||||||
|
const inPlaceholders = value.map(() => "?").join(",")
|
||||||
|
builder.whereRaw(
|
||||||
|
`(${aliasMapping[attr]}.data${nested}->>?)${castType} IN (${inPlaceholders})`,
|
||||||
|
[...field, ...value]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const operator = value === null ? "IS" : "="
|
||||||
|
|
||||||
|
if (operator === "=") {
|
||||||
|
builder.whereRaw(
|
||||||
|
`${aliasMapping[attr]}.data @> '${getPathOperation(
|
||||||
|
attr,
|
||||||
|
field as string[],
|
||||||
|
value
|
||||||
|
)}'::jsonb`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const castType = this.getPostgresCastType(attr, field)
|
||||||
|
builder.whereRaw(
|
||||||
|
`(${aliasMapping[attr]}.data${nested}->>?)${castType} ${operator} ?`,
|
||||||
|
[...field, value]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildQueryParts(
|
||||||
|
structure: Select,
|
||||||
|
parentAlias: string,
|
||||||
|
parentEntity: string,
|
||||||
|
parentProperty: string,
|
||||||
|
aliasPath: string[] = [],
|
||||||
|
level = 0,
|
||||||
|
aliasMapping: { [path: string]: string } = {}
|
||||||
|
): string[] {
|
||||||
|
const currentAliasPath = [...aliasPath, parentProperty].join(".")
|
||||||
|
|
||||||
|
const entities = this.getEntity(currentAliasPath)
|
||||||
|
|
||||||
|
const mainEntity = entities.ref.entity
|
||||||
|
const mainAlias = mainEntity.toLowerCase() + level
|
||||||
|
|
||||||
|
const allEntities: any[] = []
|
||||||
|
if (!entities.shortCutOf) {
|
||||||
|
allEntities.push({
|
||||||
|
entity: mainEntity,
|
||||||
|
parEntity: parentEntity,
|
||||||
|
parAlias: parentAlias,
|
||||||
|
alias: mainAlias,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const intermediateAlias = entities.shortCutOf.split(".")
|
||||||
|
|
||||||
|
for (let i = intermediateAlias.length - 1, x = 0; i >= 0; i--, x++) {
|
||||||
|
const intermediateEntity = this.getEntity(intermediateAlias.join("."))
|
||||||
|
|
||||||
|
intermediateAlias.pop()
|
||||||
|
|
||||||
|
if (intermediateEntity.ref.entity === parentEntity) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentIntermediateEntity = this.getEntity(
|
||||||
|
intermediateAlias.join(".")
|
||||||
|
)
|
||||||
|
|
||||||
|
const alias =
|
||||||
|
intermediateEntity.ref.entity.toLowerCase() + level + "_" + x
|
||||||
|
const parAlias =
|
||||||
|
parentIntermediateEntity.ref.entity === parentEntity
|
||||||
|
? parentAlias
|
||||||
|
: parentIntermediateEntity.ref.entity.toLowerCase() +
|
||||||
|
level +
|
||||||
|
"_" +
|
||||||
|
(x + 1)
|
||||||
|
|
||||||
|
if (x === 0) {
|
||||||
|
aliasMapping[currentAliasPath] = alias
|
||||||
|
}
|
||||||
|
|
||||||
|
allEntities.unshift({
|
||||||
|
entity: intermediateEntity.ref.entity,
|
||||||
|
parEntity: parentIntermediateEntity.ref.entity,
|
||||||
|
parAlias,
|
||||||
|
alias,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let queryParts: string[] = []
|
||||||
|
for (const join of allEntities) {
|
||||||
|
const { alias, entity, parEntity, parAlias } = join
|
||||||
|
|
||||||
|
aliasMapping[currentAliasPath] = alias
|
||||||
|
|
||||||
|
if (level > 0) {
|
||||||
|
const subQuery = this.knex.queryBuilder()
|
||||||
|
const knex = this.knex
|
||||||
|
subQuery
|
||||||
|
.select(`${alias}.id`, `${alias}.data`)
|
||||||
|
.from("index_data AS " + alias)
|
||||||
|
.join(`index_relation AS ${alias}_ref`, function () {
|
||||||
|
this.on(
|
||||||
|
`${alias}_ref.pivot`,
|
||||||
|
"=",
|
||||||
|
knex.raw("?", [`${parEntity}-${entity}`])
|
||||||
|
)
|
||||||
|
.andOn(`${alias}_ref.parent_id`, "=", `${parAlias}.id`)
|
||||||
|
.andOn(`${alias}.id`, "=", `${alias}_ref.child_id`)
|
||||||
|
})
|
||||||
|
.where(`${alias}.name`, "=", knex.raw("?", [entity]))
|
||||||
|
|
||||||
|
const joinWhere = this.selector.joinWhere ?? {}
|
||||||
|
const joinKey = Object.keys(joinWhere).find((key) => {
|
||||||
|
const k = key.split(".")
|
||||||
|
k.pop()
|
||||||
|
return k.join(".") === currentAliasPath
|
||||||
|
})
|
||||||
|
|
||||||
|
if (joinKey) {
|
||||||
|
this.parseWhere(
|
||||||
|
aliasMapping,
|
||||||
|
{ [joinKey]: joinWhere[joinKey] },
|
||||||
|
subQuery
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParts.push(`LEFT JOIN LATERAL (
|
||||||
|
${subQuery.toQuery()}
|
||||||
|
) ${alias} ON TRUE`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = this.getStructureKeys(structure)
|
||||||
|
for (const child of children) {
|
||||||
|
const childStructure = structure[child] as Select
|
||||||
|
queryParts = queryParts.concat(
|
||||||
|
this.buildQueryParts(
|
||||||
|
childStructure,
|
||||||
|
mainAlias,
|
||||||
|
mainEntity,
|
||||||
|
child,
|
||||||
|
aliasPath.concat(parentProperty),
|
||||||
|
level + 1,
|
||||||
|
aliasMapping
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryParts
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildSelectParts(
|
||||||
|
structure: Select,
|
||||||
|
parentProperty: string,
|
||||||
|
aliasMapping: { [path: string]: string },
|
||||||
|
aliasPath: string[] = [],
|
||||||
|
selectParts: object = {}
|
||||||
|
): object {
|
||||||
|
const currentAliasPath = [...aliasPath, parentProperty].join(".")
|
||||||
|
const alias = aliasMapping[currentAliasPath]
|
||||||
|
|
||||||
|
selectParts[currentAliasPath] = `${alias}.data`
|
||||||
|
selectParts[currentAliasPath + ".id"] = `${alias}.id`
|
||||||
|
|
||||||
|
const children = this.getStructureKeys(structure)
|
||||||
|
|
||||||
|
for (const child of children) {
|
||||||
|
const childStructure = structure[child] as Select
|
||||||
|
|
||||||
|
this.buildSelectParts(
|
||||||
|
childStructure,
|
||||||
|
child,
|
||||||
|
aliasMapping,
|
||||||
|
aliasPath.concat(parentProperty),
|
||||||
|
selectParts
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectParts
|
||||||
|
}
|
||||||
|
|
||||||
|
private transformOrderBy(arr: (object | string)[]): OrderBy {
|
||||||
|
const result = {}
|
||||||
|
const map = new Map()
|
||||||
|
map.set(true, "ASC")
|
||||||
|
map.set(1, "ASC")
|
||||||
|
map.set("ASC", "ASC")
|
||||||
|
map.set(false, "DESC")
|
||||||
|
map.set(-1, "DESC")
|
||||||
|
map.set("DESC", "DESC")
|
||||||
|
|
||||||
|
function nested(obj, prefix = "") {
|
||||||
|
const keys = Object.keys(obj)
|
||||||
|
if (!keys.length) {
|
||||||
|
return
|
||||||
|
} else if (keys.length > 1) {
|
||||||
|
throw new Error("Order by only supports one key per object.")
|
||||||
|
}
|
||||||
|
const key = keys[0]
|
||||||
|
let value = obj[key]
|
||||||
|
if (isObject(value)) {
|
||||||
|
nested(value, prefix + key + ".")
|
||||||
|
} else {
|
||||||
|
if (isString(value)) {
|
||||||
|
value = value.toUpperCase()
|
||||||
|
}
|
||||||
|
result[prefix + key] = map.get(value) ?? "ASC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
arr.forEach((obj) => nested(obj))
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildQuery(countAllResults = true, returnIdOnly = false): string {
|
||||||
|
const queryBuilder = this.knex.queryBuilder()
|
||||||
|
|
||||||
|
const structure = this.structure
|
||||||
|
const filter = this.selector.where ?? {}
|
||||||
|
|
||||||
|
const { orderBy: order, skip, take } = this.options ?? {}
|
||||||
|
|
||||||
|
const orderBy = this.transformOrderBy(
|
||||||
|
(order && !Array.isArray(order) ? [order] : order) ?? []
|
||||||
|
)
|
||||||
|
|
||||||
|
const rootKey = this.getStructureKeys(structure)[0]
|
||||||
|
const rootStructure = structure[rootKey] as Select
|
||||||
|
const entity = this.getEntity(rootKey).ref.entity
|
||||||
|
const rootEntity = entity.toLowerCase()
|
||||||
|
const aliasMapping: { [path: string]: string } = {}
|
||||||
|
|
||||||
|
const joinParts = this.buildQueryParts(
|
||||||
|
rootStructure,
|
||||||
|
"",
|
||||||
|
entity,
|
||||||
|
rootKey,
|
||||||
|
[],
|
||||||
|
0,
|
||||||
|
aliasMapping
|
||||||
|
)
|
||||||
|
|
||||||
|
const rootAlias = aliasMapping[rootKey]
|
||||||
|
const selectParts = !returnIdOnly
|
||||||
|
? this.buildSelectParts(rootStructure, rootKey, aliasMapping)
|
||||||
|
: { [rootKey + ".id"]: `${rootAlias}.id` }
|
||||||
|
|
||||||
|
if (countAllResults) {
|
||||||
|
selectParts["offset_"] = this.knex.raw(
|
||||||
|
`DENSE_RANK() OVER (ORDER BY ${rootEntity}0.id)`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
queryBuilder.select(selectParts)
|
||||||
|
|
||||||
|
queryBuilder.from(`index_data AS ${rootEntity}0`)
|
||||||
|
|
||||||
|
joinParts.forEach((joinPart) => {
|
||||||
|
queryBuilder.joinRaw(joinPart)
|
||||||
|
})
|
||||||
|
|
||||||
|
queryBuilder.where(`${aliasMapping[rootEntity]}.name`, "=", entity)
|
||||||
|
|
||||||
|
// WHERE clause
|
||||||
|
this.parseWhere(aliasMapping, filter, queryBuilder)
|
||||||
|
|
||||||
|
// ORDER BY clause
|
||||||
|
for (const aliasPath in orderBy) {
|
||||||
|
const path = aliasPath.split(".")
|
||||||
|
const field = path.pop()
|
||||||
|
const attr = path.join(".")
|
||||||
|
const alias = aliasMapping[attr]
|
||||||
|
const direction = orderBy[aliasPath]
|
||||||
|
|
||||||
|
queryBuilder.orderByRaw(`${alias}.data->>'${field}' ${direction}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let sql = `WITH data AS (${queryBuilder.toQuery()})
|
||||||
|
SELECT * ${
|
||||||
|
countAllResults ? ", (SELECT max(offset_) FROM data) AS count" : ""
|
||||||
|
}
|
||||||
|
FROM data`
|
||||||
|
|
||||||
|
let take_ = !isNaN(+take!) ? +take! : 15
|
||||||
|
let skip_ = !isNaN(+skip!) ? +skip! : 0
|
||||||
|
if (typeof take === "number" || typeof skip === "number") {
|
||||||
|
sql += `
|
||||||
|
WHERE offset_ > ${skip_}
|
||||||
|
AND offset_ <= ${skip_ + take_}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildObjectFromResultset(
|
||||||
|
resultSet: Record<string, any>[]
|
||||||
|
): Record<string, any>[] {
|
||||||
|
const structure = this.structure
|
||||||
|
const rootKey = this.getStructureKeys(structure)[0]
|
||||||
|
|
||||||
|
const maps: { [key: string]: { [id: string]: Record<string, any> } } = {}
|
||||||
|
const isListMap: { [path: string]: boolean } = {}
|
||||||
|
const referenceMap: { [key: string]: any } = {}
|
||||||
|
const pathDetails: {
|
||||||
|
[key: string]: { property: string; parents: string[]; parentPath: string }
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
const initializeMaps = (structure: Select, path: string[]) => {
|
||||||
|
const currentPath = path.join(".")
|
||||||
|
maps[currentPath] = {}
|
||||||
|
|
||||||
|
if (path.length > 1) {
|
||||||
|
const property = path[path.length - 1]
|
||||||
|
const parents = path.slice(0, -1)
|
||||||
|
const parentPath = parents.join(".")
|
||||||
|
|
||||||
|
isListMap[currentPath] = !!this.getEntity(currentPath).ref.parents.find(
|
||||||
|
(p) => p.targetProp === property
|
||||||
|
)?.isList
|
||||||
|
|
||||||
|
pathDetails[currentPath] = { property, parents, parentPath }
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = this.getStructureKeys(structure)
|
||||||
|
for (const key of children) {
|
||||||
|
initializeMaps(structure[key] as Select, [...path, key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initializeMaps(structure[rootKey] as Select, [rootKey])
|
||||||
|
|
||||||
|
function buildReferenceKey(
|
||||||
|
path: string[],
|
||||||
|
id: string,
|
||||||
|
row: Record<string, any>
|
||||||
|
) {
|
||||||
|
let current = ""
|
||||||
|
let key = ""
|
||||||
|
for (const p of path) {
|
||||||
|
current += `${p}`
|
||||||
|
key += row[`${current}.id`] + "."
|
||||||
|
current += "."
|
||||||
|
}
|
||||||
|
return key + id
|
||||||
|
}
|
||||||
|
|
||||||
|
resultSet.forEach((row) => {
|
||||||
|
for (const path in maps) {
|
||||||
|
const id = row[`${path}.id`]
|
||||||
|
|
||||||
|
// root level
|
||||||
|
if (!pathDetails[path]) {
|
||||||
|
if (!maps[path][id]) {
|
||||||
|
maps[path][id] = row[path] || undefined
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const { property, parents, parentPath } = pathDetails[path]
|
||||||
|
const referenceKey = buildReferenceKey(parents, id, row)
|
||||||
|
|
||||||
|
if (referenceMap[referenceKey]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
maps[path][id] = row[path] || undefined
|
||||||
|
|
||||||
|
const parentObj = maps[parentPath][row[`${parentPath}.id`]]
|
||||||
|
|
||||||
|
if (!parentObj) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const isList = isListMap[parentPath + "." + property]
|
||||||
|
if (isList) {
|
||||||
|
parentObj[property] ??= []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maps[path][id] !== undefined) {
|
||||||
|
if (isList) {
|
||||||
|
parentObj[property].push(maps[path][id])
|
||||||
|
} else {
|
||||||
|
parentObj[property] = maps[path][id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
referenceMap[referenceKey] = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.values(maps[rootKey] ?? {})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": [
|
||||||
|
"dist",
|
||||||
|
"src/**/__tests__",
|
||||||
|
"src/**/__mocks__",
|
||||||
|
"src/**/__fixtures__",
|
||||||
|
"node_modules"
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["es2021"],
|
||||||
|
"target": "es2021",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"declaration": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"sourceMap": false,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"downlevelIteration": true, // to use ES5 specific tooling
|
||||||
|
"baseUrl": ".",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"paths": {
|
||||||
|
"@models": ["./src/models"],
|
||||||
|
"@services": ["./src/services"],
|
||||||
|
"@repositories": ["./src/repositories"],
|
||||||
|
"@types": ["./src/types"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src", "integration-tests"],
|
||||||
|
"exclude": [
|
||||||
|
"dist",
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
export default `
|
export default `
|
||||||
scalar DateTime
|
|
||||||
scalar JSON
|
|
||||||
|
|
||||||
type InventoryItem {
|
type InventoryItem {
|
||||||
id: ID!
|
id: ID!
|
||||||
created_at: DateTime!
|
created_at: DateTime!
|
||||||
@@ -21,7 +18,6 @@ type InventoryItem {
|
|||||||
title: String
|
title: String
|
||||||
thumbnail: String
|
thumbnail: String
|
||||||
metadata: JSON
|
metadata: JSON
|
||||||
|
|
||||||
inventory_levels: [InventoryLevel]
|
inventory_levels: [InventoryLevel]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +27,7 @@ type InventoryLevel {
|
|||||||
updated_at: DateTime!
|
updated_at: DateTime!
|
||||||
deleted_at: DateTime
|
deleted_at: DateTime
|
||||||
inventory_item_id: String!
|
inventory_item_id: String!
|
||||||
|
inventory_item: InventoryItem!
|
||||||
location_id: String!
|
location_id: String!
|
||||||
stocked_quantity: Int!
|
stocked_quantity: Int!
|
||||||
reserved_quantity: Int!
|
reserved_quantity: Int!
|
||||||
@@ -45,6 +42,7 @@ type ReservationItem {
|
|||||||
deleted_at: DateTime
|
deleted_at: DateTime
|
||||||
line_item_id: String
|
line_item_id: String
|
||||||
inventory_item_id: String!
|
inventory_item_id: String!
|
||||||
|
inventory_item: InventoryItem!
|
||||||
location_id: String!
|
location_id: String!
|
||||||
quantity: Int!
|
quantity: Int!
|
||||||
external_id: String
|
external_id: String
|
||||||
|
|||||||
@@ -36,18 +36,6 @@ type OrderSummary {
|
|||||||
raw_pending_difference: JSON
|
raw_pending_difference: JSON
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderAdjustmentLine {
|
|
||||||
id: ID!
|
|
||||||
code: String
|
|
||||||
amount: Float
|
|
||||||
order_id: String!
|
|
||||||
description: String
|
|
||||||
promotion_id: String
|
|
||||||
provider_id: String
|
|
||||||
created_at: DateTime
|
|
||||||
updated_at: DateTime
|
|
||||||
}
|
|
||||||
|
|
||||||
type OrderShippingMethodAdjustment {
|
type OrderShippingMethodAdjustment {
|
||||||
id: ID!
|
id: ID!
|
||||||
code: String
|
code: String
|
||||||
@@ -76,16 +64,6 @@ type OrderLineItemAdjustment {
|
|||||||
item_id: String!
|
item_id: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderTaxLine {
|
|
||||||
id: ID!
|
|
||||||
description: String
|
|
||||||
tax_rate_id: String
|
|
||||||
code: String!
|
|
||||||
rate: Float
|
|
||||||
provider_id: String
|
|
||||||
created_at: DateTime
|
|
||||||
updated_at: DateTime
|
|
||||||
}
|
|
||||||
|
|
||||||
type OrderShippingMethodTaxLine {
|
type OrderShippingMethodTaxLine {
|
||||||
id: ID!
|
id: ID!
|
||||||
@@ -367,6 +345,7 @@ type OrderClaimItem {
|
|||||||
item_id: String!
|
item_id: String!
|
||||||
quantity: Int!
|
quantity: Int!
|
||||||
reason: ClaimReason!
|
reason: ClaimReason!
|
||||||
|
images: [OrderClaimItemImage]
|
||||||
raw_quantity: JSON
|
raw_quantity: JSON
|
||||||
metadata: JSON
|
metadata: JSON
|
||||||
created_at: DateTime
|
created_at: DateTime
|
||||||
@@ -376,6 +355,7 @@ type OrderClaimItem {
|
|||||||
type OrderClaimItemImage {
|
type OrderClaimItemImage {
|
||||||
id: ID!
|
id: ID!
|
||||||
claim_item_id: String!
|
claim_item_id: String!
|
||||||
|
item: OrderClaimItem!
|
||||||
url: String
|
url: String
|
||||||
metadata: JSON
|
metadata: JSON
|
||||||
created_at: DateTime
|
created_at: DateTime
|
||||||
@@ -408,7 +388,7 @@ type OrderClaim {
|
|||||||
type OrderExchange {
|
type OrderExchange {
|
||||||
order_id: String!
|
order_id: String!
|
||||||
return_items: [OrderReturnItem]!
|
return_items: [OrderReturnItem]!
|
||||||
additional_items: [OrderClaimItem]!
|
additional_items: [OrderExchangeItem]!
|
||||||
no_notification: Boolean
|
no_notification: Boolean
|
||||||
difference_due: Float
|
difference_due: Float
|
||||||
return: Return
|
return: Return
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
export default `
|
export default `
|
||||||
scalar DateTime
|
|
||||||
scalar JSON
|
|
||||||
|
|
||||||
enum TransactionState {
|
enum TransactionState {
|
||||||
NOT_STARTED
|
NOT_STARTED
|
||||||
INVOKING
|
INVOKING
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
export default `
|
export default `
|
||||||
scalar DateTime
|
|
||||||
scalar JSON
|
|
||||||
|
|
||||||
enum TransactionState {
|
enum TransactionState {
|
||||||
NOT_STARTED
|
NOT_STARTED
|
||||||
INVOKING
|
INVOKING
|
||||||
|
|||||||
@@ -6027,6 +6027,28 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"@medusajs/index@workspace:packages/modules/index":
|
||||||
|
version: 0.0.0-use.local
|
||||||
|
resolution: "@medusajs/index@workspace:packages/modules/index"
|
||||||
|
dependencies:
|
||||||
|
"@medusajs/types": ^1.11.16
|
||||||
|
"@medusajs/utils": ^1.11.9
|
||||||
|
"@mikro-orm/cli": 5.9.7
|
||||||
|
cross-env: ^5.2.1
|
||||||
|
jest: ^29.7.0
|
||||||
|
medusa-test-utils: ^1.1.44
|
||||||
|
rimraf: ^3.0.2
|
||||||
|
ts-node: ^10.9.1
|
||||||
|
tsc-alias: ^1.8.6
|
||||||
|
typescript: ^5.1.6
|
||||||
|
peerDependencies:
|
||||||
|
"@mikro-orm/core": 5.9.7
|
||||||
|
"@mikro-orm/migrations": 5.9.7
|
||||||
|
"@mikro-orm/postgresql": 5.9.7
|
||||||
|
awilix: ^8.0.1
|
||||||
|
languageName: unknown
|
||||||
|
linkType: soft
|
||||||
|
|
||||||
"@medusajs/inventory-next@workspace:^, @medusajs/inventory-next@workspace:packages/modules/inventory-next":
|
"@medusajs/inventory-next@workspace:^, @medusajs/inventory-next@workspace:packages/modules/inventory-next":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@medusajs/inventory-next@workspace:packages/modules/inventory-next"
|
resolution: "@medusajs/inventory-next@workspace:packages/modules/inventory-next"
|
||||||
|
|||||||
Reference in new issue
Block a user