feat(*): Modules export entities and fields (#5242)

This commit is contained in:
Carlos R. L. Rodrigues
2023-10-03 17:20:43 -04:00
committed by GitHub
parent eeceec791c
commit 130cbc1f43
27 changed files with 801 additions and 226 deletions

View File

@@ -0,0 +1,13 @@
---
"@medusajs/stock-location": minor
"@medusajs/link-modules": minor
"@medusajs/modules-sdk": minor
"@medusajs/inventory": minor
"@medusajs/pricing": minor
"@medusajs/product": minor
"@medusajs/medusa": patch
"@medusajs/types": patch
"@medusajs/utils": patch
---
Modules exporting schema with entities and fields

View File

@@ -1,14 +1,15 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { InventoryItem, InventoryLevel, ReservationItem } from "./models"
export const joinerConfig: ModuleJoinerConfig = {
serviceName: Modules.INVENTORY,
primaryKeys: ["id"],
linkableKeys: [
"inventory_item_id",
"inventory_level_id",
"reservation_item_id",
],
linkableKeys: {
inventory_item_id: InventoryItem.name,
inventory_level_id: InventoryLevel.name,
reservation_item_id: ReservationItem.name,
},
alias: [
{
name: "inventory_items",

View File

@@ -19,7 +19,7 @@ import {
import * as linkDefinitions from "../definitions"
import { getMigration } from "../migration"
import { InitializeModuleInjectableDependencies } from "../types"
import { composeLinkName } from "../utils"
import { composeLinkName, generateGraphQLSchema } from "../utils"
import { getLinkModuleDefinition } from "./module-definition"
export const initialize = async (
@@ -98,6 +98,8 @@ export const initialize = async (
continue
}
definition.schema = generateGraphQLSchema(definition, primary, foreign)
const moduleDefinition = getLinkModuleDefinition(
definition,
primary,

View File

@@ -0,0 +1,130 @@
import { MedusaModule } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig, ModuleJoinerRelationship } from "@medusajs/types"
import { camelToSnakeCase, lowerCaseFirst, toPascalCase } from "@medusajs/utils"
import { composeTableName } from "./compose-link-name"
export function generateGraphQLSchema(
joinerConfig: ModuleJoinerConfig,
primary: ModuleJoinerRelationship,
foreign: ModuleJoinerRelationship
) {
const fieldNames = primary.foreignKey.split(",").concat(foreign.foreignKey)
const entityName = toPascalCase(
"Link_" +
(joinerConfig.databaseConfig?.tableName ??
composeTableName(
primary.serviceName,
primary.foreignKey,
foreign.serviceName,
foreign.foreignKey
))
)
// Pivot table fields
const fields = fieldNames.reduce((acc, curr) => {
acc[curr] = {
type: "String",
nullable: false,
}
return acc
}, {})
const extraFields = joinerConfig.databaseConfig?.extraFields ?? {}
for (const column in extraFields) {
fields[column] = {
type: getGraphQLType(extraFields[column].type),
nullable: !!extraFields[column].nullable,
}
}
// Link table relationships
const primaryField = `${camelToSnakeCase(primary.alias)}: ${toPascalCase(
composeTableName(primary.serviceName)
)}`
const foreignField = `${camelToSnakeCase(foreign.alias)}: ${toPascalCase(
composeTableName(foreign.serviceName)
)}`
let typeDef = `
type ${entityName} {
${(Object.entries(fields) as any)
.map(
([field, { type, nullable }]) =>
`${field}: ${nullable ? type : `${type}!`}`
)
.join("\n ")}
${primaryField}
${foreignField}
createdAt: String!
updatedAt: String!
deletedAt: String
}
`
for (const extend of joinerConfig.extends ?? []) {
const extendedModule = MedusaModule.getModuleInstance(extend.serviceName)
if (!extendedModule && !extend.relationship.isInternalService) {
throw new Error(
`Module ${extend.serviceName} not found. Please verify that the module is configured and installed, also the module must be loaded before the link modules.`
)
}
const joinerConfig = MedusaModule.getJoinerConfig(extend.serviceName)
let extendedEntityName =
joinerConfig?.linkableKeys?.[extend.relationship.primaryKey]!
if (!extendedEntityName) {
continue
}
extendedEntityName = toPascalCase(extendedEntityName)
const linkTableFieldName = camelToSnakeCase(
lowerCaseFirst(extend.relationship.alias)
)
const type = extend.relationship.isList ? `[${entityName}]` : entityName
typeDef += `
extend type ${extendedEntityName} {
${linkTableFieldName}: ${type}
}
`
}
return typeDef
}
function getGraphQLType(type) {
const typeDef = {
numeric: "Float",
integer: "Int",
smallint: "Int",
tinyint: "Int",
mediumint: "Int",
float: "Float",
double: "Float",
boolean: "Boolean",
decimal: "Float",
string: "String",
uuid: "ID",
text: "String",
date: "Date",
time: "Time",
datetime: "DateTime",
bigint: "BigInt",
blob: "Blob",
uint8array: "[Int]",
array: "[String]",
enumArray: "[String]",
enum: "String",
json: "JSON",
jsonb: "JSON",
}
return typeDef[type] ?? "String"
}

View File

@@ -2,6 +2,7 @@ import { MODULE_RESOURCE_TYPE } from "@medusajs/types"
export * from "./compose-link-name"
export * from "./generate-entity"
export * from "./generate-schema"
export function shouldForceTransaction(target: any): boolean {
return target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED

View File

@@ -1,77 +1,5 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import * as joinerConfigs from "./joiner-configs"
export const joinerConfig: ModuleJoinerConfig[] = [
{
serviceName: "cartService",
primaryKeys: ["id"],
linkableKeys: ["cart_id"],
alias: [
{
name: "cart",
},
],
relationships: [
{
serviceName: Modules.PRODUCT,
primaryKey: "id",
foreignKey: "variant_id",
alias: "variant",
args: {
methodSuffix: "Variants",
},
},
{
serviceName: "regionService",
primaryKey: "id",
foreignKey: "region_id",
alias: "region",
},
{
serviceName: "customerService",
primaryKey: "id",
foreignKey: "customer_id",
alias: "customer",
},
],
},
{
serviceName: "shippingProfileService",
primaryKeys: ["id"],
linkableKeys: ["profile_id"],
alias: [
{
name: "shipping_profile",
},
{
name: "shipping_profiles",
},
],
},
{
serviceName: "regionService",
primaryKeys: ["id"],
linkableKeys: ["region_id"],
alias: [
{
name: "region",
},
{
name: "regions",
},
],
},
{
serviceName: "customerService",
primaryKeys: ["id"],
linkableKeys: ["customer_id"],
alias: [
{
name: "customer",
},
{
name: "customers",
},
],
},
]
export const joinerConfig = Object.values(joinerConfigs).map(
(config) => config.default
)

View File

@@ -0,0 +1,36 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
export default {
serviceName: "cartService",
primaryKeys: ["id"],
linkableKeys: { cart_id: "Cart" },
alias: [
{
name: "cart",
},
],
relationships: [
{
serviceName: Modules.PRODUCT,
primaryKey: "id",
foreignKey: "variant_id",
alias: "variant",
args: {
methodSuffix: "Variants",
},
},
{
serviceName: "regionService",
primaryKey: "id",
foreignKey: "region_id",
alias: "region",
},
{
serviceName: "customerService",
primaryKey: "id",
foreignKey: "customer_id",
alias: "customer",
},
],
} as ModuleJoinerConfig

View File

@@ -0,0 +1,15 @@
import { ModuleJoinerConfig } from "@medusajs/types"
export default {
serviceName: "customerService",
primaryKeys: ["id"],
linkableKeys: { customer_id: "Customer" },
alias: [
{
name: "customer",
},
{
name: "customers",
},
],
} as ModuleJoinerConfig

View File

@@ -0,0 +1,4 @@
export * as cart from "./cart-service"
export * as customer from "./customer-service"
export * as region from "./region-service"
export * as shippingProfile from "./shipping-profile-service"

View File

@@ -0,0 +1,15 @@
import { ModuleJoinerConfig } from "@medusajs/types"
export default {
serviceName: "regionService",
primaryKeys: ["id"],
linkableKeys: { region_id: "Region" },
alias: [
{
name: "region",
},
{
name: "regions",
},
],
} as ModuleJoinerConfig

View File

@@ -0,0 +1,29 @@
import { ModuleJoinerConfig } from "@medusajs/types"
export default {
serviceName: "shippingProfileService",
primaryKeys: ["id"],
linkableKeys: { profile_id: "ShippingProfile" },
schema: `
scalar Date
scalar JSON
type ShippingProfile {
id: ID!
name: String!
type: String!
created_at: Date!
updated_at: Date!
deleted_at: Date
metadata: JSON
}
`,
alias: [
{
name: "shipping_profile",
},
{
name: "shipping_profiles",
},
],
} as ModuleJoinerConfig

View File

@@ -16,11 +16,11 @@ export const InventoryModule = {
__joinerConfig: {
serviceName: "inventoryService",
primaryKeys: ["id"],
linkableKeys: [
"inventory_item_id",
"inventory_level_id",
"reservation_item_id",
],
linkableKeys: {
inventory_item_id: "InventoryItem",
inventory_level_id: "InventoryLevel",
reservation_item_id: "ReservationItem",
},
},
softDelete: jest.fn(() => {}),

View File

@@ -16,7 +16,7 @@ export const ProductModule = {
__joinerConfig: {
serviceName: "productService",
primaryKeys: ["id", "handle"],
linkableKeys: ["product_id", "variant_id"],
linkableKeys: { product_id: "Product", variant_id: "ProductVariant" },
alias: [],
},

View File

@@ -16,7 +16,7 @@ export const StockLocationModule = {
__joinerConfig: {
serviceName: "stockLocationService",
primaryKeys: ["id"],
linkableKeys: ["stock_location_id"],
linkableKeys: { stock_location_id: "StockLocation" },
alias: [],
},

View File

@@ -0,0 +1,111 @@
import { cleanGraphQLSchema } from "../utils/clean-graphql-schema"
describe("Clean Graphql Schema", function () {
it("Should keep the schema intact if all entities are available", function () {
const schemaStr = `
type Product {
id: ID!
title: String!
variants: [Variant]!
}
type Variant {
id: ID!
title: String!
product_id: ID!
product: Product!
}
`
const { schema, notFound } = cleanGraphQLSchema(schemaStr)
expect(schema.replace(/\s/g, "")).toEqual(schemaStr.replace(/\s/g, ""))
expect(notFound).toEqual({})
})
it("Should remove fields where the relation doesn't exist", function () {
const schemaStr = `
type Product {
id: ID!
title: String!
variants: [Variant!]!
profile: ShippingProfile!
}
`
const expectedStr = `
type Product {
id: ID!
title: String!
}
`
const { schema, notFound } = cleanGraphQLSchema(schemaStr)
expect(schema.replace(/\s/g, "")).toEqual(expectedStr.replace(/\s/g, ""))
expect(notFound).toEqual({
Product: { variants: "Variant", profile: "ShippingProfile" },
})
})
it("Should remove fields where the relation doesn't exist and flag extended entity where the main entity doesn't exist", function () {
const schemaStr = `
scalar JSON
type Product {
id: ID!
title: String!
variants: [Variant!]!
profile: ShippingProfile!
}
extend type Variant {
metadata: JSON
}
`
const expectedStr = `
scalar JSON
type Product {
id: ID!
title: String!
}
`
const { schema, notFound } = cleanGraphQLSchema(schemaStr)
expect(schema.replace(/\s/g, "")).toEqual(expectedStr.replace(/\s/g, ""))
expect(notFound).toEqual({
Product: { variants: "Variant", profile: "ShippingProfile" },
Variant: { __extended: "" },
})
})
it("Should remove fields from extend where the relation doesn't exist", function () {
const schemaStr = `
scalar JSON
type Product {
id: ID!
title: String!
variants: [Variant!]!
profile: ShippingProfile!
}
extend type Product {
variants: [Variant!]!
profile: ShippingProfile!
metadata: JSON
}
`
const expectedStr = `
scalar JSON
type Product {
id: ID!
title: String!
}
extend type Product {
metadata: JSON
}
`
const { schema, notFound } = cleanGraphQLSchema(schemaStr)
expect(schema.replace(/\s/g, "")).toEqual(expectedStr.replace(/\s/g, ""))
expect(notFound).toEqual({
Product: { variants: "Variant", profile: "ShippingProfile" },
})
})
})

View File

@@ -1,3 +1,5 @@
import { mergeTypeDefs } from "@graphql-tools/merge"
import { makeExecutableSchema } from "@graphql-tools/schema"
import { RemoteFetchDataCallback } from "@medusajs/orchestration"
import {
ExternalModuleDeclaration,
@@ -12,13 +14,14 @@ import {
} from "@medusajs/types"
import {
ContainerRegistrationKeys,
isObject,
ModulesSdkUtils,
isObject,
} from "@medusajs/utils"
import { MODULE_PACKAGE_NAMES, Modules } from "./definitions"
import { MedusaModule } from "./medusa-module"
import { RemoteLink } from "./remote-link"
import { RemoteQuery } from "./remote-query"
import { cleanGraphQLSchema } from "./utils"
export type MedusaModuleConfig = {
[key: string | Modules]:
@@ -46,72 +49,19 @@ export type SharedResources = {
}
}
export async function MedusaApp({
sharedResourcesConfig,
servicesConfig,
modulesConfigPath,
modulesConfigFileName,
modulesConfig,
linkModules,
remoteFetchData,
injectedDependencies = {},
}: {
sharedResourcesConfig?: SharedResources
loadedModules?: LoadedModule[]
servicesConfig?: ModuleJoinerConfig[]
modulesConfigPath?: string
modulesConfigFileName?: string
modulesConfig?: MedusaModuleConfig
linkModules?: ModuleJoinerConfig | ModuleJoinerConfig[]
remoteFetchData?: RemoteFetchDataCallback
injectedDependencies?: any
}): Promise<{
modules: Record<string, LoadedModule | LoadedModule[]>
link: RemoteLink | undefined
query: (
query: string | RemoteJoinerQuery | object,
variables?: Record<string, unknown>
) => Promise<any>
}> {
const modules: MedusaModuleConfig =
modulesConfig ??
(
await import(
modulesConfigPath ??
process.cwd() + (modulesConfigFileName ?? "/modules-config")
)
).default
const dbData = ModulesSdkUtils.loadDatabaseConfig(
"medusa",
sharedResourcesConfig as ModuleServiceInitializeOptions,
true
)!
if (
dbData.clientUrl &&
!injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION]
) {
injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION] =
ModulesSdkUtils.createPgConnection({
...(sharedResourcesConfig?.database ?? {}),
...dbData,
})
}
const allModules: Record<string, LoadedModule | LoadedModule[]> = {}
async function loadModules(modulesConfig, injectedDependencies) {
const allModules = {}
await Promise.all(
Object.keys(modules).map(async (moduleName) => {
const mod = modules[moduleName] as MedusaModuleConfig
Object.keys(modulesConfig).map(async (moduleName) => {
const mod = modulesConfig[moduleName]
let path: string
let declaration: any = {}
let definition: ModuleDefinition | undefined = undefined
if (isObject(mod)) {
const mod_ = mod as unknown as InternalModuleDeclaration
path = mod_.resolve ?? MODULE_PACKAGE_NAMES[moduleName]
definition = mod_.definition
declaration = { ...mod }
delete declaration.definition
} else {
@@ -119,7 +69,6 @@ export async function MedusaApp({
}
declaration.scope ??= MODULE_SCOPE.INTERNAL
if (
declaration.scope === MODULE_SCOPE.INTERNAL &&
!declaration.resources
@@ -133,7 +82,7 @@ export async function MedusaApp({
declaration,
undefined,
injectedDependencies,
(isObject(mod) ? mod.definition : undefined) as ModuleDefinition
definition
)) as LoadedModule
if (allModules[moduleName] && !Array.isArray(allModules[moduleName])) {
@@ -145,33 +94,117 @@ export async function MedusaApp({
} else {
allModules[moduleName] = loaded[moduleName]
}
return loaded
})
)
return allModules
}
let link: RemoteLink | undefined = undefined
let query: (
query: string | RemoteJoinerQuery | object,
variables?: Record<string, unknown>
) => Promise<any>
async function initializeLinks(linkModules, injectedDependencies) {
try {
const { initialize: initializeLinks } = await import(
"@medusajs/link-modules" as string
)
await initializeLinks({}, linkModules, injectedDependencies)
link = new RemoteLink()
return new RemoteLink()
} catch (err) {
console.warn("Error initializing link modules.", err)
return undefined
}
}
function cleanAndMergeSchema(loadedSchema) {
const { schema: cleanedSchema, notFound } = cleanGraphQLSchema(loadedSchema)
const mergedSchema = mergeTypeDefs(cleanedSchema)
return { schema: makeExecutableSchema({ typeDefs: mergedSchema }), notFound }
}
function getLoadedSchema(): string {
return MedusaModule.getAllJoinerConfigs()
.map((joinerConfig) => joinerConfig?.schema ?? "")
.join("\n")
}
function registerCustomJoinerConfigs(servicesConfig: ModuleJoinerConfig[]) {
for (const config of servicesConfig) {
if (!config.serviceName || config.isReadOnlyLink) {
continue
}
MedusaModule.setJoinerConfig(config.serviceName, config)
}
}
export async function MedusaApp(
{
sharedResourcesConfig,
servicesConfig,
modulesConfigPath,
modulesConfigFileName,
modulesConfig,
linkModules,
remoteFetchData,
injectedDependencies,
}: {
sharedResourcesConfig?: SharedResources
loadedModules?: LoadedModule[]
servicesConfig?: ModuleJoinerConfig[]
modulesConfigPath?: string
modulesConfigFileName?: string
modulesConfig?: MedusaModuleConfig
linkModules?: ModuleJoinerConfig | ModuleJoinerConfig[]
remoteFetchData?: RemoteFetchDataCallback
injectedDependencies?: any
} = {
injectedDependencies: {},
}
): Promise<{
modules: Record<string, LoadedModule | LoadedModule[]>
link: RemoteLink | undefined
query: (
query: string | RemoteJoinerQuery | object,
variables?: Record<string, unknown>
) => Promise<any>
entitiesMap?: Record<string, any>
notFound?: Record<string, Record<string, string>>
}> {
const modules: MedusaModuleConfig =
modulesConfig ??
(
await import(
modulesConfigPath ??
process.cwd() + (modulesConfigFileName ?? "/modules-config")
)
).default
const dbData = ModulesSdkUtils.loadDatabaseConfig(
"medusa",
sharedResourcesConfig as ModuleServiceInitializeOptions,
true
)!
registerCustomJoinerConfigs(servicesConfig ?? [])
if (
dbData.clientUrl &&
!injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION]
) {
injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION] =
ModulesSdkUtils.createPgConnection({
...(sharedResourcesConfig?.database ?? {}),
...dbData,
})
}
const allModules = await loadModules(modules, injectedDependencies)
const link = await initializeLinks(linkModules, injectedDependencies)
const loadedSchema = getLoadedSchema()
const { schema, notFound } = cleanAndMergeSchema(loadedSchema)
const remoteQuery = new RemoteQuery({
servicesConfig,
customRemoteFetchData: remoteFetchData,
})
query = async (
const query = async (
query: string | RemoteJoinerQuery | object,
variables?: Record<string, unknown>
) => {
@@ -182,5 +215,7 @@ export async function MedusaApp({
modules: allModules,
link,
query,
entitiesMap: schema.getTypeMap(),
notFound,
}
}

View File

@@ -52,6 +52,7 @@ export class MedusaModule {
private static instances_: Map<string, any> = new Map()
private static modules_: Map<string, ModuleAlias[]> = new Map()
private static loading_: Map<string, Promise<any>> = new Map()
private static joinerConfig_: Map<string, ModuleJoinerConfig> = new Map()
public static getLoadedModules(
aliases?: Map<string, string>
@@ -68,6 +69,7 @@ export class MedusaModule {
public static clearInstances(): void {
MedusaModule.instances_.clear()
MedusaModule.modules_.clear()
MedusaModule.joinerConfig_.clear()
}
public static isInstalled(moduleKey: string, alias?: string): boolean {
@@ -81,6 +83,22 @@ export class MedusaModule {
return MedusaModule.modules_.has(moduleKey)
}
public static getJoinerConfig(moduleKey: string): ModuleJoinerConfig {
return MedusaModule.joinerConfig_.get(moduleKey)!
}
public static getAllJoinerConfigs(): ModuleJoinerConfig[] {
return [...MedusaModule.joinerConfig_.values()]
}
public static setJoinerConfig(
moduleKey: string,
config: ModuleJoinerConfig
): ModuleJoinerConfig {
MedusaModule.joinerConfig_.set(moduleKey, config)
return config
}
public static getModuleInstance(
moduleKey: string,
alias?: string
@@ -218,6 +236,7 @@ export class MedusaModule {
].__joinerConfig()
services[keyName].__joinerConfig = joinerConfig
MedusaModule.setJoinerConfig(keyName, joinerConfig)
}
MedusaModule.registerModule(keyName, {
@@ -329,6 +348,7 @@ export class MedusaModule {
].__joinerConfig()
services[keyName].__joinerConfig = joinerConfig
MedusaModule.setJoinerConfig(keyName, joinerConfig)
if (!joinerConfig.isLink) {
throw new Error(

View File

@@ -147,7 +147,10 @@ export class RemoteLink {
private getLinkableKeys(mod: LoadedLinkModule) {
return (
mod.__joinerConfig.linkableKeys ?? mod.__joinerConfig.primaryKeys ?? []
(mod.__joinerConfig.linkableKeys &&
Object.keys(mod.__joinerConfig.linkableKeys)) ||
mod.__joinerConfig.primaryKeys ||
[]
)
}

View File

@@ -0,0 +1,91 @@
import { Kind, parse, print, visit } from "graphql"
export function cleanGraphQLSchema(schema: string): {
schema: string
notFound: Record<string, Record<string, string>>
} {
const extractTypeNameAndKind = (type) => {
if (type.kind === Kind.NAMED_TYPE) {
return [type.name.value, type.kind]
}
if (type.kind === Kind.NON_NULL_TYPE || type.kind === Kind.LIST_TYPE) {
return extractTypeNameAndKind(type.type)
}
return [null, null]
}
const ast = parse(schema)
const typeNames = new Set(["String", "Int", "Float", "Boolean", "ID"])
const extendedTypes = new Set()
const kinds = [
Kind.OBJECT_TYPE_DEFINITION,
Kind.INTERFACE_TYPE_DEFINITION,
Kind.ENUM_TYPE_DEFINITION,
Kind.SCALAR_TYPE_DEFINITION,
Kind.INPUT_OBJECT_TYPE_DEFINITION,
Kind.UNION_TYPE_DEFINITION,
]
ast.definitions.forEach((def: any) => {
if (kinds.includes(def.kind)) {
typeNames.add(def.name.value)
} else if (def.kind === Kind.OBJECT_TYPE_EXTENSION) {
extendedTypes.add(def.name.value)
}
})
const nonExistingMap: Record<string, Record<string, string>> = {}
const parentStack: string[] = []
/*
Traverse the graph mapping all the entities + fields and removing the ones that don't exist.
Extensions are not removed, but marked with a "__extended" key if the main entity doesn't exist. (example: Link modules injecting fields into another module)
*/
const cleanedAst = visit(ast, {
ObjectTypeExtension: {
enter(node) {
const typeName = node.name.value
parentStack.push(typeName)
if (!typeNames.has(typeName)) {
nonExistingMap[typeName] ??= {}
nonExistingMap[typeName]["__extended"] = ""
return null
}
return
},
leave() {
parentStack.pop()
},
},
ObjectTypeDefinition: {
enter(node) {
parentStack.push(node.name.value)
},
leave() {
parentStack.pop()
},
},
FieldDefinition: {
leave(node) {
const [typeName, kind] = extractTypeNameAndKind(node.type)
if (!typeNames.has(typeName) && kind === Kind.NAMED_TYPE) {
const currentParent = parentStack[parentStack.length - 1]
nonExistingMap[currentParent] ??= {}
nonExistingMap[currentParent][node.name.value] = typeName
return null
}
return
},
},
})
// Return the schema and the map of non existing entities and fields
return {
schema: print(cleanedAst),
notFound: nonExistingMap,
}
}

View File

@@ -1,3 +1,3 @@
export * from "./clean-graphql-schema"
export * from "./get-fields-and-relations"
export * from "./graphql-schema-to-fields"

View File

@@ -1,30 +1,27 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { MapToConfig } from "@medusajs/utils"
import * as Models from "@models"
import { Currency, MoneyAmount, PriceSet } from "@models"
export enum LinkableKeys {
MONEY_AMOUNT_ID = "money_amount_id",
CURRENCY_CODE = "currency_code",
PRICE_SET_ID = "price_set_id",
}
export const entityNameToLinkableKeysMap: MapToConfig = {
[Models.PriceSet.name]: [
{ mapTo: LinkableKeys.PRICE_SET_ID, valueFrom: "id" },
],
[Models.Currency.name]: [
{ mapTo: LinkableKeys.CURRENCY_CODE, valueFrom: "code" },
],
[Models.MoneyAmount.name]: [
{ mapTo: LinkableKeys.MONEY_AMOUNT_ID, valueFrom: "id" },
],
export const LinkableKeys = {
money_amount_id: MoneyAmount.name,
currency_code: Currency.name,
price_set_id: PriceSet.name,
}
const entityLinkableKeysMap: MapToConfig = {}
Object.entries(LinkableKeys).forEach(([key, value]) => {
entityLinkableKeysMap[value] ??= []
entityLinkableKeysMap[value].push({
mapTo: key,
valueFrom: key.split("_").pop()!,
})
})
export const entityNameToLinkableKeysMap: MapToConfig = entityLinkableKeysMap
export const joinerConfig: ModuleJoinerConfig = {
serviceName: Modules.PRICING,
primaryKeys: ["id", "currency_code"],
linkableKeys: Object.values(LinkableKeys),
linkableKeys: LinkableKeys,
alias: [
{
name: "price_set",

View File

@@ -11,54 +11,36 @@ import {
ProductVariant,
} from "@models"
import ProductImage from "./models/product-image"
import moduleSchema from "./schema"
export enum LinkableKeys {
PRODUCT_ID = "product_id", // Main service ID must the first
PRODUCT_HANDLE = "product_handle",
VARIANT_ID = "variant_id",
VARIANT_SKU = "variant_sku",
PRODUCT_OPTION_ID = "product_option_id",
PRODUCT_TYPE_ID = "product_type_id",
PRODUCT_CATEGORY_ID = "product_category_id",
PRODUCT_COLLECTION_ID = "product_collection_id",
PRODUCT_TAG_ID = "product_tag_id",
PRODUCT_IMAGE_ID = "product_image_id",
export const LinkableKeys = {
product_id: Product.name,
product_handle: Product.name,
variant_id: ProductVariant.name,
variant_sku: ProductVariant.name,
product_option_id: ProductOption.name,
product_type_id: ProductType.name,
product_category_id: ProductCategory.name,
product_collection_id: ProductCollection.name,
product_tag_id: ProductTag.name,
product_image_id: ProductImage.name,
}
export const entityNameToLinkableKeysMap: MapToConfig = {
[Product.name]: [
{ mapTo: LinkableKeys.PRODUCT_ID, valueFrom: "id" },
{
mapTo: LinkableKeys.PRODUCT_HANDLE,
valueFrom: "handle",
},
],
[ProductVariant.name]: [
{ mapTo: LinkableKeys.VARIANT_ID, valueFrom: "id" },
{ mapTo: LinkableKeys.VARIANT_SKU, valueFrom: "sku" },
],
[ProductOption.name]: [
{ mapTo: LinkableKeys.PRODUCT_OPTION_ID, valueFrom: "id" },
],
[ProductType.name]: [
{ mapTo: LinkableKeys.PRODUCT_TYPE_ID, valueFrom: "id" },
],
[ProductCategory.name]: [
{ mapTo: LinkableKeys.PRODUCT_CATEGORY_ID, valueFrom: "id" },
],
[ProductCollection.name]: [
{ mapTo: LinkableKeys.PRODUCT_COLLECTION_ID, valueFrom: "id" },
],
[ProductTag.name]: [{ mapTo: LinkableKeys.PRODUCT_TAG_ID, valueFrom: "id" }],
[ProductImage.name]: [
{ mapTo: LinkableKeys.PRODUCT_IMAGE_ID, valueFrom: "id" },
],
}
const entityLinkableKeysMap: MapToConfig = {}
Object.entries(LinkableKeys).forEach(([key, value]) => {
entityLinkableKeysMap[value] ??= []
entityLinkableKeysMap[value].push({
mapTo: key,
valueFrom: key.split("_").pop()!,
})
})
export const entityNameToLinkableKeysMap: MapToConfig = entityLinkableKeysMap
export const joinerConfig: ModuleJoinerConfig = {
serviceName: Modules.PRODUCT,
primaryKeys: ["id", "handle"],
linkableKeys: Object.values(LinkableKeys),
linkableKeys: LinkableKeys,
schema: moduleSchema,
alias: [
{
name: "product",

View File

@@ -0,0 +1,154 @@
export default `
scalar Date
scalar JSON
enum ProductStatus {
DRAFT
PUBLISHED
ARCHIVED
}
type Product {
id: ID!
title: String!
handle: String
subtitle: String
description: String
isGiftcard: Boolean!
status: ProductStatus!
thumbnail: String
options: [ProductOption]
variants: [ProductVariant]
weight: Float
length: Float
height: Float
width: Float
originCountry: String
hsCode: String
midCode: String
material: String
collectionId: String
collection: ProductCollection
typeId: String
type: ProductType!
tags: [ProductTag]
images: [ProductImage]
categories: [ProductCategory]
discountable: Boolean!
externalId: String
createdAt: Date!
updatedAt: Date!
deletedAt: Date
metadata: JSON
}
type ProductVariant {
id: ID!
title: String!
sku: String
barcode: String
ean: String
upc: String
inventoryQuantity: Float!
allowBackorder: Boolean!
manageInventory: Boolean!
hsCode: String
originCountry: String
midCode: String
material: String
weight: Float
length: Float
height: Float
width: Float
metadata: JSON
variantRank: Float
productId: String!
createdAt: Date!
updatedAt: Date!
deletedAt: Date
product: Product!
options: [ProductOptionValue]
}
type ProductType {
id: ID!
value: String!
metadata: JSON
createdAt: Date!
updatedAt: Date!
deletedAt: Date
}
type ProductTag {
id: ID!
value: String!
metadata: JSON
createdAt: Date!
updatedAt: Date!
deletedAt: Date
products: [Product]
}
type ProductOption {
id: ID!
title: String!
productId: String!
product: Product!
values: [ProductOptionValue]
metadata: JSON
createdAt: Date!
updatedAt: Date!
deletedAt: Date
}
type ProductOptionValue {
id: ID!
value: String!
optionId: String!
option: ProductOption!
variantId: String!
variant: ProductVariant!
metadata: JSON
createdAt: Date!
updatedAt: Date!
deletedAt: Date
}
type ProductImage {
id: ID!
url: String!
metadata: JSON
createdAt: Date!
updatedAt: Date!
deletedAt: Date
products: [Product]
}
type ProductCollection {
id: ID!
title: String!
handle: String!
products: [Product]
metadata: JSON
createdAt: Date!
updatedAt: Date!
deletedAt: Date
}
type ProductCategory {
id: ID!
name: String!
description: String!
handle: String!
mpath: String!
isActive: Boolean!
isInternal: Boolean!
rank: Float!
parentCategoryId: String
parentCategory: ProductCategory
categoryChildren: [ProductCategory]
createdAt: Date!
updatedAt: Date!
products: [Product]
}
`

View File

@@ -1,10 +1,11 @@
import { ModuleJoinerConfig } from "@medusajs/types"
import { Modules } from "@medusajs/modules-sdk"
import { StockLocation } from "./models"
export const joinerConfig: ModuleJoinerConfig = {
serviceName: Modules.STOCK_LOCATION,
primaryKeys: ["id"],
linkableKeys: ["stock_location_id"],
linkableKeys: { stock_location_id: StockLocation.name },
alias: [
{
name: "stock_location",

View File

@@ -133,6 +133,10 @@ export type ModuleJoinerConfig = Omit<
JoinerServiceConfig,
"serviceName" | "primaryKeys" | "relationships" | "extends"
> & {
/**
* GraphQL schema for the all module's available entities and fields
*/
schema?: string
relationships?: ModuleJoinerRelationship[]
extends?: {
serviceName: string
@@ -153,9 +157,9 @@ export type ModuleJoinerConfig = Omit<
*/
isLink?: boolean
/**
* Keys that can be used to link to other modules
* Keys that can be used to link to other modules. e.g { product_id: "Product" } "Product" being the entity it refers to
*/
linkableKeys?: string[]
linkableKeys?: Record<string, string>
/**
* If true it expands a RemoteQuery property but doesn't create a pivot table
*/

View File

@@ -0,0 +1,2 @@
export const camelToSnakeCase = (string) =>
string.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)

View File

@@ -22,6 +22,7 @@ export * from "./set-metadata"
export * from "./simple-hash"
export * from "./string-to-select-relation-object"
export * from "./stringify-circular"
export * from "./camel-to-snake-case"
export * from "./to-camel-case"
export * from "./to-kebab-case"
export * from "./to-pascal-case"