feat: gatsby source medusa (#924)
This commit is contained in:
1 parent
d0d8dd7bf6
commit
b8ff364276
18 files changed
+12013
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user