What:
- Definition of all Modules links
- `link-modules` package to manage the creation of all pre-defined link or custom ones
```typescript
import { initialize as iniInventory } from "@medusajs/inventory";
import { initialize as iniProduct } from "@medusajs/product";
import {
initialize as iniLinks,
runMigrations as migrateLinks
} from "@medusajs/link-modules";
await Promise.all([iniInventory(), iniProduct()]);
await migrateLinks(); // create tables based on previous loaded modules
await iniLinks(); // load link based on previous loaded modules
await iniLinks(undefined, [
{
serviceName: "product_custom_translation_service_link",
isLink: true,
databaseConfig: {
tableName: "product_transalations",
},
alias: [
{
name: "translations",
},
],
primaryKeys: ["id", "product_id", "translation_id"],
relationships: [
{
serviceName: Modules.PRODUCT,
primaryKey: "id",
foreignKey: "product_id",
alias: "product",
},
{
serviceName: "custom_translation_service",
primaryKey: "id",
foreignKey: "translation_id",
alias: "transalation",
deleteCascade: true,
},
],
extends: [
{
serviceName: Modules.PRODUCT,
relationship: {
serviceName: "product_custom_translation_service_link",
primaryKey: "product_id",
foreignKey: "id",
alias: "translations",
isList: true,
},
},
{
serviceName: "custom_translation_service",
relationship: {
serviceName: "product_custom_translation_service_link",
primaryKey: "product_id",
foreignKey: "id",
alias: "product_link",
},
},
],
},
]); // custom links
```
Remote Link
```typescript
import { RemoteLink, Modules } from "@medusajs/modules-sdk";
// [...] initialize modules and links
const remoteLink = new RemoteLink();
// upsert the relationship
await remoteLink.create({ // one (object) or many (array)
[Modules.PRODUCT]: {
variant_id: "var_abc",
},
[Modules.INVENTORY]: {
inventory_item_id: "iitem_abc",
},
data: { // optional additional fields
required_quantity: 5
}
});
// dismiss (doesn't cascade)
await remoteLink.dismiss({ // one (object) or many (array)
[Modules.PRODUCT]: {
variant_id: "var_abc",
},
[Modules.INVENTORY]: {
inventory_item_id: "iitem_abc",
},
});
// delete
await remoteLink.delete({
// every key is a module
[Modules.PRODUCT]: {
// every key is a linkable field
variant_id: "var_abc", // single or multiple values
},
});
// restore
await remoteLink.restore({
// every key is a module
[Modules.PRODUCT]: {
// every key is a linkable field
variant_id: "var_abc", // single or multiple values
},
});
```
Co-authored-by: Riqwan Thamir <5105988+riqwan@users.noreply.github.com>
195 lines
4.8 KiB
TypeScript
195 lines
4.8 KiB
TypeScript
import { RemoteJoinerQuery } from "@medusajs/types"
|
|
import {
|
|
ArgumentNode,
|
|
DirectiveNode,
|
|
DocumentNode,
|
|
FieldNode,
|
|
Kind,
|
|
OperationDefinitionNode,
|
|
SelectionSetNode,
|
|
ValueNode,
|
|
parse,
|
|
} from "graphql"
|
|
|
|
interface Argument {
|
|
name: string
|
|
value?: unknown
|
|
}
|
|
|
|
interface Directive {
|
|
name: string
|
|
args?: Argument[]
|
|
}
|
|
|
|
interface Entity {
|
|
property: string
|
|
fields: string[]
|
|
args?: Argument[]
|
|
directives?: { [field: string]: Directive[] }
|
|
}
|
|
|
|
class GraphQLParser {
|
|
private ast: DocumentNode
|
|
|
|
constructor(input: string, private variables: Record<string, unknown> = {}) {
|
|
this.ast = parse(input)
|
|
this.variables = variables
|
|
}
|
|
|
|
private parseValueNode(valueNode: ValueNode): unknown {
|
|
switch (valueNode.kind) {
|
|
case Kind.VARIABLE:
|
|
const variableName = valueNode.name.value
|
|
return this.variables ? this.variables[variableName] : undefined
|
|
case Kind.INT:
|
|
return parseInt(valueNode.value, 10)
|
|
case Kind.FLOAT:
|
|
return parseFloat(valueNode.value)
|
|
case Kind.BOOLEAN:
|
|
return Boolean(valueNode.value)
|
|
case Kind.STRING:
|
|
case Kind.ENUM:
|
|
return valueNode.value
|
|
case Kind.NULL:
|
|
return null
|
|
case Kind.LIST:
|
|
return valueNode.values.map((v) => this.parseValueNode(v))
|
|
case Kind.OBJECT:
|
|
let obj = {}
|
|
for (const field of valueNode.fields) {
|
|
obj[field.name.value] = this.parseValueNode(field.value)
|
|
}
|
|
return obj
|
|
default:
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
private parseArguments(
|
|
args: readonly ArgumentNode[]
|
|
): Argument[] | undefined {
|
|
if (!args.length) {
|
|
return
|
|
}
|
|
|
|
return args.map((arg) => {
|
|
const value = this.parseValueNode(arg.value)
|
|
|
|
return {
|
|
name: arg.name.value,
|
|
value: value,
|
|
}
|
|
})
|
|
}
|
|
|
|
private parseDirectives(directives: readonly DirectiveNode[]): Directive[] {
|
|
return directives.map((directive) => ({
|
|
name: directive.name.value,
|
|
args: this.parseArguments(directive.arguments || []),
|
|
}))
|
|
}
|
|
|
|
private createDirectivesMap(selectionSet: SelectionSetNode):
|
|
| {
|
|
[field: string]: Directive[]
|
|
}
|
|
| undefined {
|
|
const directivesMap: { [field: string]: Directive[] } = {}
|
|
let hasDirectives = false
|
|
selectionSet.selections.forEach((field) => {
|
|
const fieldName = (field as FieldNode).name.value
|
|
const fieldDirectives = this.parseDirectives(
|
|
(field as FieldNode).directives || []
|
|
)
|
|
if (fieldDirectives.length > 0) {
|
|
hasDirectives = true
|
|
directivesMap[fieldName] = fieldDirectives
|
|
}
|
|
})
|
|
return hasDirectives ? directivesMap : undefined
|
|
}
|
|
|
|
private extractEntities(
|
|
node: SelectionSetNode,
|
|
parentName = "",
|
|
mainService = ""
|
|
): Entity[] {
|
|
const entities: Entity[] = []
|
|
|
|
node.selections.forEach((selection) => {
|
|
if (selection.kind === "Field") {
|
|
const fieldNode = selection as FieldNode
|
|
|
|
if (!fieldNode.selectionSet) {
|
|
return
|
|
}
|
|
|
|
const propName = fieldNode.name.value
|
|
const entityName = parentName ? `${parentName}.${propName}` : propName
|
|
|
|
const nestedEntity: Entity = {
|
|
property: entityName.replace(`${mainService}.`, ""),
|
|
fields: fieldNode.selectionSet.selections.map(
|
|
(field) => (field as FieldNode).name.value
|
|
),
|
|
args: this.parseArguments(fieldNode.arguments || []),
|
|
directives: this.createDirectivesMap(fieldNode.selectionSet),
|
|
}
|
|
|
|
entities.push(nestedEntity)
|
|
entities.push(
|
|
...this.extractEntities(
|
|
fieldNode.selectionSet,
|
|
entityName,
|
|
mainService
|
|
)
|
|
)
|
|
}
|
|
})
|
|
|
|
return entities
|
|
}
|
|
|
|
public parseQuery(): RemoteJoinerQuery {
|
|
const queryDefinition = this.ast.definitions.find(
|
|
(definition) => definition.kind === "OperationDefinition"
|
|
) as OperationDefinitionNode
|
|
|
|
if (!queryDefinition) {
|
|
throw new Error("No query found")
|
|
}
|
|
|
|
const rootFieldNode = queryDefinition.selectionSet
|
|
.selections[0] as FieldNode
|
|
const propName = rootFieldNode.name.value
|
|
|
|
const remoteJoinConfig: RemoteJoinerQuery = {
|
|
alias: propName,
|
|
fields: [],
|
|
expands: [],
|
|
}
|
|
|
|
if (rootFieldNode.arguments) {
|
|
remoteJoinConfig.args = this.parseArguments(rootFieldNode.arguments)
|
|
}
|
|
|
|
if (rootFieldNode.selectionSet) {
|
|
remoteJoinConfig.fields = rootFieldNode.selectionSet.selections.map(
|
|
(field) => (field as FieldNode).name.value
|
|
)
|
|
remoteJoinConfig.directives = this.createDirectivesMap(
|
|
rootFieldNode.selectionSet
|
|
)
|
|
remoteJoinConfig.expands = this.extractEntities(
|
|
rootFieldNode.selectionSet,
|
|
propName,
|
|
propName
|
|
)
|
|
}
|
|
|
|
return remoteJoinConfig
|
|
}
|
|
}
|
|
|
|
export default GraphQLParser
|