feat: gatsby source medusa (#924)

This commit is contained in:
Kasper Fabricius Kristensen authored and GitHub committed 2021-12-14 20:56:31 +01:00
1 parent d0d8dd7bf6
commit b8ff364276
18 files changed
+12013

No files matched your search

+128
View File
@@ -0,0 +1,128 @@
import axios, { AxiosPromise, AxiosRequestConfig } from "axios"
import { Reporter } from "gatsby-cli/lib/reporter/reporter"
function medusaRequest(
storeURL: string,
path = "",
headers = {}
): AxiosPromise {
const options: AxiosRequestConfig = {
method: "GET",
withCredentials: true,
url: path,
headers: headers,
}
const client = axios.create({ baseURL: storeURL })
return client(options)
}
export const createClient = (
options: MedusaPluginOptions,
reporter: Reporter
): any => {
const { storeUrl, authToken } = options
/**
*
* @param {string} date used fetch products updated since the specified date
* @return {Promise<any[]>}
*/
async function products(date?: string): Promise<any[]> {
let products: any[] = []
let offset = 0
let count = 1
do {
await medusaRequest(storeUrl, `/store/products?offset=${offset}`)
.then(({ data }) => {
products = [...products, ...data.products]
count = data.count
offset = data.products.length
})
.catch((error) => {
reporter.error(
`"The following error status was produced while attempting to fetch products: ${error}`
)
return []
})
} while (products.length < count)
return products
}
/**
*
* @param {string} date used fetch regions updated since the specified date
* @return {Promise<any[]>}
*/
async function regions(date?: string): Promise<any[]> {
const regions = await medusaRequest(storeUrl, `/store/regions`)
.then(({ data }) => {
return data.regions
})
.catch((error) => {
console.warn(`
"The following error status was produced while attempting to fetch regions: ${error}
`)
return []
})
return regions
}
/**
*
* @param {string} date used fetch regions updated since the specified date
* @return {Promise<any[]>}
*/
async function orders(date?: string): Promise<any[]> {
const orders = await medusaRequest(storeUrl, `/admin/orders`, {
Authorization: `Bearer ${authToken}`,
})
.then(({ data }) => {
return data.orders
})
.catch((error) => {
console.warn(`
The following error status was produced while attempting to fetch orders: ${error}. \n
Make sure that the auth token you provided is valid.
`)
return []
})
return orders
}
/**
*
* @param {string} date used fetch regions updated since the specified date
* @return {Promise<any[]>}
*/
async function collections(date?: string): Promise<any[]> {
let collections: any[] = []
let offset = 0
let count = 1
do {
await medusaRequest(storeUrl, `/store/collections?offset=${offset}`)
.then(({ data }) => {
collections = [...collections, ...data.collections]
count = data.count
offset = data.collections.length
})
.catch((error) => {
reporter.error(
`"The following error status was produced while attempting to fetch products: ${error}`
)
return []
})
} while (collections.length < count)
return collections
}
return {
products,
collections,
regions,
orders,
}
}
@@ -0,0 +1,227 @@
import {
CreateResolversArgs,
GatsbyCache,
Node,
PluginOptionsSchemaArgs,
Reporter,
SourceNodesArgs,
Store,
} from "gatsby"
import { createRemoteFileNode } from "gatsby-source-filesystem"
import { makeSourceFromOperation } from "./make-source-from-operation"
import { createOperations } from "./operations"
export function pluginOptionsSchema({ Joi }: PluginOptionsSchemaArgs): any {
return Joi.object({
storeUrl: Joi.string().required(),
apiKey: Joi.string().optional(),
})
}
async function sourceAllNodes(
gatsbyApi: SourceNodesArgs,
pluginOptions: MedusaPluginOptions
): Promise<void> {
const {
createProductsOperation,
createRegionsOperation,
createOrdersOperation,
createCollectionsOperation,
} = createOperations(pluginOptions, gatsbyApi)
const operations = [
createProductsOperation,
createRegionsOperation,
createCollectionsOperation,
]
// if auth token is provided then source orders
if (pluginOptions.apiKey) {
operations.push(createOrdersOperation)
}
const sourceFromOperation = makeSourceFromOperation(gatsbyApi)
for (const op of operations) {
await sourceFromOperation(op)
}
}
const medusaNodeTypes = [
"MedusaRegions",
"MedusaProducts",
"MedusaOrders",
"MedusaCollections",
]
async function sourceUpdatedNodes(
gatsbyApi: SourceNodesArgs,
pluginOptions: MedusaPluginOptions
): Promise<void> {
const {
incrementalProductsOperation,
incrementalRegionsOperation,
incrementalOrdersOperation,
incrementalCollectionsOperation,
} = createOperations(pluginOptions, gatsbyApi)
const lastBuildTime = new Date(
gatsbyApi.store.getState().status.plugins?.[`gatsby-source-medusa`]?.[
`lastBuildTime`
]
)
for (const nodeType of medusaNodeTypes) {
gatsbyApi
.getNodesByType(nodeType)
.forEach((node) => gatsbyApi.actions.touchNode(node))
}
const operations = [
incrementalProductsOperation(lastBuildTime),
incrementalRegionsOperation(lastBuildTime),
incrementalCollectionsOperation(lastBuildTime),
]
if (pluginOptions.apiKey) {
operations.push(incrementalOrdersOperation(lastBuildTime))
}
const sourceFromOperation = makeSourceFromOperation(gatsbyApi)
for (const op of operations) {
await sourceFromOperation(op)
}
}
export async function sourceNodes(
gatsbyApi: SourceNodesArgs,
pluginOptions: MedusaPluginOptions
): Promise<void> {
const pluginStatus =
gatsbyApi.store.getState().status.plugins?.[`gatsby-source-medusa`]
const lastBuildTime = pluginStatus?.[`lastBuildTime`]
if (lastBuildTime !== undefined) {
gatsbyApi.reporter.info(
`Cache is warm, but incremental builds are currently not supported. Running a clean build.`
)
await sourceAllNodes(gatsbyApi, pluginOptions)
} else {
gatsbyApi.reporter.info(`Cache is cold, running a clean build.`)
await sourceAllNodes(gatsbyApi, pluginOptions)
}
gatsbyApi.reporter.info(`Finished sourcing nodes, caching last build time`)
gatsbyApi.actions.setPluginStatus(
pluginStatus !== undefined
? {
...pluginStatus,
[`lastBuildTime`]: Date.now(),
}
: {
[`lastBuildTime`]: Date.now(),
}
)
}
export function createResolvers({ createResolvers }: CreateResolversArgs): any {
const resolvers = {
MedusaProducts: {
images: {
type: ["MedusaImages"],
resolve: async (
source: any,
_args: any,
context: any,
_info: any
): Promise<any> => {
const { entries } = await context.nodeModel.findAll({
query: {
filter: { parent: { id: { eq: source.id } } },
},
type: "MedusaImages",
})
return entries
},
},
},
}
createResolvers(resolvers)
}
export async function createSchemaCustomization({
actions: { createTypes },
}: {
actions: { createTypes: any }
schema: any
}): Promise<void> {
createTypes(`
type MedusaProducts implements Node {
thumbnail: File @link(from: "fields.localThumbnail")
}
type MedusaImages implements Node {
image: File @link(from: "fields.localImage")
}
`)
}
export async function onCreateNode({
actions: { createNode, createNodeField },
cache,
createNodeId,
node,
store,
reporter,
}: {
actions: { createNode: any; createNodeField: any }
cache: GatsbyCache
createNodeId: any
node: Node
store: Store
reporter: Reporter
}): Promise<void> {
if (node.internal.type === `MedusaProducts`) {
if (node.thumbnail !== null) {
const thumbnailNode: Node | null = await createRemoteFileNode({
url: `${node.thumbnail}`,
parentNodeId: node.id,
createNode,
createNodeId,
cache,
store,
reporter,
})
if (thumbnailNode) {
createNodeField({
node,
name: `localThumbnail`,
value: thumbnailNode.id,
})
}
}
}
if (node.internal.type === `MedusaImages`) {
const imageNode: Node | null = await createRemoteFileNode({
url: `${node.url}`,
parentNodeId: node.id,
createNode,
createNodeId,
cache,
store,
reporter,
})
if (imageNode) {
createNodeField({
node,
name: `localImage`,
value: imageNode.id,
})
}
}
}
@@ -0,0 +1,25 @@
import { SourceNodesArgs } from "gatsby"
import { processNode } from "./process-node"
export function makeSourceFromOperation(gatsbyApi: SourceNodesArgs) {
return async function sourceFromOperation(
op: IMedusaOperation
): Promise<void> {
const { reporter, actions } = gatsbyApi
reporter.info(`Initiating operation query ${op.name}`)
const nodes = await op.execute()
nodes.map((rawNode) => {
const nodeArr = processNode(
rawNode,
op.name,
gatsbyApi.createContentDigest
)
nodeArr.forEach((node) => {
actions.createNode(node)
})
})
}
}
@@ -0,0 +1,34 @@
import { SourceNodesArgs } from "gatsby"
import { createClient } from "./client"
export function createOperations(
options: MedusaPluginOptions,
{ reporter }: SourceNodesArgs
): IOperations {
const client = createClient(options, reporter)
function createOperation(
name: "products" | "collections" | "regions" | "orders",
queryString?: string
): IMedusaOperation {
return {
execute: (): Promise<any[]> => client[name](queryString),
name: name,
}
}
return {
createProductsOperation: createOperation("products"),
createCollectionsOperation: createOperation("collections"),
createRegionsOperation: createOperation("regions"),
createOrdersOperation: createOperation("orders"),
incrementalProductsOperation: (date: Date): any =>
createOperation("products", date.toISOString()),
incrementalCollectionsOperation: (date: Date): any =>
createOperation("collections", date.toISOString()),
incrementalRegionsOperation: (date: Date): any =>
createOperation("regions", date.toISOString()),
incrementalOrdersOperation: (date: Date): any =>
createOperation("orders", date.toISOString()),
}
}
@@ -0,0 +1,49 @@
import { capitalize } from "./utils/capitalize"
export const processNode = (
node: any,
fieldName: string,
createContentDigest: (this: void, input: string | object) => string
): any[] => {
const nodeId: string = node.id
const nodeContent = JSON.stringify(node)
const nodeContentDigest = createContentDigest(nodeContent)
let images = []
if (fieldName === "products") {
if (node.images?.length) {
images = node.images.map((image: any) => {
const nodeImageContentDigest = createContentDigest(image.id)
const nodeImageContent = JSON.stringify(image)
const imageData = Object.assign({}, image, {
id: image.id,
parent: nodeId,
children: [],
internal: {
type: "MedusaImages",
content: nodeImageContent,
contentDigest: nodeImageContentDigest,
},
})
return imageData
})
}
delete node.images
}
const nodeData = Object.assign({}, node, {
id: nodeId,
parent: null,
children: [],
internal: {
type: `Medusa${capitalize(fieldName)}`,
content: nodeContent,
contentDigest: nodeContentDigest,
},
})
return [nodeData, ...images]
}
@@ -0,0 +1,3 @@
export function capitalize(s: string): string {
return s[0].toUpperCase() + s.slice(1)
}
@@ -0,0 +1,12 @@
export const formatUri = (uri: string): string => {
let url
try {
url = new URL(uri)
} catch (_) {
const formatted = /[\w||\d].*/.exec(uri)?.[0]
return `https://${formatted}`
}
return url.href
}