Files
medusa-store/packages/orchestration/src/joiner/graphql-ast.ts
Carlos R. L. Rodrigues 4d16acf5f0 feat(link-modules,modules-sdk, utils, types, products) - Remote Link and Link modules (#4695)
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>
2023-08-30 14:31:32 +00:00

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