From 8230734f11c642aecfc4db4b4d1f256743ff9c4a Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Thu, 27 Feb 2025 16:50:50 -0500 Subject: [PATCH 01/12] updates to sources --- components/shopify/common-app.mjs | 965 ------------------ components/shopify/common/constants.mjs | 17 +- components/shopify/common/mutations.mjs | 39 + components/shopify/common/queries.mjs | 125 +++ components/shopify/common/rest-admin.mjs | 172 ---- components/shopify/common/utils.mjs | 47 - components/shopify/package.json | 2 +- components/shopify/shopify.app.mjs | 134 ++- .../collection-updated/collection-updated.mjs | 27 + .../sources/collection-updated/test-event.mjs | 12 + components/shopify/sources/common/polling.mjs | 60 ++ components/shopify/sources/common/utils.mjs | 3 - components/shopify/sources/common/webhook.mjs | 26 +- .../customer-data-request.mjs | 3 +- .../sources/new-abandoned-cart/common.mjs | 36 - .../new-abandoned-cart/new-abandoned-cart.mjs | 16 - .../shopify/sources/new-article/common.mjs | 42 - .../sources/new-article/new-article.mjs | 34 +- .../new-event-emitted/new-event-emitted.mjs | 10 +- .../shopify/sources/new-page/common.mjs | 28 - .../shopify/sources/new-page/new-page.mjs | 28 +- .../new-product-created.mjs | 11 +- .../new-product-created/test-event.mjs | 65 ++ .../common.mjs | 36 - .../product-added-to-custom-collection.mjs | 16 - 25 files changed, 552 insertions(+), 1402 deletions(-) delete mode 100644 components/shopify/common-app.mjs create mode 100644 components/shopify/common/mutations.mjs create mode 100644 components/shopify/common/queries.mjs delete mode 100644 components/shopify/common/rest-admin.mjs delete mode 100644 components/shopify/common/utils.mjs create mode 100644 components/shopify/sources/collection-updated/collection-updated.mjs create mode 100644 components/shopify/sources/collection-updated/test-event.mjs create mode 100644 components/shopify/sources/common/polling.mjs delete mode 100644 components/shopify/sources/common/utils.mjs delete mode 100644 components/shopify/sources/new-abandoned-cart/common.mjs delete mode 100644 components/shopify/sources/new-abandoned-cart/new-abandoned-cart.mjs delete mode 100644 components/shopify/sources/new-article/common.mjs delete mode 100644 components/shopify/sources/new-page/common.mjs create mode 100644 components/shopify/sources/new-product-created/test-event.mjs delete mode 100644 components/shopify/sources/product-added-to-custom-collection/common.mjs delete mode 100644 components/shopify/sources/product-added-to-custom-collection/product-added-to-custom-collection.mjs diff --git a/components/shopify/common-app.mjs b/components/shopify/common-app.mjs deleted file mode 100644 index db8008285cb23..0000000000000 --- a/components/shopify/common-app.mjs +++ /dev/null @@ -1,965 +0,0 @@ -import fs from "fs"; -import get from "lodash.get"; -import Shopify from "shopify-api-node"; -import toPath from "lodash.topath"; -import retry from "async-retry"; -import { toSingleLineString } from "./actions/common/common.mjs"; - -export default { - type: "app", - app: "shopify", - propDefinitions: { - productId: { - type: "string", - label: "Product ID", - description: "ID of the product. Option displayed here as the title of the product", - async options({ prevContext }) { - return this.getProductOptions(prevContext); - }, - }, - productVariantId: { - type: "string", - label: "Product Variant ID", - description: "ID of the product variant", - async options({ - productId, - prevContext, - }) { - return this.getProductVariantOptions(productId, prevContext); - }, - }, - customerId: { - type: "string", - label: "Customer ID", - description: "The Customer ID. Option displayed here as email registered with the Customer ID", - useQuery: true, - async options({ - prevContext, - query, - }) { - return this.getCustomerOptions(prevContext, query); - }, - }, - locationId: { - type: "string", - label: "Location ID", - description: "The ID of the location that the inventory level belongs to. Options will display the name of the Location ID", - async options() { - const response = await this.getLocationIds(); - return response.result.map((e) => ({ - label: e.name, - value: e.id, - })); - }, - }, - inventoryItemId: { - type: "string", - label: "Inventory Item ID", - description: toSingleLineString(` - The ID of the inventory item associated with the inventory level. - There is a 1:1 relationship between a product variant and an inventory item. - Each product variant includes the ID of its related inventory item. - To view a list of Inventory Items, choose a product using the field above - `), - async options({ - productId, - prevContext, - }) { - if (!productId) { - return []; - } - const defaultParams = { - fields: "title,inventory_item_id", - }; - const { nextPageParameters = defaultParams } = prevContext; - const response = await this.resourceAction("productVariant", "list", nextPageParameters, productId); - return { - options: response.result.map((e) => ({ - label: e.title, - value: e.inventory_item_id, - })), - context: { - nextPageParameters: response.nextPageParameters, - }, - }; - }, - }, - firstName: { - type: "string", - label: "First Name", - description: "The customer's first name", - optional: true, - }, - lastName: { - type: "string", - label: "Last Name", - description: "The customer's last name", - optional: true, - }, - email: { - type: "string", - label: "Email", - description: "The unique email address of the customer", - }, - phone: { - type: "string", - label: "Phone Number", - description: toSingleLineString(` - The unique phone number (E.164 format) for this customer. - Check out [Shopify Customer API](https://shopify.dev/api/admin-rest/2022-01/resources/customer#[post]/admin/api/#{api_version}/customers.json_examples) for more details on valid formats - `), - optional: true, - }, - address: { - type: "string", - label: "Street Address", - description: "The customer's mailing address", - optional: true, - }, - company: { - type: "string", - label: "Company", - description: "The customer's company", - optional: true, - }, - city: { - type: "string", - label: "City", - description: "The customer's city, town, or village", - optional: true, - }, - province: { - type: "string", - label: "Province", - description: "The customer's region name. Typically a province, a state, or a prefecture", - optional: true, - }, - country: { - type: "string", - label: "Country", - description: "The customer's country", - optional: true, - }, - zip: { - type: "string", - label: "Zip Code", - description: "The customer's postal code", - optional: true, - }, - sendEmailInvite: { - type: "boolean", - label: "Send Email Invite", - description: "Send email invite to address", - optional: true, - }, - title: { - type: "string", - label: "Title", - description: "The name of the product", - }, - productDescription: { - type: "string", - label: "Product Description", - description: "A description of the product. Supports HTML formatting. Example: `Good snowboard!`", - optional: true, - }, - price: { - type: "string", - label: "Price", - description: "The price of the product", - optional: true, - }, - vendor: { - type: "string", - label: "Vendor", - description: "The name of the product's vendor", - optional: true, - }, - productType: { - type: "string", - label: "Product Type", - description: "A categorization for the product used for filtering and searching products", - optional: true, - }, - status: { - type: "string", - label: "Status", - description: toSingleLineString(` - The status of the product. - \`active\`: The product is ready to sell and is available to customers on the online store, sales channels, and apps. By default, existing products are set to active. - \`archived\`: The product is no longer being sold and isn't available to customers on sales channels and apps. - \`draft\`: The product isn't ready to sell and is unavailable to customers on sales channels and apps. By default, duplicated and unarchived products are set to draft - `), - optional: true, - options: [ - "active", - "archived", - "draft", - ], - }, - images: { - type: "string[]", - label: "Images", - description: "A list of [product image](https://shopify.dev/docs/admin-api/rest/reference/products/product-image) objects, each one representing an image associated with the product. For each row you can either specify a Pipedream path like `/tmp/my-image.png` or a URL like `https://example.com/image.png`. More information at [Shopify Product API](https://shopify.dev/api/admin-rest/2022-01/resources/product#[post]/admin/api/2022-01/products.json)", - optional: true, - }, - options: { - type: "string[]", - label: "Options", - description: toSingleLineString(` - The custom product properties. - For example, Size, Color, and Material. Each product can have up to 3 options and each option value can be up to 255 characters. - Product variants are made of up combinations of option values. Options cannot be created without values. - To create new options, a variant with an associated option value also needs to be created. - Example: \`[{"name":"Color","values":["Blue","Black"]},{"name":"Size","values":["155","159"]}]\` - `), - optional: true, - }, - variants: { - type: "string[]", - label: "Product Variants", - description: toSingleLineString(` - An array of product variants, each representing a different version of the product. - The position property is read-only. The position of variants is indicated by the order in which they are listed. - Example: \`[{"option1":"First","price":"10.00","sku":"123"},{"option1":"Second","price":"20.00","sku":"123"}]\` - `), - optional: true, - }, - tags: { - type: "string[]", - label: "Tags", - description: toSingleLineString(` - A string of comma-separated tags that are used for filtering and search. A product can have up to 250 tags. Each tag can have up to 255 characters. - Example: \`"Barnes & Noble","Big Air","John's Fav"\` - `), - optional: true, - }, - option: { - type: "string", - label: "Title", - description: "The option name of the product variant", - }, - imageId: { - type: "string", - label: "Image ID", - description: "The unique numeric identifier for a product's image. The image must be associated to the same product as the variant", - optional: true, - async options({ productId }) { - return this.getImageOptions(productId); - }, - }, - query: { - type: "string", - label: "Query", - description: "The search query", - }, - max: { - type: "integer", - label: "Max Records", - description: "Optionally limit the maximum number of records to return. Leave blank to retrieve all records.", - optional: true, - }, - collectionId: { - type: "string", - label: "Collection ID", - description: "Search for products by collection ID. A collection is a grouping of products that merchants can create to make their stores easier to browse. For example, a merchant might create a collection for a specific type of product that they sell, such as Footwear. Merchants can create collections by selecting products individually or by defining rules that automatically determine whether products are included.", - optional: true, - async options() { - return this.getCollectionOptions(); - }, - }, - blogId: { - type: "string", - label: "Blog ID", - description: "The ID of a Shopify blog", - async options() { - return this.getBlogOptions(); - }, - }, - articleId: { - type: "string", - label: "Article ID", - description: "The ID of a Shopify blog article", - async options({ blogId }) { - return this.getArticleOptions(blogId); - }, - }, - pageId: { - type: "string", - label: "Page ID", - description: "The ID of a Shopify page", - async options() { - return this.getPageOptions(); - }, - }, - orderId: { - type: "string", - label: "Order ID", - description: "The ID of a Shopify order", - async options() { - return this.getOrderOptions(); - }, - }, - draftOrderId: { - type: "string", - label: "Draft Order ID", - description: "The ID of a Shopify draft order", - async options() { - return this.getDraftOrderOptions(); - }, - }, - metafields: { - type: "string[]", - label: "Metafields", - description: "An array of objects, each one representing a metafield. If adding a new metafield, the object should contain `key`, `value`, `type`, and `namespace`. Example: `{{ [{ \"key\": \"new\", \"value\": \"newvalue\", \"type\": \"single_line_text_field\", \"namespace\": \"global\" }] }}`. To update an existing metafield, use the `id` and `value`. Example: `{{ [{ \"id\": \"28408051400984\", \"value\": \"updatedvalue\" }] }}`", - optional: true, - }, - sku: { - type: "string", - label: "Sku", - description: "A unique identifier for the product variant in the shop", - optional: true, - }, - }, - methods: { - getShopId() { - return this.$auth.shop_id; - }, - _monthAgo() { - const now = new Date(); - const monthAgo = new Date(now.getTime()); - monthAgo.setMonth(monthAgo.getMonth() - 1); - return monthAgo; - }, - _jsonPathToGraphQl(path) { - return toPath(path).reduceRight( - (accum, item) => accum - ? `${item} { ${accum} }` - : item, - ); - }, - /** - * Returns if an error code represents a retriable error. - * @callback isRetriableErrCode - * @param {string} errCode The error code - * @returns {boolean} If the error code is retriable - */ - /** - * Returns if a GraphQL error code represents a retriable error. - * @type {isRetriableErrCode} - */ - _isRetriableGraphQLErrCode(errCode) { - return errCode === "THROTTLED"; - }, - /** - * Options for handling error objects returned by API calls. - * @typedef {Object} ErrorOptions - * @property {(string|number)[]|string} errCodePath Path to the status/error - * code in the error object - * @property {(string|number)[]|string} errDataPath Path to the error data - * in the error object - * @property {isRetriableErrCode} isRetriableErrCode Function that returns if - * the error code is retriable - */ - /** - * Returns options for handling GraphQL error objects. - * @returns {ErrorOptions} The options - */ - _graphQLErrOpts() { - // Shopify GraphQL requests are throttled if queries exceed point limit. - // See https://shopify.dev/concepts/about-apis/rate-limits - // GraphQL err: { extensions: { code='THROTTLED' }, response: { body: { errors: [] } } } - return { - errCodePath: [ - "extensions", - "code", - ], - errDataPath: [ - "response", - "body", - "errors", - "0", - ], - isRetriableErrCode: this._isRetriableGraphQLErrCode, - }; - }, - /** - * - * @param {function} apiCall The function that makes the API request - * @param {object} opts Options for retrying the API call - * @param {ErrorOptions} opts.errOpts Options for handling errors thrown by the API call - * @param {object} opts.retryOpts Options for async-retry. See - * https://www.npmjs.com/package/async-retry - * @returns {Promise} A promise that resolves to the return value of the apiCall - */ - async _withRetries(apiCall, opts = {}) { - const { - errOpts: { - errCodePath, - errDataPath, - isRetriableErrCode, - } = this._graphQLErrOpts(), - retryOpts = { - retries: 5, - factor: 2, - minTimeout: 2000, // In milliseconds - }, - } = opts; - return retry(async (bail, retryCount) => { - try { - return await apiCall(); - } catch (err) { - const errCode = get(err, errCodePath); - if (!isRetriableErrCode(errCode)) { - const errData = get(err, errDataPath, {}); - return bail(new Error(` - Unexpected error (error code: ${errCode}): - ${JSON.stringify(errData, null, 2)} - `)); - } - - console.log(` - [Attempt #${retryCount}] Temporary error: ${err.message} - `); - throw err; - } - }, retryOpts); - }, - _makeGraphQlRequest(query, variables = {}) { - const shopifyClient = this.getShopifyInstance(); - return this._withRetries( - () => shopifyClient.graphql(query, variables), - ); - }, - /** - * Removes empty key and values from params objects - * Shopify lib doesn't handle this and therefore many requests are rejected - * @param {object} params - * @returns {object} Clear of empty key/values - */ - _makeRequestOpts(params) { - for (const key of Object.keys(params)) { - const value = params[key]; - - if (typeof value == "boolean") { - continue; - } - if (Array.isArray(value) - && (value.length === 0 - || (value.length === 1 - && value[0] === ""))) { - delete params[key]; - } - else if (!(key && (value || value === 0))) { - delete params[key]; - } - else if (value.constructor == Object) { - this._makeRequestOpts(params[key]); - if (Object.values(params[key]).length === 0) { - delete params[key]; - } - } - } - return params; - }, - _throwFormattedError(err) { - err = err.response; - throw Error(`${err.statusCode} - ${err.statusMessage} - ${JSON.stringify(err.body)}`); - }, - /** - * Transforms a string to an object or returns the object - * @param {string} stringObject - * @returns {object} - */ - parseJSONStringObjects(stringObject) { - if (!stringObject) { - return {}; - } - if (typeof stringObject == "string") { - stringObject = JSON.parse(stringObject); - } - return this._makeRequestOpts(stringObject); - }, - /** - * Transforms a list of strings to a list of objects - * @param {string[]} stringList - * @returns {object[]} - */ - parseArrayOfJSONStrings(stringList) { - if (!stringList) { - return []; - } - if (typeof stringList == "string") { - throw new Error("string type not supported, please use array of objects in structured mode"); - } - return stringList.map((x) => x - ? this.parseJSONStringObjects(x) - : x).filter((x) => Object.values(x).length > 0); - }, - /** - * Transforms a list of strings to a comma-separated string - * @param {string[]} value - * @returns {string} - */ - parseCommaSeparatedStrings(value) { - if (Array.isArray(value)) { - return value.join(); - } - if (value === undefined || typeof value == "string") { - return value; - } - throw new TypeError("variable should be an array or string"); - }, - getImageBase64Encoded(path) { - if (!fs.existsSync(path)) { - throw new Error(`File path does not exist: ${path}`); - } - const image = fs.readFileSync(path); - return image.toString("base64"); - }, - parseImages(images) { - if (!images) { - return []; - } - - return images.map((image) => { - const key = - image.startsWith("http") - ? "src" - : "attachment"; - return { - [key]: key === "attachment" - ? this.getImageBase64Encoded(image) - : image, - }; - }); - }, - dayAgo() { - const dayAgo = new Date(); - dayAgo.setDate(dayAgo.getDate() - 1); - return dayAgo; - }, - getShopifyInstance() { - return new Shopify({ - shopName: this.getShopId(), - accessToken: this.$auth.oauth_access_token, - autoLimit: true, - }); - }, - getSinceParams(sinceId = false, useCreatedAt = false, updatedAfter = null, params = {}) { - if (sinceId) params = { - ...params, - since_id: sinceId, - }; - if (updatedAfter) params = { - ...params, - updated_at_min: updatedAfter, - }; - // If no sinceId or updatedAfter, get objects created within the last month - if (!sinceId && !updatedAfter && useCreatedAt) return { - created_at_min: this._monthAgo(), - }; - return params; - }, - async getObjects(objectType, params = {}, id = null, maxPages) { - const shopify = this.getShopifyInstance(); - let objects = []; - let currPage = 1; - do { - if (maxPages && currPage > maxPages) { - break; - } - const results = id - ? await shopify[objectType].list(id, params) - : await shopify[objectType].list(params); - objects = objects.concat(results); - params = results.nextPageParameters; - currPage++; - } while (params !== undefined); - return objects; - }, - /** - * Uses Shopify lib for API requests according to resource and action - * @param {string} objectType Shopify resource - * @param {string} action Shopify resource method - * @param {object} params JSON or Query request parameters - * @param {string} id Resource ID if expected - * @returns {object} Shopify resource action result - */ - async resourceAction(objectType, action, params = {}, id = null) { - const shopify = this.getShopifyInstance(); - this._makeRequestOpts(params); - try { - const result = id - ? await shopify[objectType][action](id, params) - : await shopify[objectType][action](params); - return { - result, - nextPageParameters: result.nextPageParameters, - }; - } catch (err) { - this._throwFormattedError(err); - } - }, - async *paginate(resourceFn, params, max = null) { - let nextParams = params; - let count = 0; - do { - const { - result, - nextPageParameters, - } = await resourceFn(nextParams); - for (const item of result) { - yield item; - count++; - if (max && count >= max) { - return; - } - } - // pass cursor to get next page of results; if no cursor, no more pages - nextParams = nextPageParameters; - } while (nextParams !== undefined); - }, - async getPaginatedResults(resourceFn, params, max) { - const results = []; - const resourceStream = await this.paginate(resourceFn, params, max); - for await (const result of resourceStream) { - results.push(result); - } - return results; - }, - async getAbandonedCheckouts(sinceId) { - const params = this.getSinceParams(sinceId, true); - return this.getObjects("checkout", params); - }, - async getArticles(blogId, sinceId) { - const params = this.getSinceParams(sinceId, true); - return this.getObjects("article", params, blogId); - }, - async getBlogs() { - return this.getObjects("blog"); - }, - async getCustomers(sinceId, updatedAfter, params = {}) { - if (Object.values(this._makeRequestOpts(params)).length > 0) { - return this.resourceAction("customer", "list", params); - } else { - params = this.getSinceParams(sinceId, true, updatedAfter); - return this.getObjects("customer", params); - } - }, - async createCustomer(params) { - return this.resourceAction("customer", "create", params); - }, - async updateCustomer(customerId, params) { - return this.resourceAction("customer", "update", params, customerId); - }, - async searchCustomers(params = {}) { - return this.resourceAction("customer", "search", params); - }, - async getEvents(sinceId, filter = null, verb = null) { - const params = this.getSinceParams(sinceId, true); - params.filter = filter; - params.verb = verb; - return this.getObjects("event", params); - }, - async getDraftOrders(fulfillmentStatus) { - return this.getObjects("draftOrder", { - fulfillment_status: fulfillmentStatus, - }); - }, - async getOrders(fulfillmentStatus, useCreatedAt = false, sinceId = null, updatedAfter = null, status = "any", limit = null, maxPages = null) { - const params = this.getSinceParams(sinceId, useCreatedAt, updatedAfter); - params.status = status; - params.fulfillment_status = fulfillmentStatus; - params.limit = limit; - return this.getObjects("order", params, null, maxPages); - }, - async getOrdersById(ids = []) { - if (ids.length === 0) { - return []; - } - const params = { - ids: ids.join(","), - status: "any", - limit: 100, - }; - return this.getObjects("order", params); - }, - async createOrder(params) { - return this.resourceAction("order", "create", params); - }, - async getPages(sinceId) { - const params = this.getSinceParams(sinceId, true); - return this.getObjects("page", params); - }, - async getProducts(sinceId, useCreatedAt = true, params = {}) { - this.getSinceParams(sinceId, useCreatedAt, null, params); - return this.getObjects("product", params); - }, - async getCollects(sinceId) { - const params = this.getSinceParams(sinceId, true); - return this.getObjects("collect", params); - }, - async createCollect(params) { - return this.resourceAction("collect", "create", params); - }, - async createCustomCollection(params) { - return this.resourceAction("customCollection", "create", params); - }, - async getProduct(productId, params) { - return this.resourceAction("product", "get", params, productId); - }, - async updateProduct(productId, params) { - return this.resourceAction("product", "update", params, productId); - }, - async getProductVariant(productVariantId, params) { - return this.resourceAction("productVariant", "get", params, productVariantId); - }, - async getProductVariantByTitle(productId, title, params) { - let list = (await this.resourceAction("productVariant", "list", params, productId)).result; - list = list.filter((e) => e.title == title); - if (list.length === 0) { - throw new Error(`Product variant with title ${title} not found`); - } - return list[0]; - }, - async updateProductVariant(productVariantId, params) { - return this.resourceAction("productVariant", "update", params, productVariantId); - }, - async updateInventoryItem(inventoryItemId, params) { - return this.resourceAction("inventoryItem", "update", params, inventoryItemId); - }, - async createProduct(params) { - return this.resourceAction("product", "create", params); - }, - async createProductVariant(productId, params) { - return this.resourceAction("productVariant", "create", params, productId); - }, - async createSmartCollection(params) { - return this.resourceAction("smartCollection", "create", params); - }, - async getLocationIds() { - return this.resourceAction("location", "list"); - }, - async updateInventoryLevel(params) { - return this.resourceAction("inventoryLevel", "set", params); - }, - async getMetafield(metafieldId, params = {}) { - return this.resourceAction("metafield", "get", params, metafieldId); - }, - async listMetafields(params) { - return this.getObjects("metafield", params); - }, - async createMetafield(params) { - return this.resourceAction("metafield", "create", params); - }, - async updateMetafield(metafieldId, params) { - return this.resourceAction("metafield", "update", params, metafieldId); - }, - async getProductOptions(prevContext) { - const defaultParams = { - limit: 50, - }; - const { nextPageParameters = defaultParams } = prevContext; - const response = await this.resourceAction("product", "list", nextPageParameters); - return { - options: response.result.map((e) => ({ - label: e.title, - value: e.id, - })), - context: { - nextPageParameters: response.nextPageParameters, - }, - }; - }, - async getProductVariantOptions(productId, prevContext) { - const defaultParams = { - fields: "id,title", - }; - const { nextPageParameters = defaultParams } = prevContext; - const response = await this.resourceAction("productVariant", "list", nextPageParameters, productId); - return { - options: response.result.map((e) => ({ - label: e.title, - value: e.id, - })), - context: { - nextPageParameters: response.nextPageParameters, - }, - }; - }, - async getCustomerOptions(prevContext, query) { - const defaultParams = { - limit: 50, - query, - }; - const { nextPageParameters = defaultParams } = prevContext; - const response = await this.resourceAction("customer", "search", nextPageParameters); - return { - options: response.result.map((e) => ({ - label: e.email, - value: e.id, - })), - context: { - nextPageParameters: response.nextPageParameters, - }, - }; - }, - async getCollectionOptions() { - const collections = await this.getObjects("customCollection"); - return collections?.map((collection) => ({ - label: collection.title, - value: collection.id, - })) || []; - }, - async getImageOptions(productId) { - if (!productId) { - return []; - } - const response = await this.resourceAction("productImage", "list", { - fields: "src,id", - }, productId); - return response.result.map((e) => ({ - label: e.src, - value: e.id, - })); - }, - async getBlogOptions() { - const blogs = await this.getObjects("blog"); - return blogs?.map((blog) => ({ - label: blog.title, - value: blog.id, - })) || []; - }, - async getArticleOptions(blogId) { - const articles = await this.getObjects("article", {}, blogId); - return articles?.map((article) => ({ - label: article.title, - value: article.id, - })) || []; - }, - async getPageOptions() { - const pages = await this.getObjects("page"); - return pages?.map((page) => ({ - label: page.title, - value: page.id, - })) || []; - }, - async getOrderOptions() { - const orders = await this.getObjects("order"); - return orders?.map((order) => order.id) || []; - }, - async getDraftOrderOptions() { - const draftOrders = await this.getObjects("draftOrder"); - return draftOrders?.map((draftOrder) => draftOrder.id) || []; - }, - async getMetafieldOptions(ownerResource, ownerId) { - const metafields = await this.listMetafields({ - metafield: { - owner_resource: ownerResource, - owner_id: ownerId, - }, - }); - return metafields?.map((metafield) => ({ - label: metafield.key, - value: metafield.id, - })) || []; - }, - createWebhook(params) { - return this.resourceAction("webhook", "create", params); - }, - deleteWebhook(webhookId) { - return this.resourceAction("webhook", "delete", webhookId); - }, - async *queryOrders(opts = {}) { - const { - sortKey = "UPDATED_AT", - filter = "", - fields = [], - } = opts; - const nodeFields = [ - "id", - ...fields.map(this._jsonPathToGraphQl), - ].join("\n"); - const query = ` - query orders($after: String, $filter: String, $sortKey: OrderSortKeys) { - orders(after: $after, first: 100, query: $filter, sortKey: $sortKey) { - pageInfo { - hasNextPage - } - edges { - cursor - node { - ${nodeFields} - } - } - } - } - `; - - let { prevCursor: after = null } = opts; - while (true) { - const variables = { - after, - filter, - sortKey, - }; - const { orders } = await this._makeGraphQlRequest(query, variables); - const { edges = [] } = orders; - for (const edge of edges) { - const { - node: order, - cursor, - } = edge; - yield { - order, - cursor, - }; - after = cursor; - } - - if (!orders.pageInfo.hasNextPage) { - return; - } - } - }, - /** - * Adds tags to a resource type. - * @param {string} resource Type of resource to query - * @param {string} id Resource ID - * @param {string} tagString List of tags to add - * @returns {object} Response from Shopify GraphQL API - */ - async addTags(resource, id, tagString) { - const gid = `gid://shopify/${resource}/${id}`; - - let tags = [ - tagString, - ]; - if (tags.includes(",")) { - tags = tagString.split(",").map((item) => item.trim()); - } - - const query = ` - mutation tagsAdd($gid: ID!, $tags: [String!]!) { - tagsAdd(id: $gid, tags: $tags) { - node { - id - } - userErrors { - field - message - } - } - } - `; - - const variables = { - gid, - tags, - }; - - return await this._makeGraphQlRequest(query, variables); - }, - }, -}; diff --git a/components/shopify/common/constants.mjs b/components/shopify/common/constants.mjs index e0c4a1639029f..a8707fedde4de 100644 --- a/components/shopify/common/constants.mjs +++ b/components/shopify/common/constants.mjs @@ -1,12 +1,9 @@ -const STORE_PLACEHOLDER = "{store_name}"; -const BASE_URL = `https://${STORE_PLACEHOLDER}.myshopify.com`; -const VERSION_PATH = "/admin/api/2024-04"; +const DEFAULT_LIMIT = 20; +const MAX_LIMIT = 250; +const API_VERSION = "2025-01"; -const DEFAULT_MAX = 300; - -export default { - BASE_URL, - VERSION_PATH, - STORE_PLACEHOLDER, - DEFAULT_MAX, +export { + DEFAULT_LIMIT, + MAX_LIMIT, + API_VERSION, }; diff --git a/components/shopify/common/mutations.mjs b/components/shopify/common/mutations.mjs new file mode 100644 index 0000000000000..b0a7fafd8c8ff --- /dev/null +++ b/components/shopify/common/mutations.mjs @@ -0,0 +1,39 @@ +const CREATE_WEBHOOK = ` + mutation webhookSubscriptionCreate( + $topic: WebhookSubscriptionTopic!, + $webhookSubscription: WebhookSubscriptionInput! + ) { + webhookSubscriptionCreate( + topic: $topic, + webhookSubscription: $webhookSubscription + ) { + webhookSubscription { + id + topic + filter + format + callbackUrl + } + userErrors { + field + message + } + } + }`; + +const DELETE_WEBHOOK = ` + mutation webhookSubscriptionDelete($id: ID!) { + webhookSubscriptionDelete(id: $id) { + userErrors { + field + message + } + deletedWebhookSubscriptionId + } + } +`; + +export default { + CREATE_WEBHOOK, + DELETE_WEBHOOK, +}; diff --git a/components/shopify/common/queries.mjs b/components/shopify/common/queries.mjs new file mode 100644 index 0000000000000..709f6973b6652 --- /dev/null +++ b/components/shopify/common/queries.mjs @@ -0,0 +1,125 @@ +const GET_BLOG = ` + query BlogShow($id: ID!, $first: Int, $after: String, $reverse: Boolean) { + blog(id: $id) { + articles(first: $first, after: $after, reverse: $reverse) { + nodes { + id + title + author { + name + } + blog { + id + } + body + createdAt + updatedAt + isPublished + publishedAt + summary + tags + } + pageInfo { + endCursor + } + } + } + } +`; + +const LIST_BLOGS = ` + query BlogList($first: Int, $after: String) { + blogs(first: $first, after: $after) { + nodes { + id + title + } + pageInfo { + endCursor + } + } + } +`; + +const LIST_PAGES = ` + query PageList($first: Int, $after: String, $reverse: Boolean) { + pages(first: $first, after: $after, reverse: $reverse) { + nodes { + id + title + body + bodySummary + createdAt + handle + isPublished + metafields (first: $first) { + nodes { + id + key + namespace + value + } + } + publishedAt + updatedAt + } + pageInfo { + endCursor + } + } + } +`; + +const LIST_PRODUCTS = ` + query ($first: Int, $after: String, $reverse: Boolean, $sortKey: ProductSortKeys) { + products (first: $first, after: $after, reverse: $reverse, sortKey: $sortKey) { + nodes { + id + title + category { + id + } + collections (first: $first) { + nodes { + id + title + } + } + createdAt + description + handle + metafields (first: $first) { + nodes { + id + key + namespace + value + } + } + onlineStoreUrl + productType + publishedAt + status + tags + totalInventory + updatedAt + variants (first: $first) { + nodes { + id + title + } + } + vendor + } pageInfo { + endCursor + } + } + } +`; + +export default { + GET_BLOG, + LIST_BLOGS, + LIST_PAGES, + LIST_PRODUCTS, +}; diff --git a/components/shopify/common/rest-admin.mjs b/components/shopify/common/rest-admin.mjs deleted file mode 100644 index 59c3ffaeabf04..0000000000000 --- a/components/shopify/common/rest-admin.mjs +++ /dev/null @@ -1,172 +0,0 @@ -import { axios } from "@pipedream/platform"; -import common from "../shopify.app.mjs"; -import constants from "./constants.mjs"; -import utils from "./utils.mjs"; - -export default { - ...common, - propDefinitions: { - ...common.propDefinitions, - bodyHtml: { - type: "string", - label: "Body HTML", - description: "The text content of the page, complete with HTML markup.", - }, - pageId: { - type: "string", - label: "Page ID", - description: "The unique numeric identifier for the page.", - async options() { - const { pages } = await this.listPages(); - return pages.map(({ - id: value, title: label, - }) => ({ - label, - value, - })); - }, - }, - blogId: { - type: "string", - label: "Blog ID", - description: "The unique numeric identifier for the blog.", - async options() { - const { blogs } = await this.listBlogs(); - return blogs.map(({ - id: value, title: label, - }) => ({ - label, - value, - })); - }, - }, - articleId: { - type: "string", - label: "Article ID", - description: "The unique numeric identifier for the article.", - async options({ blogId }) { - const { articles } = await this.listBlogArticles({ - blogId, - }); - return articles.map(({ - id: value, title: label, - }) => ({ - label, - value, - })); - }, - }, - }, - methods: { - ...common.methods, - getBaseUrl() { - const baseUrl = `${constants.BASE_URL}${constants.VERSION_PATH}`; - return baseUrl.replace(constants.STORE_PLACEHOLDER, this.getShopId()); - }, - getUrl(path, url) { - return url || `${this.getBaseUrl()}${path}.json`; - }, - getHeaders(headers) { - return { - "Content-Type": "application/json", - "X-Shopify-Access-Token": this.$auth.oauth_access_token, - ...headers, - }; - }, - makeRequest({ - step = this, path, headers, url, ...args - } = {}) { - - const config = { - headers: this.getHeaders(headers), - url: this.getUrl(path, url), - ...args, - }; - - return axios(step, config); - }, - post(args = {}) { - return this.makeRequest({ - method: "post", - ...args, - }); - }, - put(args = {}) { - return this.makeRequest({ - method: "put", - ...args, - }); - }, - delete(args = {}) { - return this.makeRequest({ - method: "delete", - ...args, - }); - }, - listPages(args = {}) { - return this.makeRequest({ - path: "/pages", - ...args, - }); - }, - listBlogs(args = {}) { - return this.makeRequest({ - path: "/blogs", - ...args, - }); - }, - listBlogArticles({ - blogId, ...args - } = {}) { - return this.makeRequest({ - path: `/blogs/${blogId}/articles`, - ...args, - }); - }, - async *getResourcesStream({ - resourceFn, - resourceFnArgs, - resourceName, - max = constants.DEFAULT_MAX, - }) { - let url; - let resourcesCount = 0; - - while (true) { - const stream = - await resourceFn({ - responseType: "stream", - url, - ...resourceFnArgs, - }); - - const { headers: { link } } = stream; - - const linkParsed = utils.parseLinkHeader(link); - const response = await utils.getDataFromStream(stream); - const nextResources = resourceName && response[resourceName] || response; - - if (!nextResources?.length) { - console.log("No more resources found"); - return; - } - - for (const resource of nextResources) { - yield resource; - resourcesCount += 1; - - if (resourcesCount >= max) { - return; - } - } - - if (!linkParsed?.next) { - console.log("Pagination complete"); - return; - } - - url = linkParsed.next; - } - }, - }, -}; diff --git a/components/shopify/common/utils.mjs b/components/shopify/common/utils.mjs deleted file mode 100644 index 70a9ac5c57696..0000000000000 --- a/components/shopify/common/utils.mjs +++ /dev/null @@ -1,47 +0,0 @@ -function getDataFromStream(stream) { - const buffer = []; - return new Promise((resolve, reject) => { - stream.on("data", (chunk) => buffer.push(chunk)); - stream.on("end", () => { - const data = Buffer.concat(buffer).toString(); - return resolve(JSON.parse(data)); - }); - stream.on("error", (err) => reject(err)); - }); -} - -function parseLinkHeader(linkHeader) { - return linkHeader?.split(",") - .reduce((props, link) => { - const [ - url, - rel, - ] = link.split(";"); - const [ - , value, - ] = url.split("<"); - const [ - , key, - ] = rel.split("="); - const clearKey = key.replace(/"/g, ""); - const clearValue = value.replace(/>/g, ""); - return { - ...props, - [clearKey]: clearValue, - }; - }, {}); -} - -async function streamIterator(stream) { - const resources = []; - for await (const resource of stream) { - resources.push(resource); - } - return resources; -} - -export default { - streamIterator, - getDataFromStream, - parseLinkHeader, -}; diff --git a/components/shopify/package.json b/components/shopify/package.json index 41ed5af0e5bc9..87ea55a1760cc 100644 --- a/components/shopify/package.json +++ b/components/shopify/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/shopify", - "version": "0.6.8", + "version": "0.6.9", "description": "Pipedream Shopify Components", "main": "shopify.app.mjs", "keywords": [ diff --git a/components/shopify/shopify.app.mjs b/components/shopify/shopify.app.mjs index 3ab28599a6407..e9cad0d2be3d8 100644 --- a/components/shopify/shopify.app.mjs +++ b/components/shopify/shopify.app.mjs @@ -1,7 +1,137 @@ -import commonApp from "./common-app.mjs"; +import Shopify from "shopify-api-node"; +import get from "lodash.get"; +import retry from "async-retry"; +import mutations from "./common/mutations.mjs"; +import queries from "./common/queries.mjs"; +import { + DEFAULT_LIMIT, API_VERSION, +} from "./common/constants.mjs"; export default { - ...commonApp, type: "app", app: "shopify", + propDefinitions: { + blogId: { + type: "string", + label: "Blogs", + description: "The identifier of a blog", + async options({ prevContext }) { + const variables = { + first: DEFAULT_LIMIT, + }; + if (prevContext?.after) { + variables.after = prevContext.after; + } + const { + blogs: { + nodes, pageInfo, + }, + } = await this.listBlogs(variables); + return { + options: nodes?.map(({ + id: value, title: label, + }) => ({ + value, + label, + })), + context: { + after: pageInfo?.endCursor, + }, + }; + }, + }, + }, + methods: { + getShopId() { + return this.$auth.shop_id; + }, + getShopifyInstance() { + return new Shopify({ + shopName: this.getShopId(), + accessToken: this.$auth.oauth_access_token, + autoLimit: true, + apiVersion: API_VERSION, + }); + }, + _makeGraphQlRequest(query, variables = {}) { + const shopifyClient = this.getShopifyInstance(); + return this._withRetries( + () => shopifyClient.graphql(query, variables), + ); + }, + _graphQLErrOpts() { + // Shopify GraphQL requests are throttled if queries exceed point limit. + // See https://shopify.dev/concepts/about-apis/rate-limits + // GraphQL err: { extensions: { code='THROTTLED' }, response: { body: { errors: [] } } } + return { + errCodePath: [ + "extensions", + "code", + ], + errDataPath: [ + "response", + "body", + "errors", + "0", + ], + isRetriableErrCode: this._isRetriableGraphQLErrCode, + }; + }, + _isRetriableGraphQLErrCode(errCode) { + return errCode === "THROTTLED"; + }, + async _withRetries(apiCall, opts = {}) { + const { + errOpts: { + errCodePath, + errDataPath, + isRetriableErrCode, + } = this._graphQLErrOpts(), + retryOpts = { + retries: 5, + factor: 2, + minTimeout: 2000, // In milliseconds + }, + } = opts; + return retry(async (bail, retryCount) => { + try { + return await apiCall(); + } catch (err) { + const errCode = get(err, errCodePath); + if (!isRetriableErrCode(errCode)) { + const errData = get(err, errDataPath, {}); + return bail(new Error(` + Unexpected error (error code: ${errCode}): + ${JSON.stringify(errData, null, 2)} + `)); + } + + console.log(` + [Attempt #${retryCount}] Temporary error: ${err.message} + `); + throw err; + } + }, retryOpts); + }, + createWebhook(variables) { + return this._makeGraphQlRequest(mutations.CREATE_WEBHOOK, variables); + }, + deleteWebhook(webhookId) { + return this._makeGraphQlRequest(mutations.DELETE_WEBHOOK, { + id: webhookId, + }); + }, + getBlog(variables) { + return this._makeGraphQlRequest(queries.GET_BLOG, variables); + }, + listBlogs(variables) { + return this._makeGraphQlRequest(queries.LIST_BLOGS, variables); + }, + listPages(variables) { + return this._makeGraphQlRequest(queries.LIST_PAGES, variables); + }, + listProducts(variables) { + return this._makeGraphQlRequest(queries.LIST_PRODUCTS, variables); + }, + }, }; diff --git a/components/shopify/sources/collection-updated/collection-updated.mjs b/components/shopify/sources/collection-updated/collection-updated.mjs new file mode 100644 index 0000000000000..2aeb15eedd1f9 --- /dev/null +++ b/components/shopify/sources/collection-updated/collection-updated.mjs @@ -0,0 +1,27 @@ +import common from "../common/webhook.mjs"; +import sampleEmit from "./test-event.mjs"; + +export default { + ...common, + key: "shopify-collection-updated", + name: "Collection Updated (Instant)", + description: "Emit new event whenever a collection is updated, including whenever products are added or removed from a collection.", + version: "0.0.7", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + getTopic() { + return "COLLECTIONS_UPDATE"; + }, + generateMeta(collection) { + const ts = Date.parse(collection.updatedAt); + return { + id: `${collection.id}${ts}`, + summary: `Collection Updated: ${collection.title}`, + ts, + }; + }, + }, + sampleEmit, +}; diff --git a/components/shopify/sources/collection-updated/test-event.mjs b/components/shopify/sources/collection-updated/test-event.mjs new file mode 100644 index 0000000000000..ff6531d033e68 --- /dev/null +++ b/components/shopify/sources/collection-updated/test-event.mjs @@ -0,0 +1,12 @@ +export default { + "id": 438779904280, + "handle": "test-collection", + "title": "test collection", + "updated_at": "2025-02-27T16:40:47-05:00", + "body_html": "", + "published_at": "2023-02-27T11:23:07-05:00", + "sort_order": "best-selling", + "template_suffix": "", + "published_scope": "web", + "admin_graphql_api_id": "gid://shopify/Collection/438779904280" +} \ No newline at end of file diff --git a/components/shopify/sources/common/polling.mjs b/components/shopify/sources/common/polling.mjs new file mode 100644 index 0000000000000..7c082b7c726d0 --- /dev/null +++ b/components/shopify/sources/common/polling.mjs @@ -0,0 +1,60 @@ +import app from "../../shopify.app.mjs"; +import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; + +export default { + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, + }, + }, + }, + methods: { + _getLastTs() { + return this.db.get("lastTs") || 0; + }, + _setLastTs(lastTs) { + this.db.set("lastTs", lastTs); + }, + emitEvent(item) { + const meta = this.generateMeta(item); + this.$emit(item, meta); + }, + getResults() { + throw new Error("getResults is not implemented"); + }, + getTsField() { + throw new Error("getTsField is not implemented"); + }, + generateMeta() { + throw new Error("generateMeta is not implemented"); + }, + }, + async run() { + const lastTs = this._getLastTs(); + let maxTs = lastTs; + + const results = await this.getResults(); + const tsField = this.getTsField(); + + const items = []; + for (const item of results) { + const ts = Date.parse(item[tsField]); + if (ts >= lastTs) { + items.push(item); + maxTs = Math.max(maxTs, ts); + } + } + + if (!items.length) { + return; + } + + this._setLastTs(maxTs); + + items.reverse().forEach((item) => this.emitEvent(item)); + }, +}; diff --git a/components/shopify/sources/common/utils.mjs b/components/shopify/sources/common/utils.mjs deleted file mode 100644 index a871fff52c21d..0000000000000 --- a/components/shopify/sources/common/utils.mjs +++ /dev/null @@ -1,3 +0,0 @@ -export function dateToISOStringWithoutMs(date) { - return date.toISOString().replace(/\.\d{3}Z$/, "Z"); -} diff --git a/components/shopify/sources/common/webhook.mjs b/components/shopify/sources/common/webhook.mjs index e2237db7389f2..1b8de8f85a99f 100644 --- a/components/shopify/sources/common/webhook.mjs +++ b/components/shopify/sources/common/webhook.mjs @@ -24,17 +24,25 @@ export default { }, hooks: { async activate() { - const { result: webhook } = await this.app.createWebhook({ - address: this.http.endpoint, - topic: this.getTopic(), - metafield_namespaces: this.metafieldNamespaces, - private_metafield_namespaces: this.privateMetafieldNamespaces, - }); - this.setWebhookId(webhook.id); + const { webhookSubscriptionCreate: { webhookSubscription: { id } } } + = await this.app.createWebhook({ + topic: this.getTopic(), + webhookSubscription: { + callbackUrl: this.http.endpoint, + format: "JSON", + metafieldNamespaces: [ + ...(this.metafieldNamespaces || []), + ...(this.privateMetafieldNamespaces || []), + ], + }, + }); + this.setWebhookId(id); }, async deactivate() { const webhookId = this.getWebhookId(); - await this.app.deleteWebhook(webhookId); + if (webhookId) { + await this.app.deleteWebhook(webhookId); + } }, }, methods: { @@ -57,7 +65,7 @@ export default { shopId, ] = domain.split(constants.DOMAIN_SUFFIX); return this.app.getShopId() === shopId - && this.getTopic() === topic; + && constants.EVENT_TOPIC[this.getTopic()] === topic; }, isRelevant() { return true; diff --git a/components/shopify/sources/customer-data-request/customer-data-request.mjs b/components/shopify/sources/customer-data-request/customer-data-request.mjs index 971133831cd7e..1b34da849c3b5 100644 --- a/components/shopify/sources/customer-data-request/customer-data-request.mjs +++ b/components/shopify/sources/customer-data-request/customer-data-request.mjs @@ -2,13 +2,12 @@ import shopify from "../../shopify.app.mjs"; export default { name: "New Customer Data Request", - version: "0.0.13", + version: "0.0.14", key: "shopify-customer-data-request", description: "Emit new customer data requests for data via a GDPR request.", type: "source", props: { shopify, - // eslint-disable-next-line pipedream/props-label, pipedream/props-description shopifyApphook: { type: "$.interface.apphook", appProp: "shopify", diff --git a/components/shopify/sources/new-abandoned-cart/common.mjs b/components/shopify/sources/new-abandoned-cart/common.mjs deleted file mode 100644 index 687c2806ee61d..0000000000000 --- a/components/shopify/sources/new-abandoned-cart/common.mjs +++ /dev/null @@ -1,36 +0,0 @@ -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; - -export default { - props: { - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - }, - methods: { - _getSinceId() { - return this.db.get("sinceId") || null; - }, - _setSinceId(sinceId) { - this.db.set("sinceId", sinceId); - }, - }, - async run() { - const sinceId = this._getSinceId(); - const results = await this.shopify.getAbandonedCheckouts(sinceId); - - for (const cart of results) { - this.$emit(cart, { - id: cart.id, - summary: cart.email, - ts: Date.now(), - }); - } - - if (results.length > 0) - this._setSinceId(results[results.length - 1].id); - }, -}; diff --git a/components/shopify/sources/new-abandoned-cart/new-abandoned-cart.mjs b/components/shopify/sources/new-abandoned-cart/new-abandoned-cart.mjs deleted file mode 100644 index 4ee05fa0bb42e..0000000000000 --- a/components/shopify/sources/new-abandoned-cart/new-abandoned-cart.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import shopify from "../../shopify.app.mjs"; -import common from "./common.mjs"; - -export default { - ...common, - key: "shopify-new-abandoned-cart", - name: "New Abandoned Cart", - type: "source", - description: "Emit new event each time a user abandons their cart.", - version: "0.0.19", - dedupe: "unique", - props: { - shopify, - ...common.props, - }, -}; diff --git a/components/shopify/sources/new-article/common.mjs b/components/shopify/sources/new-article/common.mjs deleted file mode 100644 index 44861cbcc3448..0000000000000 --- a/components/shopify/sources/new-article/common.mjs +++ /dev/null @@ -1,42 +0,0 @@ -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; - -export default { - props: { - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - blogIds: { - type: "string[]", - label: "Blogs", - description: "A list of Blog IDs", - async options() { - const blogs = await this.shopify.getBlogs(); - return blogs.map((blog) => { - return { - label: blog.title, - value: blog.id, - }; - }); - }, - }, - }, - async run() { - for (const blogId of this.blogIds) { - let sinceId = this.db.get(blogId) || null; - let results = await this.shopify.getArticles(blogId, sinceId); - for (const article of results) { - this.$emit(article, { - id: article.id, - summary: article.title, - ts: Date.now(), - }); - } - if (results[results.length - 1]) - this.db.set(blogId, results[results.length - 1].id); - } - }, -}; diff --git a/components/shopify/sources/new-article/new-article.mjs b/components/shopify/sources/new-article/new-article.mjs index 474b4b2dc4efc..28999609153b7 100644 --- a/components/shopify/sources/new-article/new-article.mjs +++ b/components/shopify/sources/new-article/new-article.mjs @@ -1,5 +1,5 @@ -import shopify from "../../shopify.app.mjs"; -import common from "./common.mjs"; +import common from "../common/polling.mjs"; +import { MAX_LIMIT } from "../../common/constants.mjs"; export default { ...common, @@ -7,10 +7,36 @@ export default { name: "New Article", type: "source", description: "Emit new event for each new article in a blog.", - version: "0.0.18", + version: "0.0.19", dedupe: "unique", props: { - shopify, ...common.props, + blogId: { + propDefinition: [ + common.props.app, + "blogId", + ], + }, + }, + methods: { + ...common.methods, + async getResults() { + const { blog: { articles: { nodes } } } = await this.app.getBlog({ + id: this.blogId, + first: MAX_LIMIT, + reverse: true, + }); + return nodes; + }, + getTsField() { + return "createdAt"; + }, + generateMeta(article) { + return { + id: article.id, + summary: `New Article: ${article.title}`, + ts: Date.parse(article[this.getTsField()]), + }; + }, }, }; diff --git a/components/shopify/sources/new-event-emitted/new-event-emitted.mjs b/components/shopify/sources/new-event-emitted/new-event-emitted.mjs index ba13198c864d7..60adf344928ed 100644 --- a/components/shopify/sources/new-event-emitted/new-event-emitted.mjs +++ b/components/shopify/sources/new-event-emitted/new-event-emitted.mjs @@ -7,7 +7,7 @@ export default { name: "New Event Emitted (Instant)", type: "source", description: "Emit new event for each new Shopify event.", - version: "0.0.14", + version: "0.0.15", dedupe: "unique", props: { ...common.props, @@ -15,7 +15,13 @@ export default { type: "string", label: "Event Topic", description: "Event topic that triggers the webhook.", - options: constants.EVENT_TOPICS, + options: Object.entries(constants.EVENT_TOPIC).map(([ + key, + value, + ]) => ({ + value: key, + label: value, + })), }, }, methods: { diff --git a/components/shopify/sources/new-page/common.mjs b/components/shopify/sources/new-page/common.mjs deleted file mode 100644 index eebe3089f28f1..0000000000000 --- a/components/shopify/sources/new-page/common.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; - -export default { - props: { - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - }, - async run() { - const sinceId = this.db.get("since_id") || null; - let results = await this.shopify.getPages(sinceId); - - for (const page of results) { - this.$emit(page, { - id: page.id, - summary: page.title, - ts: Date.now(), - }); - } - - if (results[results.length - 1]) - this.db.set("since_id", results[results.length - 1].id); - }, -}; diff --git a/components/shopify/sources/new-page/new-page.mjs b/components/shopify/sources/new-page/new-page.mjs index 354b395f53719..60ce927cb473d 100644 --- a/components/shopify/sources/new-page/new-page.mjs +++ b/components/shopify/sources/new-page/new-page.mjs @@ -1,5 +1,5 @@ -import shopify from "../../shopify.app.mjs"; -import common from "./common.mjs"; +import common from "../common/polling.mjs"; +import { MAX_LIMIT } from "../../common/constants.mjs"; export default { ...common, @@ -7,10 +7,26 @@ export default { name: "New Page", type: "source", description: "Emit new event for each new page published.", - version: "0.0.18", + version: "0.0.19", dedupe: "unique", - props: { - shopify, - ...common.props, + methods: { + ...common.methods, + async getResults() { + const { pages: { nodes } } = await this.app.listPages({ + first: MAX_LIMIT, + reverse: true, + }); + return nodes; + }, + getTsField() { + return "createdAt"; + }, + generateMeta(page) { + return { + id: page.id, + summary: `New Page: ${page.title}`, + ts: Date.parse(page[this.getTsField()]), + }; + }, }, }; diff --git a/components/shopify/sources/new-product-created/new-product-created.mjs b/components/shopify/sources/new-product-created/new-product-created.mjs index 1eb80e8d36827..00a00a66a7fbe 100644 --- a/components/shopify/sources/new-product-created/new-product-created.mjs +++ b/components/shopify/sources/new-product-created/new-product-created.mjs @@ -1,5 +1,5 @@ import common from "../common/webhook.mjs"; -import constants from "../common/constants.mjs"; +import sampleEmit from "./test-event.mjs"; export default { ...common, @@ -7,20 +7,21 @@ export default { name: "New Product Created (Instant)", type: "source", description: "Emit new event for each product added to a store.", - version: "0.0.14", + version: "0.0.15", dedupe: "unique", methods: { ...common.methods, getTopic() { - return constants.EVENT_TOPIC.PRODUCTS_CREATE; + return "PRODUCTS_CREATE"; }, generateMeta(resource) { - const ts = Date.parse(resource.created_at); + const ts = Date.parse(resource.createdAt); return { id: resource.id, - summary: `New Product ${resource.id}.`, + summary: `New Product ${resource.title}`, ts, }; }, }, + sampleEmit, }; diff --git a/components/shopify/sources/new-product-created/test-event.mjs b/components/shopify/sources/new-product-created/test-event.mjs new file mode 100644 index 0000000000000..4731fa8ea8b19 --- /dev/null +++ b/components/shopify/sources/new-product-created/test-event.mjs @@ -0,0 +1,65 @@ +export default { + "admin_graphql_api_id": "gid://shopify/Product/9674076979480", + "body_html": null, + "created_at": "2025-02-27T15:08:28-05:00", + "handle": "product", + "id": 9674076979480, + "product_type": "", + "published_at": "2025-02-27T15:08:28-05:00", + "template_suffix": null, + "title": "a product", + "updated_at": "2025-02-27T15:08:29-05:00", + "vendor": "Testing", + "status": "active", + "published_scope": "web", + "tags": "", + "variants": [ + { + "admin_graphql_api_id": "gid://shopify/ProductVariant/49822590533912", + "barcode": null, + "compare_at_price": null, + "created_at": "2025-02-27T15:08:28-05:00", + "fulfillment_service": "manual", + "id": 49822590533912, + "inventory_management": null, + "inventory_policy": "deny", + "position": 1, + "price": "0.00", + "product_id": 9674076979480, + "sku": "", + "taxable": true, + "title": "Default Title", + "updated_at": "2025-02-27T15:08:28-05:00", + "option1": "Default Title", + "option2": null, + "option3": null, + "grams": 0, + "image_id": null, + "weight": 0, + "weight_unit": "lb", + "inventory_item_id": 51875516416280, + "inventory_quantity": 0, + "old_inventory_quantity": 0, + "requires_shipping": true + } + ], + "options": [ + { + "name": "Title", + "id": 12129291993368, + "product_id": 9674076979480, + "position": 1, + "values": [ + "Default Title" + ] + } + ], + "images": [], + "image": null, + "variant_gids": [ + { + "admin_graphql_api_id": "gid://shopify/ProductVariant/49822590533912", + "updated_at": "2025-02-27T20:08:28.000Z" + } + ] +} \ No newline at end of file diff --git a/components/shopify/sources/product-added-to-custom-collection/common.mjs b/components/shopify/sources/product-added-to-custom-collection/common.mjs deleted file mode 100644 index efccc9d33d432..0000000000000 --- a/components/shopify/sources/product-added-to-custom-collection/common.mjs +++ /dev/null @@ -1,36 +0,0 @@ -import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; - -export default { - props: { - db: "$.service.db", - timer: { - type: "$.interface.timer", - default: { - intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, - }, - }, - }, - methods: { - _getSinceId() { - return this.db.get("sinceId") || null; - }, - _setSinceId(sinceId) { - this.db.set("sinceId", sinceId); - }, - }, - async run() { - const sinceId = this._getSinceId(); - const results = await this.shopify.getCollects(sinceId); - - for (const collect of results) { - this.$emit(collect, { - id: collect.id, - summary: `Product ${collect.product_id} added to collection ${collect.collection_id}`, - ts: Date.parse(collect.created_at), - }); - } - - if (results.length > 0) - this._setSinceId(results[results.length - 1].id); - }, -}; diff --git a/components/shopify/sources/product-added-to-custom-collection/product-added-to-custom-collection.mjs b/components/shopify/sources/product-added-to-custom-collection/product-added-to-custom-collection.mjs deleted file mode 100644 index ab49324ecc55d..0000000000000 --- a/components/shopify/sources/product-added-to-custom-collection/product-added-to-custom-collection.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import shopify from "../../shopify.app.mjs"; -import common from "./common.mjs"; - -export default { - ...common, - key: "shopify-product-added-to-custom-collection", - name: "New product added to custom collection", - description: "Emit new event each time a product is added to a custom collection.", - version: "0.0.6", - type: "source", - dedupe: "unique", - props: { - shopify, - ...common.props, - }, -}; From 2f746a52f4806ba90cb0cdc630515e7e21ac550d Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Wed, 5 Mar 2025 16:02:41 -0500 Subject: [PATCH 02/12] actions updates --- .../add-product-to-custom-collection.mjs | 24 +- .../common.mjs | 19 - .../shopify/actions/add-tags/add-tags.mjs | 69 ++- .../shopify/actions/add-tags/common.mjs | 46 -- components/shopify/actions/common/common.mjs | 22 - .../common/{consts.mjs => constants.mjs} | 16 - .../actions/common/metafield-actions.mjs | 170 ++++++-- .../shopify/actions/common/metaobjects.mjs | 150 +------ components/shopify/actions/common/rules.mjs | 25 -- .../shopify/actions/create-article/common.mjs | 34 -- .../actions/create-article/create-article.mjs | 73 +++- .../shopify/actions/create-blog/common.mjs | 26 -- .../actions/create-blog/create-blog.mjs | 28 +- .../create-custom-collection/common.mjs | 35 -- .../create-custom-collection.mjs | 37 +- .../actions/create-customer/common.mjs | 33 -- .../actions/create-metafield/common.mjs | 55 --- .../create-metafield/create-metafield.mjs | 57 ++- .../actions/create-metaobject/common.mjs | 59 --- .../create-metaobject/create-metaobject.mjs | 72 +++- .../shopify/actions/create-order/common.mjs | 173 -------- .../shopify/actions/create-page/common.mjs | 30 -- .../actions/create-page/create-page.mjs | 38 +- .../actions/create-product-variant/common.mjs | 69 --- .../create-product-variant.mjs | 138 +++++- .../shopify/actions/create-product/common.mjs | 19 - .../actions/create-product/create-product.mjs | 95 ++-- .../create-smart-collection/common.mjs | 79 ---- .../create-smart-collection.mjs | 84 +++- .../shopify/actions/delete-article/common.mjs | 30 -- .../actions/delete-article/delete-article.mjs | 25 +- .../shopify/actions/delete-blog/common.mjs | 26 -- .../actions/delete-blog/delete-blog.mjs | 19 +- .../actions/delete-metafield/common.mjs | 35 -- .../delete-metafield/delete-metafield.mjs | 57 ++- .../shopify/actions/delete-page/common.mjs | 26 -- .../actions/delete-page/delete-page.mjs | 19 +- .../shopify/actions/get-articles/common.mjs | 20 - .../actions/get-articles/get-articles.mjs | 42 +- .../shopify/actions/get-metafields/common.mjs | 46 -- .../actions/get-metafields/get-metafields.mjs | 39 +- .../actions/get-metaobjects/common.mjs | 15 - .../get-metaobjects/get-metaobjects.mjs | 34 +- .../shopify/actions/get-pages/common.mjs | 19 - .../shopify/actions/get-pages/get-pages.mjs | 38 +- .../common.mjs | 33 -- .../search-custom-collection-by-name.mjs | 65 ++- .../actions/search-customers/common.mjs | 15 - .../actions/search-product-variant/common.mjs | 63 --- .../search-product-variant.mjs | 123 +++++- .../actions/search-products/common.mjs | 52 --- .../search-products/search-products.mjs | 98 ++++- .../shopify/actions/update-article/common.mjs | 36 -- .../actions/update-article/update-article.mjs | 74 +++- .../actions/update-customer/common.mjs | 26 -- .../actions/update-inventory-level/common.mjs | 33 -- .../update-inventory-level.mjs | 45 +- .../actions/update-metafield/common.mjs | 57 --- .../update-metafield/update-metafield.mjs | 68 ++- .../actions/update-metaobject/common.mjs | 117 ----- .../update-metaobject/update-metaobject.mjs | 108 ++++- .../shopify/actions/update-page/common.mjs | 34 -- .../actions/update-page/update-page.mjs | 46 +- .../actions/update-product-variant/common.mjs | 114 ----- .../update-product-variant.mjs | 134 ++++-- .../shopify/actions/update-product/common.mjs | 59 --- .../actions/update-product/update-product.mjs | 122 ++++-- components/shopify/common/constants.mjs | 130 +++++- components/shopify/common/mutations.mjs | 406 ++++++++++++++++++ components/shopify/common/queries.mjs | 373 +++++++++++++++- components/shopify/common/utils.mjs | 33 ++ components/shopify/shopify.app.mjs | 376 +++++++++++++++- .../new-abandoned-cart/new-abandoned-cart.mjs | 32 ++ .../sources/new-article/new-article.mjs | 2 +- 74 files changed, 2947 insertions(+), 2192 deletions(-) delete mode 100644 components/shopify/actions/add-product-to-custom-collection/common.mjs delete mode 100644 components/shopify/actions/add-tags/common.mjs delete mode 100644 components/shopify/actions/common/common.mjs rename components/shopify/actions/common/{consts.mjs => constants.mjs} (87%) delete mode 100644 components/shopify/actions/common/rules.mjs delete mode 100644 components/shopify/actions/create-article/common.mjs delete mode 100644 components/shopify/actions/create-blog/common.mjs delete mode 100644 components/shopify/actions/create-custom-collection/common.mjs delete mode 100644 components/shopify/actions/create-customer/common.mjs delete mode 100644 components/shopify/actions/create-metafield/common.mjs delete mode 100644 components/shopify/actions/create-metaobject/common.mjs delete mode 100644 components/shopify/actions/create-order/common.mjs delete mode 100644 components/shopify/actions/create-page/common.mjs delete mode 100644 components/shopify/actions/create-product-variant/common.mjs delete mode 100644 components/shopify/actions/create-product/common.mjs delete mode 100644 components/shopify/actions/create-smart-collection/common.mjs delete mode 100644 components/shopify/actions/delete-article/common.mjs delete mode 100644 components/shopify/actions/delete-blog/common.mjs delete mode 100644 components/shopify/actions/delete-metafield/common.mjs delete mode 100644 components/shopify/actions/delete-page/common.mjs delete mode 100644 components/shopify/actions/get-articles/common.mjs delete mode 100644 components/shopify/actions/get-metafields/common.mjs delete mode 100644 components/shopify/actions/get-metaobjects/common.mjs delete mode 100644 components/shopify/actions/get-pages/common.mjs delete mode 100644 components/shopify/actions/search-custom-collection-by-name/common.mjs delete mode 100644 components/shopify/actions/search-customers/common.mjs delete mode 100644 components/shopify/actions/search-product-variant/common.mjs delete mode 100644 components/shopify/actions/search-products/common.mjs delete mode 100644 components/shopify/actions/update-article/common.mjs delete mode 100644 components/shopify/actions/update-customer/common.mjs delete mode 100644 components/shopify/actions/update-inventory-level/common.mjs delete mode 100644 components/shopify/actions/update-metafield/common.mjs delete mode 100644 components/shopify/actions/update-metaobject/common.mjs delete mode 100644 components/shopify/actions/update-page/common.mjs delete mode 100644 components/shopify/actions/update-product-variant/common.mjs delete mode 100644 components/shopify/actions/update-product/common.mjs create mode 100644 components/shopify/common/utils.mjs create mode 100644 components/shopify/sources/new-abandoned-cart/new-abandoned-cart.mjs diff --git a/components/shopify/actions/add-product-to-custom-collection/add-product-to-custom-collection.mjs b/components/shopify/actions/add-product-to-custom-collection/add-product-to-custom-collection.mjs index 5aa2a9b891161..79c42a66937e7 100644 --- a/components/shopify/actions/add-product-to-custom-collection/add-product-to-custom-collection.mjs +++ b/components/shopify/actions/add-product-to-custom-collection/add-product-to-custom-collection.mjs @@ -1,12 +1,10 @@ import shopify from "../../shopify.app.mjs"; -import common from "./common.mjs"; export default { - ...common, key: "shopify-add-product-to-custom-collection", - name: "Add Products to Custom Collections", - description: "Adds a product or products to a custom collection or collections. [See the documentation](https://shopify.dev/docs/api/admin-rest/2023-01/resources/collect#post-collects)", - version: "0.0.6", + name: "Add Products to Custom Collection", + description: "Adds a product or products to a custom collection. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/collectionAddProductsV2)", + version: "0.0.7", type: "action", props: { shopify, @@ -18,15 +16,23 @@ export default { type: "string[]", label: "Product IDs", }, - collectionIds: { + collectionId: { propDefinition: [ shopify, "collectionId", ], - type: "string[]", - label: "Collection IDs", - description: "IDs of the collections that the product will be added to", optional: false, }, }, + async run({ $ }) { + const response = await this.shopify.addProductsToCollection({ + id: this.collectionId, + productIds: this.productIds, + }); + if (response.collectionAddProductsV2.userErrors.length > 0) { + throw new Error(response.collectionAddProductsV2.userErrors[0].message); + } + $.export("$summary", `Added product(s) \`${this.productIds}\` to collection \`${this.collectionId}\``); + return response; + }, }; diff --git a/components/shopify/actions/add-product-to-custom-collection/common.mjs b/components/shopify/actions/add-product-to-custom-collection/common.mjs deleted file mode 100644 index 64d876d17cee7..0000000000000 --- a/components/shopify/actions/add-product-to-custom-collection/common.mjs +++ /dev/null @@ -1,19 +0,0 @@ -export default { - async run({ $ }) { - const results = []; - - for (const productId of this.productIds) { - for (const collectionId of this.collectionIds) { - const data = { - product_id: productId, - collection_id: collectionId, - }; - const { result } = await this.shopify.createCollect(data); - results.push(result); - } - } - - $.export("$summary", `Added ${this.productIds.length} product(s) to ${this.collectionIds.length} collection(s).`); - return results; - }, -}; diff --git a/components/shopify/actions/add-tags/add-tags.mjs b/components/shopify/actions/add-tags/add-tags.mjs index 3e454d9d31dea..15aa44765d4e7 100644 --- a/components/shopify/actions/add-tags/add-tags.mjs +++ b/components/shopify/actions/add-tags/add-tags.mjs @@ -1,15 +1,72 @@ import shopify from "../../shopify.app.mjs"; -import common from "./common.mjs"; export default { - ...common, - name: "Add Tags", - version: "0.0.12", key: "shopify-add-tags", - description: "Add tags. [See the documentation](https://shopify.dev/api/admin-graphql/2022-07/mutations/tagsadd)", + name: "Add Tags", + description: "Add tags. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/tagsAdd)", + version: "0.0.13", type: "action", props: { shopify, - ...common.props, + resource: { + type: "string", + label: "Resource Type", + description: "The Shopify Admin API resource type.", + options: [ + "Product", + "Customer", + "Order", + "DraftOrder", + "Article", + ], + reloadProps: true, + }, + gid: { + type: "string", + label: "Resource ID", + description: "The Shopify Admin Resource ID. For example, the ID of a Product, Customer, Order, DraftOrder, or OnlineStoreArticle. Can be found at the end of the URL in Shopify Admin.", + }, + tags: { + propDefinition: [ + shopify, + "tags", + ], + }, + }, + async additionalProps(existingProps) { + const props = {}; + if (!this.resource) { + return props; + } + if (this.resource === "Product") { + props.gid = { + ...existingProps.gid, + options: async ({ prevContext }) => { + try { + return await this.shopify.getPropOptions({ + resourceFn: this.shopify.listProducts, + resourceKeys: [ + "products", + ], + prevContext, + }); + } catch { + console.log("Unable to fetch product options"); + } + }, + }; + } + return props; + }, + async run({ $ }) { + const response = await this.shopify.addTags({ + gid: this.gid, + tags: this.tags, + }); + if (response.tagsAdd.userErrors.length > 0) { + throw new Error(response.tagsAdd.userErrors[0].message); + } + $.export("$summary", `Added tag(s) \`${this.tags}\` with id \`${response.tagsAdd.node.id}\``); + return response; }, }; diff --git a/components/shopify/actions/add-tags/common.mjs b/components/shopify/actions/add-tags/common.mjs deleted file mode 100644 index 7e3986fe78dd8..0000000000000 --- a/components/shopify/actions/add-tags/common.mjs +++ /dev/null @@ -1,46 +0,0 @@ -export default { - props: { - resource: { - label: "Resource Type", - type: "string", - description: "The Shopify Admin API resource type.", - options: [ - "Product", - "Customer", - "Order", - "DraftOrder", - "OnlineStoreArticle", - ], - }, - id: { - label: "Resource ID", - type: "string", - description: - "The Shopify Admin Resource ID. For example, the ID of a Product, Customer, Order, DraftOrder, or OnlineStoreArticle. Can be found at the end of the URL in Shopify Admin.", - }, - tags: { - label: "Tags", - type: "string", - description: "Separate tags with a comma to add multiple tags.", - }, - }, - async run({ $ }) { - const { - resource, - id, - tags, - } = this; - - const res = await this.shopify.addTags(resource, id, tags); - - if (res.tagsAdd.userErrors.length > 0) { - throw new Error(res.tagsAdd.userErrors[0].message); - } - - $.export( - "$summary", - `Added tag(s) \`${tags}\` with id \`${res.tagsAdd.node.id}\``, - ); - return res; - }, -}; diff --git a/components/shopify/actions/common/common.mjs b/components/shopify/actions/common/common.mjs deleted file mode 100644 index 38d5bf289e0c3..0000000000000 --- a/components/shopify/actions/common/common.mjs +++ /dev/null @@ -1,22 +0,0 @@ -/** - * A utility function that accepts a string as an argument and reformats it in - * order to remove newline characters and consecutive spaces. Useful when - * dealing with very long templated strings that are split into multiple lines. - * - * @example - * // returns "This is a much cleaner string" - * toSingleLineString(` - * This is a much - * cleaner string - * `); - * - * @param {string} multiLineString the input string to reformat - * @returns a formatted string based on the content of the input argument, - * without newlines and multiple spaces - */ -export function toSingleLineString(multiLineString) { - return multiLineString - .trim() - .replace(/\n/g, " ") - .replace(/\s{2,}/g, " "); -} diff --git a/components/shopify/actions/common/consts.mjs b/components/shopify/actions/common/constants.mjs similarity index 87% rename from components/shopify/actions/common/consts.mjs rename to components/shopify/actions/common/constants.mjs index 4403f82128436..639fc61472752 100644 --- a/components/shopify/actions/common/consts.mjs +++ b/components/shopify/actions/common/constants.mjs @@ -7,14 +7,6 @@ const RESOURCE_TYPES = [ label: "Collection", value: "collection", }, - { - label: "Customer", - value: "customer", - }, - { - label: "Draft Order", - value: "draft_order", - }, { label: "Page", value: "page", @@ -23,10 +15,6 @@ const RESOURCE_TYPES = [ label: "Product", value: "product", }, - { - label: "Product Image", - value: "product_image", - }, { label: "Product Variant", value: "variants", @@ -35,10 +23,6 @@ const RESOURCE_TYPES = [ label: "Article", value: "article", }, - { - label: "Order", - value: "order", - }, ]; const METAFIELD_TYPES = { diff --git a/components/shopify/actions/common/metafield-actions.mjs b/components/shopify/actions/common/metafield-actions.mjs index d91d70f61125a..4d586195e508b 100644 --- a/components/shopify/actions/common/metafield-actions.mjs +++ b/components/shopify/actions/common/metafield-actions.mjs @@ -1,5 +1,7 @@ import shopify from "../../shopify.app.mjs"; -import consts from "../common/consts.mjs"; +import constants from "./constants.mjs"; +import utils from "../../common/utils.mjs"; +import { MAX_LIMIT } from "../../common/constants.mjs"; export default { props: { @@ -8,7 +10,7 @@ export default { type: "string", label: "Resource Type", description: "Filter by the resource type on which the metafield is attached to", - options: consts.RESOURCE_TYPES, + options: constants.RESOURCE_TYPES, reloadProps: true, }, }, @@ -18,74 +20,91 @@ export default { product: { ...shopify.propDefinitions.productId, options: async ({ prevContext }) => { - return this.shopify.getProductOptions(prevContext); + return this.shopify.getPropOptions({ + resourceFn: this.shopify.listProducts, + resourceKeys: [ + "products", + ], + prevContext, + }); }, }, variants: { ...shopify.propDefinitions.productVariantId, options: async ({ prevContext }) => { - return this.shopify.getProductVariantOptions(this.productId, prevContext); - }, - }, - product_image: { - ...shopify.propDefinitions.imageId, - options: async() => { - return this.shopify.getImageOptions(this.productId); - }, - optional: false, - }, - customer: { - ...shopify.propDefinitions.customerId, - options: async ({ - prevContext, query, - }) => { - return this.shopify.getCustomerOptions(prevContext, query); + return this.shopify.getPropOptions({ + resourceFn: this.shopify.listProductVariants, + resourceKeys: [ + "productVariants", + ], + variables: { + query: `product_id:${this.productId.split("/").pop()}`, + }, + prevContext, + }); }, }, collection: { ...shopify.propDefinitions.collectionId, - options: async () => { - return this.shopify.getCollectionOptions(); + options: async ({ prevContext }) => { + return this.shopify.getPropOptions({ + resourceFn: this.shopify.listCollections, + resourceKeys: [ + "collections", + ], + prevContext, + }); }, optional: false, }, blog: { ...shopify.propDefinitions.blogId, - options: async () => { - return this.shopify.getBlogOptions(); + options: async ({ prevContext }) => { + return this.shopify.getPropOptions({ + resourceFn: this.shopify.listBlogs, + resourceKeys: [ + "blogs", + ], + prevContext, + }); }, }, article: { ...shopify.propDefinitions.articleId, - options: async () => { - return this.shopify.getArticleOptions(this.blogId); + options: async ({ prevContext }) => { + return this.shopify.getPropOptions({ + resourceFn: this.shopify.listBlogArticles, + resourceKeys: [ + "blog", + "articles", + ], + variables: { + id: this.blogId, + }, + prevContext, + }); }, }, page: { ...shopify.propDefinitions.pageId, - options: async () => { - return this.shopify.getPageOptions(); - }, - }, - order: { - ...shopify.propDefinitions.orderId, - options: async () => { - return this.shopify.getOrderOptions(); - }, - }, - draft_order: { - ...shopify.propDefinitions.draftOrderId, - options: async () => { - return this.shopify.getDraftOrderOptions(); + options: async ({ prevContext }) => { + return this.shopify.getPropOptions({ + resourceFn: this.shopify.listPages, + resourceKeys: [ + "pages", + ], + prevContext, + }); }, }, }; const props = {}; - if (ownerResource === "variants" || ownerResource === "product_image") { + if (ownerResource === "variants") { props.productId = resources.product; } + if (ownerResource === "article") { props.blogId = resources.blog; } @@ -95,13 +114,67 @@ export default { ownerId: resources[ownerResource], }; }, - async getMetafieldIdByKey(key, namespace, ownerId, ownerResource) { - const results = await this.shopify.listMetafields({ - metafield: { - owner_resource: ownerResource, - owner_id: ownerId, - }, + async getMetafields({ + resourceFn, resourceKeys = [], variables, + }) { + let results = await resourceFn(variables); + for (const key of resourceKeys) { + results = results[key]; + } + return results.metafields.nodes; + }, + async listMetafields(ownerResource, ownerId) { + const variables = { + id: ownerId, + first: MAX_LIMIT, + }; + let resourceFn, resourceKeys; + if (ownerResource === "product") { + resourceFn = this.shopify.getProduct; + resourceKeys = [ + "product", + ]; + } + if (ownerResource === "variants") { + resourceFn = this.shopify.getProductVariant; + resourceKeys = [ + "productVariant", + ]; + } + if (ownerResource === "collection") { + resourceFn = this.shopify.getCollection; + resourceKeys = [ + "collection", + ]; + } + if (ownerResource === "blog") { + resourceFn = this.shopify.getBlog; + resourceKeys = [ + "blog", + ]; + } + if (ownerResource === "article") { + resourceFn = this.shopify.getArticle; + resourceKeys = [ + "article", + ]; + } + if (ownerResource === "page") { + resourceFn = this.shopify.getPage; + resourceKeys = [ + "page", + ]; + } + + return this.getMetafields({ + resourceFn, + resourceKeys, + variables, }); + }, + async getMetafieldIdByKey(key, namespace, ownerId, ownerResource) { + const results = await this.listMetafields(ownerResource, ownerId); + const metafield = results?.filter( (field) => field.key === key && field.namespace === namespace, ); @@ -111,8 +184,11 @@ export default { return metafield[0].id; }, async createMetafieldsArray(metafieldsOriginal, ownerId, ownerResource) { + if (!metafieldsOriginal) { + return; + } const metafields = []; - const metafieldsArray = this.shopify.parseArrayOfJSONStrings(metafieldsOriginal); + const metafieldsArray = utils.parseJson(metafieldsOriginal); for (const meta of metafieldsArray) { if (meta.id) { metafields.push(meta); diff --git a/components/shopify/actions/common/metaobjects.mjs b/components/shopify/actions/common/metaobjects.mjs index c0c8b74c853a8..700e894976dce 100644 --- a/components/shopify/actions/common/metaobjects.mjs +++ b/components/shopify/actions/common/metaobjects.mjs @@ -1,146 +1,36 @@ -import { axios } from "@pipedream/platform"; +import queries from "../../common/queries.mjs"; +import mutations from "../../common/mutations.mjs"; export default { methods: { - async makeGraphQLRequest({ - $ = this, ...args - }) { - return axios($, { - url: `https://${this.shopify.getShopId()}.myshopify.com/admin/api/2023-04/graphql.json`, - method: "POST", - headers: { - "X-Shopify-Access-Token": `${this.shopify.$auth.oauth_access_token}`, - }, - ...args, - }); - }, - formatFields(fields) { - return JSON.stringify(fields).replace(new RegExp("\"key\"", "g"), "key") - .replace(new RegExp("\"value\"", "g"), "value"); - }, - getMetaobject({ - id, $, - }) { - return this.makeGraphQLRequest({ - data: { - query: `query { - metaobject(id: "${id}") { - id - handle - type - displayName - updatedAt - fields { - key - type - value - definition { - name - } - } - } - }`, - }, - $, - }); - }, - listMetaobjects({ - type, $, - }) { - return this.makeGraphQLRequest({ - data: { - query: `query { - metaobjects(type: "${type}", first: 100) { - nodes { - id - handle - type - displayName - updatedAt - fields { - key - type - value - } - } - } - }`, - }, - $, - }); - }, - listMetaobjectDefinitions({ $ } = {}) { - return this.makeGraphQLRequest({ - data: { - query: `query { - metaobjectDefinitions(first: 100) { - nodes { - id - name - type - fieldDefinitions { - key - type { - name - } - name - } - } - } - }`, - }, - $, + getMetaobject({ id }) { + return this.shopify._makeGraphQlRequest(queries.GET_METAOBJECT, { + id, }); }, createMetaobject({ - type, fields, $, + type, fields, }) { - return this.makeGraphQLRequest({ - data: { - query: `mutation metaobjectCreate { - metaobjectCreate(metaobject: { - type: "${type}", - capabilities: { - publishable: { - status: ACTIVE - } - } - fields: ${this.formatFields(fields)} - }) { - metaobject { - id - handle - type - } - userErrors { - field - message - } - } - }`, + return this.shopify._makeGraphQlRequest(mutations.CREATE_METAOBJECT, { + metaobject: { + type, + capabilities: { + publishable: { + status: "ACTIVE", + }, + }, + fields, }, - $, }); }, updateMetaobject({ - id, fields, $, + id, fields, }) { - return this.makeGraphQLRequest({ - data: { - query: `mutation metaobjectUpdate { - metaobjectUpdate(id: "${id}", metaobject: { fields: ${this.formatFields(fields)} }) { - metaobject { - id - type - } - userErrors { - field - message - } - } - }`, + return this.shopify._makeGraphQlRequest(mutations.UPDATE_METAOBJECT, { + id, + metaobject: { + fields, }, - $, }); }, }, diff --git a/components/shopify/actions/common/rules.mjs b/components/shopify/actions/common/rules.mjs deleted file mode 100644 index f9a1991670815..0000000000000 --- a/components/shopify/actions/common/rules.mjs +++ /dev/null @@ -1,25 +0,0 @@ -const rules = { - column: [ - "title", - "type", - "vendor", - "variant_title", - "variant_compare_at_price", - "variant_weight", - "variant_inventory", - "variant_price", - "tag", - ], - relation: [ - "greater_than", - "less_than", - "equals", - "not_equals", - "starts_with", - "ends_with", - "contains", - "not_contains", - ], -}; - -export default rules; diff --git a/components/shopify/actions/create-article/common.mjs b/components/shopify/actions/create-article/common.mjs deleted file mode 100644 index 7180aa903f479..0000000000000 --- a/components/shopify/actions/create-article/common.mjs +++ /dev/null @@ -1,34 +0,0 @@ -export default { - methods: { - createArticle({ - blogId, ...args - } = {}) { - return this.app.post({ - path: `/blogs/${blogId}/articles`, - ...args, - }); - }, - }, - async run({ $: step }) { - const { - blogId, - title, - bodyHtml, - } = this; - - const response = await this.createArticle({ - step, - blogId, - data: { - article: { - title, - body_html: bodyHtml, - }, - }, - }); - - step.export("$summary", `Created new page with ID ${response.article.id}`); - - return response; - }, -}; diff --git a/components/shopify/actions/create-article/create-article.mjs b/components/shopify/actions/create-article/create-article.mjs index 20473ff5d2bfe..f941d159a6301 100644 --- a/components/shopify/actions/create-article/create-article.mjs +++ b/components/shopify/actions/create-article/create-article.mjs @@ -1,34 +1,75 @@ -import app from "../../common/rest-admin.mjs"; -import common from "./common.mjs"; +import shopify from "../../shopify.app.mjs"; export default { - ...common, key: "shopify-create-article", name: "Create Article", - description: "Create a new blog article. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/article#post-blogs-blog-id-articles)", - version: "0.0.6", + description: "Create a new blog article. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/articleCreate)", + version: "0.0.7", type: "action", props: { - app, + shopify, blogId: { propDefinition: [ - app, + shopify, "blogId", ], }, title: { - description: "The title of the article.", - propDefinition: [ - app, - "title", - ], + type: "string", + label: "Title", + description: "The title of the article", + }, + author: { + type: "string", + label: "Author", + description: "The name of the author of the article", + }, + body: { + type: "string", + label: "Body", + description: "The text content of the article, complete with HTML markup", + optional: true, }, - bodyHtml: { - description: "The text content of the article, complete with HTML markup.", + summary: { + type: "string", + label: "Summary", + description: "A summary of the article, which can include HTML markup. The summary is used by the online store theme to display the article on other pages, such as the home page or the main blog page.", + optional: true, + }, + image: { + type: "string", + label: "Image", + description: "The URL of the image associated with the article", + optional: true, + }, + tags: { propDefinition: [ - app, - "bodyHtml", + shopify, + "tags", ], + optional: true, }, }, + async run({ $ }) { + const response = await this.shopify.createArticle({ + article: { + blogId: this.blogId, + title: this.title, + author: { + name: this.author, + }, + body: this.body, + summary: this.summary, + image: this.image && { + url: this.image, + }, + tags: this.tags, + }, + }); + if (response.articleCreate.userErrors.length > 0) { + throw new Error(response.articleCreate.userErrors[0].message); + } + $.export("$summary", `Created new article with ID ${response.articleCreate.article.id}`); + return response; + }, }; diff --git a/components/shopify/actions/create-blog/common.mjs b/components/shopify/actions/create-blog/common.mjs deleted file mode 100644 index c25960782ca0d..0000000000000 --- a/components/shopify/actions/create-blog/common.mjs +++ /dev/null @@ -1,26 +0,0 @@ -export default { - methods: { - createBlog(args = {}) { - return this.app.post({ - path: "/blogs", - ...args, - }); - }, - }, - async run({ $: step }) { - const { title } = this; - - const response = await this.createBlog({ - step, - data: { - blog: { - title, - }, - }, - }); - - step.export("$summary", `Created new page with ID ${response.blog.id}`); - - return response; - }, -}; diff --git a/components/shopify/actions/create-blog/create-blog.mjs b/components/shopify/actions/create-blog/create-blog.mjs index d2801dee2caec..106677f3c56ce 100644 --- a/components/shopify/actions/create-blog/create-blog.mjs +++ b/components/shopify/actions/create-blog/create-blog.mjs @@ -1,21 +1,29 @@ -import app from "../../common/rest-admin.mjs"; -import common from "./common.mjs"; +import shopify from "../../shopify.app.mjs"; export default { - ...common, key: "shopify-create-blog", name: "Create Blog", - description: "Create a new blog. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/blog#post-blogs)", - version: "0.0.6", + description: "Create a new blog. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/blogCreate)", + version: "0.0.7", type: "action", props: { - app, + shopify, title: { + type: "string", + label: "Title", description: "The title of the blog.", - propDefinition: [ - app, - "title", - ], }, }, + async run({ $ }) { + const response = await this.shopify.createBlog({ + blog: { + title: this.title, + }, + }); + if (response.blogCreate.userErrors.length > 0) { + throw new Error(response.blogCreate.userErrors[0].message); + } + $.export("$summary", `Created new blog with ID ${response.blogCreate.blog.id}`); + return response; + }, }; diff --git a/components/shopify/actions/create-custom-collection/common.mjs b/components/shopify/actions/create-custom-collection/common.mjs deleted file mode 100644 index 1429bf14d4bb6..0000000000000 --- a/components/shopify/actions/create-custom-collection/common.mjs +++ /dev/null @@ -1,35 +0,0 @@ -export default { - props: { - imageUrl: { - type: "string", - label: "Image URL", - description: "The source URL that specifies the location of the image", - optional: true, - }, - published: { - type: "boolean", - label: "Published", - description: "Whether the custom collection is published to the Online Store channel", - optional: true, - }, - }, - async run({ $ }) { - const collects = this.products?.map((product) => ({ - product_id: product, - })) || []; - - const data = { - title: this.title, - collects, - image: { - src: this.imageUrl, - }, - published: this.published, - metafields: this.shopify.parseArrayOfJSONStrings(this.metafields), - }; - - const { result } = await this.shopify.createCustomCollection(data); - $.export("$summary", `Created new custom collection \`${result.title}\` with ID \`${result.id}\``); - return result; - }, -}; diff --git a/components/shopify/actions/create-custom-collection/create-custom-collection.mjs b/components/shopify/actions/create-custom-collection/create-custom-collection.mjs index e24b085fea73f..491843c202795 100644 --- a/components/shopify/actions/create-custom-collection/create-custom-collection.mjs +++ b/components/shopify/actions/create-custom-collection/create-custom-collection.mjs @@ -1,20 +1,17 @@ import shopify from "../../shopify.app.mjs"; -import common from "./common.mjs"; +import utils from "../../common/utils.mjs"; export default { - ...common, key: "shopify-create-custom-collection", name: "Create Custom Collection", - description: "Create a new custom collection. [See the documentation](https://shopify.dev/docs/api/admin-rest/2023-01/resources/customcollection#post-custom-collections)", - version: "0.0.6", + description: "Create a new custom collection. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/collectionCreate)", + version: "0.0.7", type: "action", props: { shopify, title: { - propDefinition: [ - shopify, - "title", - ], + type: "string", + label: "Title", description: "The name of the custom collection", }, products: { @@ -31,6 +28,28 @@ export default { "metafields", ], }, - ...common.props, + imageUrl: { + type: "string", + label: "Image URL", + description: "The source URL that specifies the location of the image", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.shopify.createCollection({ + input: { + title: this.title, + products: this.products, + metafields: this.metafields && utils.parseJson(this.metafields), + image: this.imageUrl && { + src: this.imageUrl, + }, + }, + }); + if (response.collectionCreate.userErrors.length > 0) { + throw new Error(response.collectionCreate.userErrors[0].message); + } + $.export("$summary", `Created new custom collection \`${this.title}\` with ID \`${response.collectionCreate.collection.id}\``); + return response; }, }; diff --git a/components/shopify/actions/create-customer/common.mjs b/components/shopify/actions/create-customer/common.mjs deleted file mode 100644 index f66b1e027b3cb..0000000000000 --- a/components/shopify/actions/create-customer/common.mjs +++ /dev/null @@ -1,33 +0,0 @@ -export default { - props: { - sendEmailInvite: { - type: "boolean", - label: "Send Email Invite", - description: "Send email invite to address", - optional: true, - }, - }, - async run({ $ }) { - let data = { - first_name: this.firstName, - last_name: this.lastName, - email: this.email, - phone: this.phone, - addresses: [ - { - address1: this.address, - company: this.company, - city: this.city, - province: this.province, - country: this.country, - zip: this.zip, - }, - ], - send_email_invite: this.sendEmailInvite, - }; - - let response = (await this.shopify.createCustomer(data)).result; - $.export("$summary", `Created new customer \`${this.email}\` with id \`${response.id}\``); - return response; - }, -}; diff --git a/components/shopify/actions/create-metafield/common.mjs b/components/shopify/actions/create-metafield/common.mjs deleted file mode 100644 index 7be866818eeaa..0000000000000 --- a/components/shopify/actions/create-metafield/common.mjs +++ /dev/null @@ -1,55 +0,0 @@ -import consts from "../common/consts.mjs"; - -export default { - props: { - namespace: { - type: "string", - label: "Namespace", - description: "A container for a group of metafields. Grouping metafields within a namespace prevents your metafields from conflicting with other metafields with the same key name. Must have between 3-255 characters.", - }, - key: { - type: "string", - label: "Key", - description: "The key of the metafield. Keys can be up to 64 characters long and can contain alphanumeric characters, hyphens, underscores, and periods.", - }, - type: { - type: "string", - label: "Type", - description: "The type of data that the metafield stores in the `value` field. Refer to the list of [supported types](https://shopify.dev/apps/custom-data/metafields/types).", - async options() { - return Object.keys(consts.METAFIELD_TYPES); - }, - reloadProps: true, - }, - }, - async additionalProps() { - const props = await this.getOwnerIdProp(this.ownerResource); - - if (this.type) { - props.value = { - type: consts.METAFIELD_TYPES[this.type], - label: "Value", - description: "The data to store in the metafield", - }; - } - - return props; - }, - async run({ $ }) { - const value = this.type.includes("list.") - ? JSON.stringify(this.value) - : this.value; - - const params = { - owner_id: `${this.ownerId}`, - owner_resource: this.ownerResource, - key: this.key, - type: this.type, - value, - namespace: this.namespace, - }; - const response = await this.shopify.createMetafield(params); - $.export("$summary", `Created metafield for object with ID ${this.ownerId}`); - return response; - }, -}; diff --git a/components/shopify/actions/create-metafield/create-metafield.mjs b/components/shopify/actions/create-metafield/create-metafield.mjs index f4f4fd5014cf8..24bb5b1b4b60b 100644 --- a/components/shopify/actions/create-metafield/create-metafield.mjs +++ b/components/shopify/actions/create-metafield/create-metafield.mjs @@ -1,19 +1,60 @@ -import metafieldActions from "../common/metafield-actions.mjs"; -import common from "./common.mjs"; +import common from "../common/metafield-actions.mjs"; +import constants from "../common/constants.mjs"; export default { ...common, key: "shopify-create-metafield", name: "Create Metafield", - description: "Creates a metafield belonging to a resource. [See the docs](https://shopify.dev/api/admin-rest/2023-01/resources/metafield#post-blogs-blog-id-metafields)", - version: "0.0.10", + description: "Creates a metafield belonging to a resource. [See the documentation](https://shopify.dev/docs/api/admin-graphql/unstable/mutations/metafieldsSet)", + version: "0.0.11", type: "action", props: { - ...metafieldActions.props, ...common.props, + namespace: { + type: "string", + label: "Namespace", + description: "A container for a group of metafields. Grouping metafields within a namespace prevents your metafields from conflicting with other metafields with the same key name. Must have between 3-255 characters.", + }, + key: { + type: "string", + label: "Key", + description: "The key of the metafield. Keys can be up to 64 characters long and can contain alphanumeric characters, hyphens, underscores, and periods.", + }, + type: { + type: "string", + label: "Type", + description: "The type of data that the metafield stores in the `value` field. Refer to the list of [supported types](https://shopify.dev/apps/custom-data/metafields/types).", + options: Object.keys(constants.METAFIELD_TYPES), + reloadProps: true, + }, }, - methods: { - ...metafieldActions.methods, - ...common.methods, + async additionalProps() { + const props = await this.getOwnerIdProp(this.ownerResource); + + if (this.type) { + props.value = { + type: "string", + label: "Value", + description: "The data to store in the metafield", + }; + } + + return props; + }, + async run({ $ }) { + const response = await this.shopify.createMetafield({ + metafields: { + key: this.key, + type: this.type, + value: this.value, + namespace: this.namespace, + ownerId: this.ownerId, + }, + }); + if (response.metafieldsSet.userErrors.length > 0) { + throw new Error(response.metafieldsSet.userErrors[0].message); + } + $.export("$summary", `Created metafield for object with ID ${this.ownerId}`); + return response; }, }; diff --git a/components/shopify/actions/create-metaobject/common.mjs b/components/shopify/actions/create-metaobject/common.mjs deleted file mode 100644 index f28a6d20c77f7..0000000000000 --- a/components/shopify/actions/create-metaobject/common.mjs +++ /dev/null @@ -1,59 +0,0 @@ -import consts from "../common/consts.mjs"; - -export default { - async additionalProps() { - const props = {}; - if (!this.type) { - return props; - } - const { data: { metaobjectDefinitions: { nodes } } } = await this.listMetaobjectDefinitions(); - const { fieldDefinitions } = nodes.find(({ type }) => type === this.type); - for (const def of fieldDefinitions) { - props[def.key] = { - type: consts.METAFIELD_TYPES[def.type.name], - label: def.name, - optional: true, - }; - } - return props; - }, - async run({ $ }) { - const { data: { metaobjectDefinitions: { nodes } } } = await this.listMetaobjectDefinitions(); - const { fieldDefinitions } = nodes.find(({ type }) => type === this.type); - - const fields = []; - for (const def of fieldDefinitions) { - if (this[def.key]) { - fields.push({ - key: def.key, - value: typeof this[def.key] === "string" - ? this[def.key] - : JSON.stringify(this[def.key]), - }); - } - } - - const response = await this.createMetaobject({ - type: this.type, - fields, - $, - }); - - let errorMessage; - if (response?.errors?.length) { - errorMessage = response.errors[0].message; - } - if (response?.data?.metaobjectCreate?.userErrors?.length) { - errorMessage = response.data.metaobjectCreate.userErrors[0].message; - } - if (errorMessage) { - throw new Error(`${errorMessage}`); - } - - if (response?.data?.metaobjectCreate?.metaobject?.id) { - $.export("$summary", `Successfully created metaobject with ID ${response.data.metaobjectCreate.metaobject.id}`); - } - - return response; - }, -}; diff --git a/components/shopify/actions/create-metaobject/create-metaobject.mjs b/components/shopify/actions/create-metaobject/create-metaobject.mjs index 951f2f510bda0..c0db75759b8eb 100644 --- a/components/shopify/actions/create-metaobject/create-metaobject.mjs +++ b/components/shopify/actions/create-metaobject/create-metaobject.mjs @@ -1,27 +1,73 @@ import shopify from "../../shopify.app.mjs"; -import metaobjects from "../common/metaobjects.mjs"; -import common from "./common.mjs"; +import common from "../common/metaobjects.mjs"; +import { MAX_LIMIT } from "../../common/constants.mjs"; export default { - ...metaobjects, ...common, key: "shopify-create-metaobject", name: "Create Metaobject", - description: "Creates a metaobject. [See the documentation](https://shopify.dev/docs/api/admin-graphql/2023-04/mutations/metaobjectCreate)", - version: "0.0.5", + description: "Creates a metaobject. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/metaobjectCreate)", + version: "0.0.6", type: "action", props: { shopify, type: { - type: "string", - label: "Type", - description: "The Metaobject Type", - async options() { - const { data: { metaobjectDefinitions: { nodes } } } - = await this.listMetaobjectDefinitions(); - return nodes?.map(({ type }) => type) || []; - }, + propDefinition: [ + shopify, + "metaobjectType", + ], reloadProps: true, }, }, + async additionalProps() { + const props = {}; + if (!this.type) { + return props; + } + const { metaobjectDefinitions: { nodes } } = await this.shopify.listMetaobjectDefinitions({ + first: MAX_LIMIT, + }); + const { fieldDefinitions } = nodes.find(({ id }) => id === this.type); + for (const def of fieldDefinitions) { + props[def.key] = { + type: "string", + label: def.name, + optional: true, + }; + } + return props; + }, + async run({ $ }) { + const { metaobjectDefinitions: { nodes } } = await this.shopify.listMetaobjectDefinitions({ + first: MAX_LIMIT, + }); + const { + fieldDefinitions, type, + } = nodes.find(({ id }) => id === this.type); + + const fields = []; + for (const def of fieldDefinitions) { + if (this[def.key]) { + fields.push({ + key: def.key, + value: this[def.key], + }); + } + } + + const response = await this.createMetaobject({ + type, + fields, + }); + + if (response.metaobjectCreate.userErrors.length > 0) { + throw new Error(response.metaobjectCreate.userErrors[0].message); + } + + if (response?.metaobjectCreate?.metaobject?.id) { + $.export("$summary", `Successfully created metaobject with ID ${response.metaobjectCreate.metaobject.id}`); + } + + return response; + }, }; diff --git a/components/shopify/actions/create-order/common.mjs b/components/shopify/actions/create-order/common.mjs deleted file mode 100644 index e17ee06def94c..0000000000000 --- a/components/shopify/actions/create-order/common.mjs +++ /dev/null @@ -1,173 +0,0 @@ -import { toSingleLineString } from "../common/common.mjs"; - -export default { - props: { - lineItems: { - type: "string[]", - label: "Line Items", - description: toSingleLineString(` - A list of line item objects, each containing information about an item in the order. - Example: \`{ "variant_id": 447654529, "quantity": 1, "price": 2.50, "name": "Name", "title": "Title" }\`. - More details when searching **line_items** in [Shopify Order Object](https://shopify.dev/api/admin-rest/2022-01/resources/order#resource_object) - `), - }, - billingAddress: { - type: "object", - label: "Billing Address", - description: toSingleLineString(` - The mailing address associated with the payment method. - More details when searching **billing_address** in [Shopify Order Object](https://shopify.dev/api/admin-rest/2022-01/resources/order#resource_object) - `), - optional: true, - }, - shippingAddress: { - type: "object", - label: "Shipping Address", - description: toSingleLineString(` - The mailing address to where the order will be shipped. - More details when searching **billing_address** in [Shopify Order Object](https://shopify.dev/api/admin-rest/2022-01/resources/order#resource_object) - `), - optional: true, - }, - financialStatus: { - type: "string", - label: "Financial Status", - description: "The status of payments associated with the order. Can only be set when the order is created", - options: [ - "pending", - "authorized", - "partially_paid", - "paid", - "partially_refunded", - "refunded", - "voided", - ], - optional: true, - }, - discountCode: { - type: "object", - label: "Discount Code", - description: toSingleLineString(` - A discount applied to the order. - Example: \`{ "code": "SPRING30", "type": "fixed_amount", "amount": "30.00" }\`. - More details when searching **discount_codes** in [Shopify Order Object](https://shopify.dev/api/admin-rest/2022-01/resources/order#resource_object) - `), - optional: true, - }, - fulfillments: { - type: "string[]", - label: "Fulfillments", - description: "An array of fulfillments associated with the order. For more information, see the [Fulfillment API](https://shopify.dev/api/admin-rest/2022-01/resources/fulfillment)", - optional: true, - }, - fulfillmentStatus: { - type: "string", - label: "Fulfillment Status", - description: "The order's status in terms of fulfilled line items", - options: [ - "fulfilled", - "null", - "partial", - "restocked", - ], - optional: true, - }, - sendReceipt: { - type: "boolean", - label: "Send Receipt", - description: "Whether to send an order confirmation to the customer", - optional: true, - }, - sendFulfillmentReceipt: { - type: "boolean", - label: "Send Fulfillment Receipt", - description: "Whether to send a shipping confirmation to the customer", - optional: true, - }, - taxLines: { - type: "string[]", - label: "Tax Lines", - description: toSingleLineString(` - An array of tax line objects, each of which details a tax applicable to the order. - Example: \`[ { "rate": 0.06, "price": 11.94, "title": "State Tax", "channel_liable": true } ]\`. - More details when searching **tax_lines** in [Shopify Order Object](https://shopify.dev/api/admin-rest/2022-01/resources/order#resource_object) - `), - optional: true, - }, - currency: { - type: "string", - label: "Currency", - description: "The three-letter code ([ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) format) for the shop currency", - optional: true, - }, - note: { - type: "string", - label: "Note", - description: "An optional note that a shop owner can attach to the order", - optional: true, - }, - noteAttributes: { - type: "string[]", - label: "Note Attributes", - description: "Extra information that is added to the order. Appears in the Additional details section of an order details page. Each array entry must contain an object with `name` and `value` keys.", - optional: true, - }, - inventoryBehavior: { - type: "string", - label: "Inventory Behavior", - description: "The behaviour to use when updating inventory", - options: [ - { - value: "bypass", - label: "Do not claim inventory", - }, - { - value: "decrement_ignoring_policy", - label: "Ignore the product's inventory policy and claim inventory", - }, - { - value: "decrement_obeying_policy", - label: "Follow the product's inventory policy and claim inventory, if possible", - }, - ], - optional: true, - }, - shippingLines: { - type: "string[]", - label: "Shipping Lines", - description: toSingleLineString(` - An array of objects, each of which details a shipping method used. - More details when searching **shipping_lines** in [Shopify Order Object](https://shopify.dev/api/admin-rest/2022-01/resources/order#resource_object) - `), - optional: true, - }, - }, - async run({ $ }) { - let data = { - line_items: this.shopify.parseArrayOfJSONStrings(this.lineItems), - billing_address: this.shopify.parseJSONStringObjects(this.billingAddress), - shipping_address: this.shopify.parseJSONStringObjects(this.shippingAddress), - financial_status: this.financialStatus, - discount_codes: [ - this.shopify.parseJSONStringObjects(this.discountCode), - ], - fulfillments: this.shopify.parseArrayOfJSONStrings(this.fulfillments), - fulfillment_status: this.fulfillmentStatus, - send_receipt: this.sendReceipt, - send_fulfillment_receipt: this.sendFulfillmentReceipt, - tax_lines: this.shopify.parseArrayOfJSONStrings(this.taxLines), - currency: this.currency, - customer: { - id: this.customerId, - }, - note: this.note, - note_attributes: this.shopify.parseArrayOfJSONStrings(this.noteAttributes), - inventory_behaviour: this.inventoryBehavior, - shipping_lines: this.shopify.parseArrayOfJSONStrings(this.shippingLines), - }; - - let response = (await this.shopify.createOrder(data)).result; - $.export("$summary", `Created new order with id \`${response.id}\``); - return response; - }, -}; diff --git a/components/shopify/actions/create-page/common.mjs b/components/shopify/actions/create-page/common.mjs deleted file mode 100644 index 07f339ab2225c..0000000000000 --- a/components/shopify/actions/create-page/common.mjs +++ /dev/null @@ -1,30 +0,0 @@ -export default { - methods: { - createPage(args = {}) { - return this.app.post({ - path: "/pages", - ...args, - }); - }, - }, - async run({ $: step }) { - const { - title, - bodyHtml, - } = this; - - const response = await this.createPage({ - step, - data: { - page: { - title, - body_html: bodyHtml, - }, - }, - }); - - step.export("$summary", `Created new page with ID ${response.page.id}`); - - return response; - }, -}; diff --git a/components/shopify/actions/create-page/create-page.mjs b/components/shopify/actions/create-page/create-page.mjs index 33a2eb5ca38f9..2ec46e74a94ce 100644 --- a/components/shopify/actions/create-page/create-page.mjs +++ b/components/shopify/actions/create-page/create-page.mjs @@ -1,27 +1,35 @@ -import app from "../../common/rest-admin.mjs"; -import common from "./common.mjs"; +import shopify from "../../shopify.app.mjs"; export default { - ...common, key: "shopify-create-page", name: "Create Page", - description: "Create a new page. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/page#post-pages)", - version: "0.0.6", + description: "Create a new page. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/pageCreate)", + version: "0.0.7", type: "action", props: { - app, + shopify, title: { + type: "string", + label: "Title", description: "The title of the page.", - propDefinition: [ - app, - "title", - ], }, - bodyHtml: { - propDefinition: [ - app, - "bodyHtml", - ], + body: { + type: "string", + label: "Body", + description: "The text content of the page, complete with HTML markup", }, }, + async run({ $ }) { + const response = await this.shopify.createPage({ + page: { + title: this.title, + body: this.body, + }, + }); + if (response.pageCreate.userErrors.length > 0) { + throw new Error(response.pageCreate.userErrors[0].message); + } + $.export("$summary", `Created new page with ID ${response.pageCreate.page.id}`); + return response; + }, }; diff --git a/components/shopify/actions/create-product-variant/common.mjs b/components/shopify/actions/create-product-variant/common.mjs deleted file mode 100644 index 5ad25688143f6..0000000000000 --- a/components/shopify/actions/create-product-variant/common.mjs +++ /dev/null @@ -1,69 +0,0 @@ -import { ConfigurationError } from "@pipedream/platform"; - -export default { - props: { - available: { - type: "string", - label: "Available Quantity", - description: "Sets the available inventory quantity", - optional: true, - }, - barcode: { - type: "string", - label: "Barcode", - description: "The barcode, UPC, or ISBN number for the product", - optional: true, - }, - weight: { - type: "string", - label: "Weight", - description: "The weight of the product variant in the unit system specified with Weight Unit", - optional: true, - }, - weightUnit: { - type: "string", - label: "Weight Unit", - description: "The unit of measurement that applies to the product variant's weight. If you don't specify a value for weight_unit, then the shop's default unit of measurement is applied.", - optional: true, - options: [ - "g", - "kg", - "oz", - "lb", - ], - }, - }, - async run({ $ }) { - if (this.available && !this.locationId) { - throw new ConfigurationError("Must enter LocationId to set the available quantity"); - } - - const productVariant = { - option1: this.option, - price: this.price, - image_id: this.imageId, - sku: this.sku, - barcode: this.barcode, - weight: this.weight, - weight_unit: this.weightUnit, - }; - let { result } = await this.shopify.createProductVariant( - this.productId, - productVariant, - ); - - if (this.available) { - const { result: inventoryLevel } = await this.shopify.updateInventoryLevel({ - inventory_item_id: result.inventory_item_id, - location_id: this.locationId, - available: this.available, - }); - const { result: updatedProductVariant } = await this.shopify.getProductVariant(result.id); - result = updatedProductVariant; - result.inventoryLevel = inventoryLevel; - } - - $.export("$summary", `Created new product variant \`${result.title}\` with id \`${result.id}\``); - return result; - }, -}; diff --git a/components/shopify/actions/create-product-variant/create-product-variant.mjs b/components/shopify/actions/create-product-variant/create-product-variant.mjs index bc83cd1b75922..0b60d1f53032d 100644 --- a/components/shopify/actions/create-product-variant/create-product-variant.mjs +++ b/components/shopify/actions/create-product-variant/create-product-variant.mjs @@ -1,12 +1,13 @@ import shopify from "../../shopify.app.mjs"; -import common from "./common.mjs"; +import utils from "../../common/utils.mjs"; +import { MAX_LIMIT } from "../../common/constants.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { - ...common, key: "shopify-create-product-variant", name: "Create Product Variant", - description: "Create a new product variant. [See the documentation](https://shopify.dev/api/admin-rest/2022-01/resources/product-variant#[post]/admin/api/2022-01/products/{product_id}/variants.json)", - version: "0.0.13", + description: "Create a new product variant. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/productVariantsBulkCreate)", + version: "0.0.14", type: "action", props: { shopify, @@ -16,33 +17,32 @@ export default { "productId", ], }, - option: { + optionIds: { propDefinition: [ shopify, - "option", + "productOptionIds", + (c) => ({ + productId: c.productId, + }), ], }, price: { - propDefinition: [ - shopify, - "price", - ], + type: "string", + label: "Price", description: "The price of the product variant", + optional: true, }, - imageId: { - propDefinition: [ - shopify, - "imageId", - (c) => ({ - productId: c.productId, - }), - ], + image: { + type: "string", + label: "Image", + description: "The URL of an image to be added to the product variant", + optional: true, }, sku: { - propDefinition: [ - shopify, - "sku", - ], + type: "string", + label: "Sku", + description: "A unique identifier for the product variant in the shop", + optional: true, }, locationId: { propDefinition: [ @@ -51,6 +51,98 @@ export default { ], optional: true, }, - ...common.props, + available: { + type: "integer", + label: "Available Quantity", + description: "Sets the available inventory quantity", + optional: true, + }, + barcode: { + type: "string", + label: "Barcode", + description: "The barcode, UPC, or ISBN number for the product", + optional: true, + }, + weight: { + type: "string", + label: "Weight", + description: "The weight of the product variant in the unit system specified with Weight Unit", + optional: true, + }, + weightUnit: { + type: "string", + label: "Weight Unit", + description: "The unit of measurement that applies to the product variant's weight. If you don't specify a value for weight_unit, then the shop's default unit of measurement is applied.", + optional: true, + options: [ + "g", + "kg", + "oz", + "lb", + ], + }, + metafields: { + propDefinition: [ + shopify, + "metafields", + ], + }, + }, + methods: { + async getOptionValues() { + const { product: { options } } = await this.shopify.getProduct({ + id: this.productId, + first: MAX_LIMIT, + }); + const productOptions = {}; + for (const option of options) { + for (const optionValue of option.optionValues) { + productOptions[optionValue.id] = option.id; + } + } + const optionValues = this.optionIds.map((id) => ({ + id, + optionId: productOptions[id], + })); + return optionValues; + }, + }, + async run({ $ }) { + if (this.available && !this.locationId) { + throw new ConfigurationError("Must enter LocationId to set the available quantity"); + } + + const response = await this.shopify.createProductVariants({ + productId: this.productId, + variants: [ + { + optionValues: await this.getOptionValues(), + price: this.price, + mediaSrc: this.image, + barcode: this.barcode, + inventoryItem: (this.sku || this.weightUnit || this.weight) && { + sku: this.sku, + measurement: (this.weightUnit || this.weight) && { + weight: { + unit: this.weightUnit, + value: this.weight, + }, + }, + }, + inventoryQuantities: (this.available || this.locationId) && [ + { + availableQuantity: this.available, + locationId: this.locationId, + }, + ], + metafields: this.metafields && utils.parseJson(this.metafields), + }, + ], + }); + if (response.productVariantsBulkCreate.userErrors.length > 0) { + throw new Error(response.productVariantsBulkCreate.userErrors[0].message); + } + $.export("$summary", `Created new product variant \`${response.productVariantsBulkCreate.productVariants[0].title}\` with ID \`${response.productVariantsBulkCreate.productVariants[0].id}\``); + return response; }, }; diff --git a/components/shopify/actions/create-product/common.mjs b/components/shopify/actions/create-product/common.mjs deleted file mode 100644 index e7fe90b5fb61d..0000000000000 --- a/components/shopify/actions/create-product/common.mjs +++ /dev/null @@ -1,19 +0,0 @@ -export default { - async run({ $ }) { - let data = { - title: this.title, - body_html: this.productDescription, - vendor: this.vendor, - product_type: this.productType, - status: this.status, - images: this.shopify.parseImages(this.images), - variants: this.shopify.parseArrayOfJSONStrings(this.variants), - options: this.shopify.parseArrayOfJSONStrings(this.options), - tags: this.shopify.parseCommaSeparatedStrings(this.tags), - }; - - let response = (await this.shopify.createProduct(data)).result; - $.export("$summary", `Created new product \`${response.title}\` with id \`${response.id}\``); - return response; - }, -}; diff --git a/components/shopify/actions/create-product/create-product.mjs b/components/shopify/actions/create-product/create-product.mjs index 52ee14b761584..ecf52e7f15093 100644 --- a/components/shopify/actions/create-product/create-product.mjs +++ b/components/shopify/actions/create-product/create-product.mjs @@ -1,69 +1,88 @@ import shopify from "../../shopify.app.mjs"; -import common from "./common.mjs"; +import utils from "../../common/utils.mjs"; export default { - ...common, key: "shopify-create-product", name: "Create Product", - description: "Create a new product. [See the documentation](https://shopify.dev/api/admin-rest/2022-01/resources/product#[post]/admin/api/2022-01/products.json)", - version: "0.0.12", + description: "Create a new product. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/productCreate)", + version: "0.0.13", type: "action", props: { shopify, title: { - propDefinition: [ - shopify, - "title", - ], + type: "string", + label: "Title", + description: "Title of the new product", }, productDescription: { - propDefinition: [ - shopify, - "productDescription", - ], + type: "string", + label: "Product Description", + description: "A description of the product. Supports HTML formatting. Example: `Good snowboard!`", + optional: true, }, vendor: { - propDefinition: [ - shopify, - "vendor", - ], + type: "string", + label: "Vendor", + description: "The name of the product's vendor", + optional: true, }, productType: { - propDefinition: [ - shopify, - "productType", - ], + type: "string", + label: "Product Type", + description: "A categorization for the product used for filtering and searching products", + optional: true, }, status: { - propDefinition: [ - shopify, - "status", + type: "string", + label: "Status", + description: "The status of the product. `active`: The product is ready to sell and is available to customers on the online store, sales channels, and apps. By default, existing products are set to active. `archived`: The product is no longer being sold and isn't available to customers on sales channels and apps. `draft`: The product isn't ready to sell and is unavailable to customers on sales channels and apps. By default, duplicated and unarchived products are set to draft", + optional: true, + options: [ + "ACTIVE", + "ARCHIVED", + "DRAFT", ], }, images: { - propDefinition: [ - shopify, - "images", - ], + type: "string[]", + label: "Images", + description: "A list of URLs of images to associate with the new product", + optional: true, }, options: { - propDefinition: [ - shopify, - "options", - ], - }, - variants: { - propDefinition: [ - shopify, - "variants", - ], + type: "string[]", + label: "Options", + description: "The custom product properties. For example, Size, Color, and Material. Each product can have up to 3 options and each option value can be up to 255 characters. Product variants are made of up combinations of option values. Options cannot be created without values. To create new options, a variant with an associated option value also needs to be created. Example: `[{\"name\":\"Color\",\"values\":[{\"name\": \"Blue\"},{\"name\": \"Black\"}]},{\"name\":\"Size\",\"values\":[{\"name\": \"155\"},{\"name\": \"159\"}]}]`", + optional: true, }, tags: { propDefinition: [ shopify, "tags", ], + optional: true, }, - ...common.props, + }, + async run({ $ }) { + const response = await this.shopify.createProduct({ + product: { + title: this.title, + descriptionHtml: this.productDescription, + vendor: this.vendor, + productType: this.productType, + status: this.status, + productOptions: this.options && utils.parseJson(this.options), + tags: this.tags, + }, + media: this.images && this.images.map((image) => ({ + mediaContentType: "IMAGE", + originalSource: image, + })), + }); + if (response.productCreate.userErrors.length > 0) { + throw new Error(response.productCreate.userErrors[0].message); + } + $.export("$summary", `Created new product \`${this.title}\` with id \`${response.productCreate.product.id}\``); + return response; }, }; diff --git a/components/shopify/actions/create-smart-collection/common.mjs b/components/shopify/actions/create-smart-collection/common.mjs deleted file mode 100644 index f64c9ad7cd295..0000000000000 --- a/components/shopify/actions/create-smart-collection/common.mjs +++ /dev/null @@ -1,79 +0,0 @@ -import rules from "../common/rules.mjs"; -import { toSingleLineString } from "../common/common.mjs"; - -export default { - props: { - disjunctive: { - type: "boolean", - label: "Disjunctive", - description: toSingleLineString(` - If \`false\`, the product must match all the rules to be included in the smart collection. - Otherwise, it only needs to match at least one rule - `), - optional: true, - default: false, - }, - rules: { - type: "integer", - label: "Number of rules", - description: "The number of rules to input", - default: 1, - min: 1, - reloadProps: true, - }, - }, - async additionalProps() { - const props = {}; - for (let i = 1; i <= this.rules; i++) { - props[`column_${i}`] = { - type: "string", - label: `Rule ${i} - Column`, - description: toSingleLineString(` - The property of a product being used to populate the smart collection - `), - options: rules.column, - }; - props[`relation_${i}`] = { - type: "string", - label: `Rule ${i} - Relation`, - description: toSingleLineString(` - The relationship between the **column** choice, and the **condition** - `), - options: rules.relation, - }; - props[`condition_${i}`] = { - type: "string", - label: `Rule ${i} - Condition`, - description: toSingleLineString(` - Select products for a smart collection using a **condition**. - Values is either \`string\` or \`number\`, depending on the **relation** value - `), - }; - } - return props; - }, - async run({ $ }) { - const params = { - title: this.title, - disjunctive: this.disjunctive, - rules: [], - }; - - for (let i = 1; i <= this.rules; i++) { - const column = `column_${i}`; - const relation = `relation_${i}`; - const condition = `condition_${i}`; - - params.rules.push({ - column: this[column], - relation: this[relation], - condition: this[condition], - }); - } - - const response = await this.shopify.createSmartCollection(params); - const { result } = response; - $.export("$summary", `Created new smart collection \`${result.title}\` with id \`${result.id}\``); - return result; - }, -}; diff --git a/components/shopify/actions/create-smart-collection/create-smart-collection.mjs b/components/shopify/actions/create-smart-collection/create-smart-collection.mjs index 426fe039520d4..ea0b675e2cfa5 100644 --- a/components/shopify/actions/create-smart-collection/create-smart-collection.mjs +++ b/components/shopify/actions/create-smart-collection/create-smart-collection.mjs @@ -1,27 +1,83 @@ import shopify from "../../shopify.app.mjs"; -import { toSingleLineString } from "../common/common.mjs"; -import common from "./common.mjs"; +import { + COLLECTION_RULE_COLUMNS, COLLECTION_RULE_RELATIONS, +} from "../../common/constants.mjs"; export default { - ...common, key: "shopify-create-smart-collection", name: "Create Smart Collection", - description: toSingleLineString(` - Creates a smart collection. - You can fill in any number of rules by selecting more than one option in each prop. - [See documentation](https://shopify.dev/api/admin-rest/2021-10/resources/smartcollection#post-smart-collections) - `), - version: "0.0.12", + description: "Creates a smart collection. You can fill in any number of rules by selecting more than one option in each prop.[See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/collectionCreate)", + version: "0.0.13", type: "action", props: { shopify, title: { - propDefinition: [ - shopify, - "title", - ], + type: "string", + label: "Title", description: "Title of the smart collection", }, - ...common.props, + disjunctive: { + type: "boolean", + label: "Disjunctive", + description: "If `false`, the product must match all the rules to be included in the smart collection. Otherwise, it only needs to match at least one rule", + optional: true, + default: false, + }, + rules: { + type: "integer", + label: "Number of rules", + description: "The number of rules to input", + default: 1, + min: 1, + reloadProps: true, + }, + }, + async additionalProps() { + const props = {}; + for (let i = 1; i <= this.rules; i++) { + props[`column_${i}`] = { + type: "string", + label: `Rule ${i} - Column`, + description: "The property of a product being used to populate the smart collection", + options: COLLECTION_RULE_COLUMNS, + }; + props[`relation_${i}`] = { + type: "string", + label: `Rule ${i} - Relation`, + description: "The relationship between the **column** choice, and the **condition**", + options: COLLECTION_RULE_RELATIONS, + }; + props[`condition_${i}`] = { + type: "string", + label: `Rule ${i} - Condition`, + description: "Select products for a smart collection using a **condition**. Values is either `string` or `number`, depending on the **relation** value", + }; + } + return props; + }, + async run({ $ }) { + const rules = []; + for (let i = 1; i <= this.rules; i++) { + rules.push({ + column: this[`column_${i}`], + relation: this[`relation_${i}`], + condition: this[`condition_${i}`], + }); + } + + const response = await this.shopify.createCollection({ + input: { + title: this.title, + ruleSet: { + appliedDisjunctively: this.disjunctive, + rules, + }, + }, + }); + if (response.collectionCreate.userErrors.length > 0) { + throw new Error(response.collectionCreate.userErrors[0].message); + } + $.export("$summary", `Created new smart collection \`${this.title}\` with ID \`${response.collectionCreate.collection.id}\``); + return response; }, }; diff --git a/components/shopify/actions/delete-article/common.mjs b/components/shopify/actions/delete-article/common.mjs deleted file mode 100644 index 5a7da23a1773d..0000000000000 --- a/components/shopify/actions/delete-article/common.mjs +++ /dev/null @@ -1,30 +0,0 @@ -export default { - methods: { - deleteBlogArticle({ - blogId, articleId, ...args - } = {}) { - return this.app.delete({ - path: `/blogs/${blogId}/articles/${articleId}`, - ...args, - }); - }, - }, - async run({ $: step }) { - const { - blogId, - articleId, - } = this; - - await this.deleteBlogArticle({ - step, - blogId, - articleId, - }); - - step.export("$summary", `Deleted article with ID ${articleId} from blog with ID ${blogId}.`); - - return { - success: true, - }; - }, -}; diff --git a/components/shopify/actions/delete-article/delete-article.mjs b/components/shopify/actions/delete-article/delete-article.mjs index 23ef4f6171d31..d3dfcff1286a4 100644 --- a/components/shopify/actions/delete-article/delete-article.mjs +++ b/components/shopify/actions/delete-article/delete-article.mjs @@ -1,29 +1,34 @@ -import app from "../../common/rest-admin.mjs"; -import common from "./common.mjs"; +import shopify from "../../shopify.app.mjs"; export default { - ...common, key: "shopify-delete-article", name: "Delete Article", - description: "Delete an existing blog article. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/article#delete-blogs-blog-id-articles-article-id)", - version: "0.0.6", + description: "Delete an existing blog article. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/articleDelete)", + version: "0.0.7", type: "action", props: { - app, + shopify, blogId: { propDefinition: [ - app, + shopify, "blogId", ], }, articleId: { propDefinition: [ - app, + shopify, "articleId", - ({ blogId }) => ({ - blogId, + (c) => ({ + blogId: c.blogId, }), ], }, }, + async run({ $ }) { + const response = await this.shopify.deleteArticle({ + id: this.articleId, + }); + $.export("$summary", `Deleted article with ID ${this.articleId} from blog with ID ${this.blogId}`); + return response; + }, }; diff --git a/components/shopify/actions/delete-blog/common.mjs b/components/shopify/actions/delete-blog/common.mjs deleted file mode 100644 index 1b29e65203854..0000000000000 --- a/components/shopify/actions/delete-blog/common.mjs +++ /dev/null @@ -1,26 +0,0 @@ -export default { - methods: { - deleteBlog({ - blogId, ...args - } = {}) { - return this.app.delete({ - path: `/blogs/${blogId}`, - ...args, - }); - }, - }, - async run({ $: step }) { - const { blogId } = this; - - await this.deleteBlog({ - step, - blogId, - }); - - step.export("$summary", `Deleted blog with ID ${blogId}`); - - return { - success: true, - }; - }, -}; diff --git a/components/shopify/actions/delete-blog/delete-blog.mjs b/components/shopify/actions/delete-blog/delete-blog.mjs index 0795f818d8c7d..d8eafbc5355e3 100644 --- a/components/shopify/actions/delete-blog/delete-blog.mjs +++ b/components/shopify/actions/delete-blog/delete-blog.mjs @@ -1,20 +1,25 @@ -import app from "../../common/rest-admin.mjs"; -import common from "./common.mjs"; +import shopify from "../../shopify.app.mjs"; export default { - ...common, key: "shopify-delete-blog", name: "Delete Blog", - description: "Delete an existing blog. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/blog#delete-blogs-blog-id)", - version: "0.0.6", + description: "Delete an existing blog. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/blogDelete)", + version: "0.0.7", type: "action", props: { - app, + shopify, blogId: { propDefinition: [ - app, + shopify, "blogId", ], }, }, + async run({ $ }) { + const response = await this.shopify.deleteBlog({ + id: this.blogId, + }); + $.export("$summary", `Deleted blog with ID ${this.blogId}`); + return response; + }, }; diff --git a/components/shopify/actions/delete-metafield/common.mjs b/components/shopify/actions/delete-metafield/common.mjs deleted file mode 100644 index ff5ea8e726fa5..0000000000000 --- a/components/shopify/actions/delete-metafield/common.mjs +++ /dev/null @@ -1,35 +0,0 @@ -export default { - async additionalProps() { - const props = await this.getOwnerIdProp(this.ownerResource); - - if (props.ownerId) { - props.ownerId = { - ...props.ownerId, - reloadProps: true, - }; - } - - if (this.ownerResource && this.ownerId) { - props.metafieldId = { - type: "string", - label: "Metafield ID", - description: "The metafield to update", - options: async () => { - return this.shopify.getMetafieldOptions(this.ownerResource, this.ownerId); - }, - }; - } - - return props; - }, - async run({ $ }) { - const params = { - metafield_id: this.metafieldId, - owner_id: `${this.ownerId}`, - owner_resource: this.ownerResource, - }; - await this.shopify.resourceAction("metafield", "delete", params, this.metafieldId); - $.export("$summary", `Deleted metafield with ID ${this.metafieldId}`); - // nothing to return - }, -}; diff --git a/components/shopify/actions/delete-metafield/delete-metafield.mjs b/components/shopify/actions/delete-metafield/delete-metafield.mjs index 5deb7069659b3..6e80e7d1a1f67 100644 --- a/components/shopify/actions/delete-metafield/delete-metafield.mjs +++ b/components/shopify/actions/delete-metafield/delete-metafield.mjs @@ -1,19 +1,56 @@ -import metafieldActions from "../common/metafield-actions.mjs"; -import common from "./common.mjs"; +import common from "../common/metafield-actions.mjs"; export default { ...common, key: "shopify-delete-metafield", name: "Delete Metafield", - description: "Deletes a metafield belonging to a resource. [See the documentation](https://shopify.dev/docs/api/admin-rest/2023-01/resources/metafield#delete-blogs-blog-id-metafields-metafield-id)", - version: "0.0.8", + description: "Deletes a metafield belonging to a resource. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/metafieldsDelete)", + version: "0.0.9", type: "action", - props: { - ...metafieldActions.props, - ...common.props, + async additionalProps() { + const props = await this.getOwnerIdProp(this.ownerResource); + + if (props.ownerId) { + props.ownerId = { + ...props.ownerId, + reloadProps: true, + }; + } + + if (this.ownerResource && this.ownerId) { + props.metafieldId = { + type: "string", + label: "Metafield ID", + description: "The metafield to update", + options: async () => { + const metafields = await this.listMetafields(this.ownerResource, this.ownerId); + return metafields?.map(({ + id: value, key: label, + }) => ({ + value, + label, + })) || []; + }, + }; + } + + return props; }, - methods: { - ...metafieldActions.methods, - ...common.methods, + async run({ $ }) { + const metafields = await this.listMetafields(this.ownerResource, this.ownerId); + const metafield = metafields.find(({ id }) => id === this.metafieldId); + + const response = await this.shopify.deleteMetafield({ + metafields: { + key: metafield.key, + ownerId: this.ownerId, + namespace: metafield.namespace, + }, + }); + if (response.metafieldsDelete.userErrors.length > 0) { + throw new Error(response.metafieldsDelete.userErrors[0].message); + } + $.export("$summary", `Deleted metafield with ID ${this.metafieldId}`); + return response; }, }; diff --git a/components/shopify/actions/delete-page/common.mjs b/components/shopify/actions/delete-page/common.mjs deleted file mode 100644 index 55f38b9bc329e..0000000000000 --- a/components/shopify/actions/delete-page/common.mjs +++ /dev/null @@ -1,26 +0,0 @@ -export default { - methods: { - deletePage({ - pageId, ...args - } = {}) { - return this.app.delete({ - path: `/pages/${pageId}`, - ...args, - }); - }, - }, - async run({ $: step }) { - const { pageId } = this; - - await this.deletePage({ - step, - pageId, - }); - - step.export("$summary", `Deleted page with ID ${pageId}`); - - return { - success: true, - }; - }, -}; diff --git a/components/shopify/actions/delete-page/delete-page.mjs b/components/shopify/actions/delete-page/delete-page.mjs index 29931ac7c6859..115e11d6d7179 100644 --- a/components/shopify/actions/delete-page/delete-page.mjs +++ b/components/shopify/actions/delete-page/delete-page.mjs @@ -1,20 +1,25 @@ -import app from "../../common/rest-admin.mjs"; -import common from "./common.mjs"; +import shopify from "../../shopify.app.mjs"; export default { - ...common, key: "shopify-delete-page", name: "Delete Page", - description: "Delete an existing page. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/page#delete-pages-page-id)", - version: "0.0.6", + description: "Delete an existing page. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/pageDelete)", + version: "0.0.7", type: "action", props: { - app, + shopify, pageId: { propDefinition: [ - app, + shopify, "pageId", ], }, }, + async run({ $ }) { + const response = await this.shopify.deletePage({ + id: this.pageId, + }); + $.export("$summary", `Deleted page with ID ${this.pageId}`); + return response; + }, }; diff --git a/components/shopify/actions/get-articles/common.mjs b/components/shopify/actions/get-articles/common.mjs deleted file mode 100644 index 4ea09d07fdf34..0000000000000 --- a/components/shopify/actions/get-articles/common.mjs +++ /dev/null @@ -1,20 +0,0 @@ -import utils from "../../common/utils.mjs"; - -export default { - async run({ $: step }) { - const stream = this.app.getResourcesStream({ - resourceFn: this.app.listBlogArticles, - resourceFnArgs: { - step, - blogId: this.blogId, - }, - resourceName: "articles", - }); - - const articles = await utils.streamIterator(stream); - - step.export("$summary", `Successfully retrieved ${articles.length} article(s).`); - - return articles; - }, -}; diff --git a/components/shopify/actions/get-articles/get-articles.mjs b/components/shopify/actions/get-articles/get-articles.mjs index 8672f34edf977..f79ffe9ee7074 100644 --- a/components/shopify/actions/get-articles/get-articles.mjs +++ b/components/shopify/actions/get-articles/get-articles.mjs @@ -1,20 +1,48 @@ -import app from "../../common/rest-admin.mjs"; -import common from "./common.mjs"; +import shopify from "../../shopify.app.mjs"; export default { - ...common, key: "shopify-get-articles", name: "Get Articles", - description: "Retrieve a list of all articles from a blog. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/article#get-blogs-blog-id-articles)", - version: "0.0.6", + description: "Retrieve a list of all articles from a blog. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/queries/articles)", + version: "0.0.7", type: "action", props: { - app, + shopify, blogId: { propDefinition: [ - app, + shopify, "blogId", ], }, + maxResults: { + propDefinition: [ + shopify, + "maxResults", + ], + }, + reverse: { + propDefinition: [ + shopify, + "reverse", + ], + }, + }, + async run({ $ }) { + const articles = await this.shopify.getPaginated({ + resourceFn: this.shopify.listBlogArticles, + resourceKeys: [ + "blog", + "articles", + ], + variables: { + id: this.blogId, + reverse: this.reverse, + }, + max: this.maxResults, + }); + $.export("$summary", `Successfully retrieved ${articles.length} article${articles.length === 1 + ? "" + : "s"}`); + return articles; }, }; diff --git a/components/shopify/actions/get-metafields/common.mjs b/components/shopify/actions/get-metafields/common.mjs deleted file mode 100644 index 36109e17462fb..0000000000000 --- a/components/shopify/actions/get-metafields/common.mjs +++ /dev/null @@ -1,46 +0,0 @@ -export default { - props: { - namespace: { - type: "string[]", - label: "Namespace", - description: "Filter results by namespace", - optional: true, - }, - key: { - type: "string[]", - label: "Key", - description: "Filter results by key", - optional: true, - }, - }, - async additionalProps() { - return this.getOwnerIdProp(this.ownerResource); - }, - async run({ $ }) { - const { - ownerResource, - ownerId, - namespace, - key, - } = this; - - const params = { - metafield: { - owner_resource: ownerResource, - owner_id: ownerId, - }, - }; - let response = await this.shopify.listMetafields(params); - - if (namespace?.length > 0) { - response = response.filter((field) => namespace.includes(field.namespace)); - } - - if (key?.length > 0) { - response = response.filter((field) => key.includes(field.key)); - } - - $.export("$summary", `Found ${response.length} metafield(s) for object with ID ${ownerId}`); - return response; - }, -}; diff --git a/components/shopify/actions/get-metafields/get-metafields.mjs b/components/shopify/actions/get-metafields/get-metafields.mjs index b0953bd5fec66..adf50fe23a217 100644 --- a/components/shopify/actions/get-metafields/get-metafields.mjs +++ b/components/shopify/actions/get-metafields/get-metafields.mjs @@ -1,19 +1,42 @@ -import metafieldActions from "../common/metafield-actions.mjs"; -import common from "./common.mjs"; +import common from "../common/metafield-actions.mjs"; export default { ...common, key: "shopify-get-metafields", name: "Get Metafields", - description: "Retrieves a list of metafields that belong to a resource. [See the docs](https://shopify.dev/api/admin-rest/2023-01/resources/metafield#get-metafields?metafield[owner-id]=382285388&metafield[owner-resource]=blog)", - version: "0.0.11", + description: "Retrieves a list of metafields that belong to a resource. [See the documentation](https://shopify.dev/docs/api/admin-graphql/unstable/queries/metafields)", + version: "0.0.12", type: "action", props: { - ...metafieldActions.props, ...common.props, + namespace: { + type: "string[]", + label: "Namespace", + description: "Filter results by namespace", + optional: true, + }, + key: { + type: "string[]", + label: "Key", + description: "Filter results by key", + optional: true, + }, }, - methods: { - ...metafieldActions.methods, - ...common.methods, + async additionalProps() { + return this.getOwnerIdProp(this.ownerResource); + }, + async run({ $ }) { + let response = await this.listMetafields(this.ownerResource, this.ownerId); + + if (this.namespace?.length > 0) { + response = response.filter((field) => this.namespace.includes(field.namespace)); + } + + if (this.key?.length > 0) { + response = response.filter((field) => this.key.includes(field.key)); + } + + $.export("$summary", `Found ${response.length} metafield(s) for object with ID ${this.ownerId}`); + return response; }, }; diff --git a/components/shopify/actions/get-metaobjects/common.mjs b/components/shopify/actions/get-metaobjects/common.mjs deleted file mode 100644 index f8424936bcd34..0000000000000 --- a/components/shopify/actions/get-metaobjects/common.mjs +++ /dev/null @@ -1,15 +0,0 @@ -export default { - async run({ $ }) { - const response = await this.listMetaobjects({ - type: this.type, - $, - }); - - const numObjects = (response.data.metaobjects.nodes).length; - $.export("$summary", `Successfully retrieved ${numObjects} metaobject${numObjects === 1 - ? "" - : "s"}`); - - return response; - }, -}; diff --git a/components/shopify/actions/get-metaobjects/get-metaobjects.mjs b/components/shopify/actions/get-metaobjects/get-metaobjects.mjs index 3887872bbf5c8..d1a62938d47fd 100644 --- a/components/shopify/actions/get-metaobjects/get-metaobjects.mjs +++ b/components/shopify/actions/get-metaobjects/get-metaobjects.mjs @@ -1,26 +1,34 @@ import shopify from "../../shopify.app.mjs"; -import metaobjects from "../common/metaobjects.mjs"; -import common from "./common.mjs"; +import common from "../common/metaobjects.mjs"; +import { MAX_LIMIT } from "../../common/constants.mjs"; export default { - ...metaobjects, ...common, key: "shopify-get-metaobjects", name: "Get Metaobjects", - description: "Retrieves a list of metaobjects. [See the documentation](https://shopify.dev/docs/api/admin-graphql/2023-04/queries/metaobjects)", - version: "0.0.4", + description: "Retrieves a list of metaobjects. [See the documentation](https://shopify.dev/docs/api/admin-graphql/unstable/queries/metaobjects)", + version: "0.0.5", type: "action", props: { shopify, type: { - type: "string", - label: "Type", - description: "The Metaobject Type", - async options() { - const { data: { metaobjectDefinitions: { nodes } } } - = await this.listMetaobjectDefinitions(); - return nodes?.map(({ type }) => type) || []; - }, + propDefinition: [ + shopify, + "metaobjectType", + ], }, }, + async run({ $ }) { + const response = await this.shopify.listMetaobjects({ + type: this.type, + first: MAX_LIMIT, + }); + + const numObjects = (response.metaobjects.nodes).length; + $.export("$summary", `Successfully retrieved ${numObjects} metaobject${numObjects === 1 + ? "" + : "s"}`); + + return response; + }, }; diff --git a/components/shopify/actions/get-pages/common.mjs b/components/shopify/actions/get-pages/common.mjs deleted file mode 100644 index ff2f74642f3e4..0000000000000 --- a/components/shopify/actions/get-pages/common.mjs +++ /dev/null @@ -1,19 +0,0 @@ -import utils from "../../common/utils.mjs"; - -export default { - async run({ $: step }) { - const stream = this.app.getResourcesStream({ - resourceFn: this.app.listPages, - resourceFnArgs: { - step, - }, - resourceName: "pages", - }); - - const pages = await utils.streamIterator(stream); - - step.export("$summary", `Successfully retrieved ${pages.length} page(s).`); - - return pages; - }, -}; diff --git a/components/shopify/actions/get-pages/get-pages.mjs b/components/shopify/actions/get-pages/get-pages.mjs index d749198a2458a..399aa1e572d5c 100644 --- a/components/shopify/actions/get-pages/get-pages.mjs +++ b/components/shopify/actions/get-pages/get-pages.mjs @@ -1,14 +1,40 @@ -import app from "../../common/rest-admin.mjs"; -import common from "./common.mjs"; +import shopify from "../../shopify.app.mjs"; export default { - ...common, key: "shopify-get-pages", name: "Get Pages", - description: "Retrieve a list of all pages. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/page#get-pages)", - version: "0.0.6", + description: "Retrieve a list of all pages. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/queries/pages)", + version: "0.0.7", type: "action", props: { - app, + shopify, + maxResults: { + propDefinition: [ + shopify, + "maxResults", + ], + }, + reverse: { + propDefinition: [ + shopify, + "reverse", + ], + }, + }, + async run({ $ }) { + const pages = await this.shopify.getPaginated({ + resourceFn: this.shopify.listPages, + resourceKeys: [ + "pages", + ], + variables: { + reverse: this.reverse, + }, + max: this.maxResults, + }); + $.export("$summary", `Successfully retrieved ${pages.length} page${pages.length === 1 + ? "" + : "s"}`); + return pages; }, }; diff --git a/components/shopify/actions/search-custom-collection-by-name/common.mjs b/components/shopify/actions/search-custom-collection-by-name/common.mjs deleted file mode 100644 index 00b7f415a1290..0000000000000 --- a/components/shopify/actions/search-custom-collection-by-name/common.mjs +++ /dev/null @@ -1,33 +0,0 @@ -export default { - props: { - exactMatch: { - type: "boolean", - label: "Exact Match", - description: "The custom collection title search should be an exact match", - optional: true, - default: false, - }, - }, - async run({ $ }) { - const { - title, - exactMatch, - } = this; - - const params = {}; - if (exactMatch) { - params.title = title; - } - - let collections = await this.shopify.getObjects("customCollection", params); - - if (!exactMatch) { - const lowerCaseTitle = title.toLowerCase(); - collections = collections.filter(({ title }) => - title.toLowerCase().includes(lowerCaseTitle)); - } - - $.export("$summary", `Found ${collections.length} collection(s) matching search criteria.`); - return collections; - }, -}; diff --git a/components/shopify/actions/search-custom-collection-by-name/search-custom-collection-by-name.mjs b/components/shopify/actions/search-custom-collection-by-name/search-custom-collection-by-name.mjs index 9c1d318506c0f..4d74d8a30705f 100644 --- a/components/shopify/actions/search-custom-collection-by-name/search-custom-collection-by-name.mjs +++ b/components/shopify/actions/search-custom-collection-by-name/search-custom-collection-by-name.mjs @@ -1,22 +1,73 @@ import shopify from "../../shopify.app.mjs"; -import common from "./common.mjs"; +import { COLLECTION_SORT_KEY } from "../../common/constants.mjs"; export default { - ...common, key: "shopify-search-custom-collection-by-name", name: "Search Custom Collection by Name", - description: "Search for a custom collection by name/title. [See the documentation](https://shopify.dev/docs/api/admin-rest/2023-01/resources/customcollection#get-custom-collections)", - version: "0.0.5", + description: "Search for a custom collection by name/title. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/queries/collections)", + version: "0.0.6", type: "action", props: { shopify, title: { + type: "string", + label: "Title", + description: "The name of the custom collection", + optional: true, + }, + exactMatch: { + type: "boolean", + label: "Exact Match", + description: "The collection title search should be an exact match", + optional: true, + default: false, + }, + maxResults: { propDefinition: [ shopify, - "title", + "maxResults", ], - description: "The name of the custom collection", }, - ...common.props, + sortKey: { + propDefinition: [ + shopify, + "sortKey", + ], + options: COLLECTION_SORT_KEY, + }, + reverse: { + propDefinition: [ + shopify, + "reverse", + ], + }, + }, + async run({ $ }) { + let collections = await this.shopify.getPaginated({ + resourceFn: this.shopify.listCollections, + resourceKeys: [ + "collections", + ], + variables: { + sortKey: this.sortKey, + reverse: this.reverse, + query: this.title && this.exactMatch + ? `title:"${this.title}"` + : undefined, + }, + }); + if (this.title && !this.exactMatch) { + collections = collections.filter((collection) => + collection.title.toLowerCase().includes(this.title.toLowerCase())); + } + + if (collections.length > this.maxResults) { + collections.length = this.maxResults; + } + + $.export("$summary", `Successfully retrieved ${collections.length} collection${collections.length === 1 + ? "" + : "s"}`); + return collections; }, }; diff --git a/components/shopify/actions/search-customers/common.mjs b/components/shopify/actions/search-customers/common.mjs deleted file mode 100644 index f50a114140b7d..0000000000000 --- a/components/shopify/actions/search-customers/common.mjs +++ /dev/null @@ -1,15 +0,0 @@ -export default { - async run({ $ }) { - let params = { - query: this.query, - }; - - let response = await this.shopify.getPaginatedResults( - this.shopify.searchCustomers, - params, - this.max, - ); - $.export("$summary", `Found ${response.length} customer(s)`); - return response; - }, -}; diff --git a/components/shopify/actions/search-product-variant/common.mjs b/components/shopify/actions/search-product-variant/common.mjs deleted file mode 100644 index 28a890c5e97be..0000000000000 --- a/components/shopify/actions/search-product-variant/common.mjs +++ /dev/null @@ -1,63 +0,0 @@ -export default { - props: { - createIfNotFound: { - type: "boolean", - label: "Create If Not Found", - description: "Creates the product variant with **Title** and the fields below if not found", - optional: true, - default: false, - reloadProps: true, - }, - }, - async additionalProps() { - let props = {}; - if (this.createIfNotFound) { - props.price = { - type: "string", - label: "Price", - description: "The price of the product variant", - optional: true, - }; - props.imageId = { - type: "string", - label: "Image ID", - description: "The unique numeric identifier for a product's image. The image must be associated to the same product as the variant", - optional: true, - }; - } - return props; - }, - async run({ $ }) { - if (!(this.productVariantId || this.title)) { - throw new Error("Required field missing: Fill in `Product Variant ID` or `Title`"); - } - - try { - let response; - if (this.productVariantId) { - response = await this.shopify.getProductVariant(this.productVariantId); - } else { - response = await this.shopify.getProductVariantByTitle(this.productId, this.title); - } - - $.export("$summary", `Found product variant \`${response.result.title}\` with id \`${response.result.id}\``); - return response; - } catch (err) { - if (!this.createIfNotFound) { - throw err; - } - - let productVariant = { - option1: this.title, - price: this.price, - image_id: this.imageId, - }; - let response = (await this.shopify.createProductVariant( - this.productId, - productVariant, - )).result; - $.export("$summary", `Created new product variant \`${response.title}\` with id \`${response.id}\``); - return response; - } - }, -}; diff --git a/components/shopify/actions/search-product-variant/search-product-variant.mjs b/components/shopify/actions/search-product-variant/search-product-variant.mjs index 7ef213e513c50..4d18a08d7cdd8 100644 --- a/components/shopify/actions/search-product-variant/search-product-variant.mjs +++ b/components/shopify/actions/search-product-variant/search-product-variant.mjs @@ -1,12 +1,13 @@ import shopify from "../../shopify.app.mjs"; -import common from "./common.mjs"; +import utils from "../../common/utils.mjs"; +import { MAX_LIMIT } from "../../common/constants.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { - ...common, key: "shopify-search-product-variant", name: "Search for Product Variant", - description: "Search for product variants or create one if not found. [See the documentation](https://shopify.dev/api/admin-rest/2022-01/resources/product-variant#top)", - version: "0.0.12", + description: "Search for product variants or create one if not found. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/queries/productVariants)", + version: "0.0.13", type: "action", props: { shopify, @@ -20,19 +21,123 @@ export default { propDefinition: [ shopify, "productVariantId", - (c) => c, + (c) => ({ + productId: c.productId, + }), ], description: "ID of the product variant. Takes precedence over **Title**", optional: true, }, title: { + type: "string", + label: "Title", + description: "The name of the product variant", + optional: true, + }, + createIfNotFound: { + type: "boolean", + label: "Create If Not Found", + description: "Creates the product variant with **Title** and the fields below if not found", + optional: true, + default: false, + reloadProps: true, + }, + optionIds: { propDefinition: [ shopify, - "title", + "productOptionIds", + (c) => ({ + productId: c.productId, + }), ], - description: "The name of the product variant", - optional: true, + hidden: true, }, - ...common.props, + }, + async additionalProps(existingProps) { + let props = {}; + if (this.createIfNotFound) { + existingProps.optionIds.hidden = false; + props.price = { + type: "string", + label: "Price", + description: "The price of the product variant", + optional: true, + }; + props.image = { + type: "string", + label: "Image URL", + description: "The URL of an image to attach to the product variant", + optional: true, + }; + } + return props; + }, + methods: { + async getOptionValues() { + const { product: { options } } = await this.shopify.getProduct({ + id: this.productId, + first: MAX_LIMIT, + }); + const productOptions = {}; + for (const option of options) { + for (const optionValue of option.optionValues) { + productOptions[optionValue.id] = option.id; + } + } + const optionValues = this.optionIds.map((id) => ({ + id, + optionId: productOptions[id], + })); + return optionValues; + }, + }, + async run({ $ }) { + if (!(this.productVariantId || this.title)) { + throw new ConfigurationError("Required field missing: Fill in `Product Variant ID` or `Title`"); + } + + try { + let response; + if (this.productVariantId) { + response = await this.shopify.getProductVariant({ + id: this.productVariantId, + first: MAX_LIMIT, + }); + } else { + response = await this.shopify.listProductVariants({ + query: `product_id:${utils.getIdFromGid(this.productId)} AND title:${JSON.stringify(this.title)}`, + first: MAX_LIMIT, + }); + } + + const title = response.productVariants + ? response.productVariants.nodes[0].title + : response.productVariant.title; + const id = response.productVariants + ? response.productVariants.nodes[0].id + : response.productVariant.id; + $.export("$summary", `Found product variant \`${title}\` with ID \`${id}\``); + return response; + } catch (err) { + if (!this.createIfNotFound) { + throw err; + } + + let response = await this.shopify.createProductVariants({ + productId: this.productId, + variants: [ + { + optionValues: this.optionIds && await this.getOptionValues(), + price: this.price, + mediaSrc: this.image, + }, + ], + }); + if (response.productVariantsBulkCreate.userErrors.length > 0) { + throw new Error(response.productVariantsBulkCreate.userErrors[0].message); + } + $.export("$summary", `Created new product variant \`${response.productVariantsBulkCreate.productVariants.title}\` with ID \`${response.productVariantsBulkCreate.productVariants.id}\``); + return response; + } }, }; diff --git a/components/shopify/actions/search-products/common.mjs b/components/shopify/actions/search-products/common.mjs deleted file mode 100644 index 726176925829a..0000000000000 --- a/components/shopify/actions/search-products/common.mjs +++ /dev/null @@ -1,52 +0,0 @@ -export default { - props: { - exactMatch: { - type: "boolean", - label: "Exact Match", - description: "The product title search should be an exact match", - optional: true, - default: false, - }, - }, - async run({ $ }) { - const { - title, - exactMatch, - productIds, - collectionId, - productType, - vendor, - } = this; - - let productIdString = productIds; - if (Array.isArray(productIds)) { - productIdString = productIds.join(); - } - else if (typeof productIds === "string") { - if (productIds.startsWith("[") && productIds.endsWith("]")) { - productIdString = productIds.slice(1, -1); - } - productIdString = productIdString.replace(/\s/g, ""); - } - - const params = { - ids: productIdString, - collection_id: collectionId, - product_type: productType, - vendor, - }; - if (title && exactMatch) { - params.title = title; - } - - let products = await this.shopify.getProducts(false, false, params); - - if (title && !exactMatch) { - products = products.filter((product) => - product.title.toLowerCase().includes(title.toLowerCase())); - } - - $.export("$summary", `Found ${products.length} product(s) matching search criteria.`); - return products; - }, -}; diff --git a/components/shopify/actions/search-products/search-products.mjs b/components/shopify/actions/search-products/search-products.mjs index c36e4f67a21ab..62beb302157ae 100644 --- a/components/shopify/actions/search-products/search-products.mjs +++ b/components/shopify/actions/search-products/search-products.mjs @@ -1,22 +1,28 @@ import shopify from "../../shopify.app.mjs"; -import common from "./common.mjs"; +import { PRODUCT_SORT_KEY } from "../../common/constants.mjs"; +import utils from "../../common/utils.mjs"; export default { - ...common, key: "shopify-search-products", name: "Search for Products", - description: "Search for products. [See the documentation](https://shopify.dev/api/admin-rest/2022-01/resources/product#[get]/admin/api/2022-01/products.json)", - version: "0.0.12", + description: "Search for products. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/queries/products)", + version: "0.0.13", type: "action", props: { shopify, title: { - propDefinition: [ - shopify, - "title", - ], + type: "string", + label: "Title", + description: "The name of the product", optional: true, }, + exactMatch: { + type: "boolean", + label: "Exact Match", + description: "The product title search should be an exact match", + optional: true, + default: false, + }, productIds: { propDefinition: [ shopify, @@ -30,19 +36,87 @@ export default { shopify, "collectionId", ], + optional: true, }, productType: { + type: "string", + label: "Product Type", + description: "A categorization for the product used for filtering and searching products", + optional: true, + }, + vendor: { + type: "string", + label: "Vendor", + description: "The name of the product's vendor", + optional: true, + }, + maxResults: { propDefinition: [ shopify, - "productType", + "maxResults", ], }, - vendor: { + sortKey: { propDefinition: [ shopify, - "vendor", + "sortKey", ], + options: PRODUCT_SORT_KEY, }, - ...common.props, + reverse: { + propDefinition: [ + shopify, + "reverse", + ], + }, + }, + async run({ $ }) { + const queryArray = []; + if (this.title && this.exactMatch) { + queryArray.push(`title:"${this.title}"`); + } + if (this.productIds?.length) { + const idArray = this.productIds.map((id) => `id:${utils.getIdFromGid(id)}`); + queryArray.push(`(${idArray.join(" OR ")})`); + } + if (this.collectionId) { + queryArray.push(`collection_id:${utils.getIdFromGid(this.collectionId)}`); + } + if (this.productType) { + queryArray.push(`product_type:${this.productType}`); + } + if (this.vendor) { + queryArray.push(`vendor:${this.vendor}`); + } + + const query = queryArray.length + ? queryArray.join(" AND ") + : undefined; + + let products = await this.shopify.getPaginated({ + resourceFn: this.shopify.listProducts, + resourceKeys: [ + "products", + ], + variables: { + query, + sortKey: this.sortKey, + reverse: this.reverse, + }, + }); + + if (this.title && !this.exactMatch) { + products = products.filter((product) => + product.title.toLowerCase().includes(this.title.toLowerCase())); + } + + if (products.length > this.maxResults) { + products.length = this.maxResults; + } + + $.export("$summary", `Found ${products.length} product${products.length === 1 + ? "" + : "s"} matching search criteria`); + return products; }, }; diff --git a/components/shopify/actions/update-article/common.mjs b/components/shopify/actions/update-article/common.mjs deleted file mode 100644 index 412573b57fae9..0000000000000 --- a/components/shopify/actions/update-article/common.mjs +++ /dev/null @@ -1,36 +0,0 @@ -export default { - methods: { - updateBlogArticle({ - blogId, articleId, ...args - } = {}) { - return this.app.put({ - path: `/blogs/${blogId}/articles/${articleId}`, - ...args, - }); - }, - }, - async run({ $: step }) { - const { - blogId, - articleId, - title, - bodyHtml, - } = this; - - const response = await this.updateBlogArticle({ - step, - blogId, - articleId, - data: { - article: { - title, - body_html: bodyHtml, - }, - }, - }); - - step.export("$summary", `Updated article with ID ${response.article.id}.`); - - return response; - }, -}; diff --git a/components/shopify/actions/update-article/update-article.mjs b/components/shopify/actions/update-article/update-article.mjs index efb3cea6889d1..f8d60baeaed58 100644 --- a/components/shopify/actions/update-article/update-article.mjs +++ b/components/shopify/actions/update-article/update-article.mjs @@ -1,24 +1,22 @@ -import app from "../../common/rest-admin.mjs"; -import common from "./common.mjs"; +import shopify from "../../shopify.app.mjs"; export default { - ...common, key: "shopify-update-article", name: "Update Article", - description: "Update a blog article. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/article#put-blogs-blog-id-articles-article-id)", - version: "0.0.6", + description: "Update a blog article. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/articleUpdate)", + version: "0.0.7", type: "action", props: { - app, + shopify, blogId: { propDefinition: [ - app, + shopify, "blogId", ], }, articleId: { propDefinition: [ - app, + shopify, "articleId", ({ blogId }) => ({ blogId, @@ -26,18 +24,64 @@ export default { ], }, title: { - description: "The title of the article.", - propDefinition: [ - app, - "title", - ], + type: "string", + label: "Title", + description: "The title of the article", + optional: true, }, bodyHtml: { + type: "string", + label: "Body", description: "The text content of the article, complete with HTML markup.", + optional: true, + }, + author: { + type: "string", + label: "Author", + description: "The name of the author of the article", + optional: true, + }, + summary: { + type: "string", + label: "Summary", + description: "A summary of the article, which can include HTML markup. The summary is used by the online store theme to display the article on other pages, such as the home page or the main blog page.", + optional: true, + }, + image: { + type: "string", + label: "Image", + description: "The URL of the image associated with the article", + optional: true, + }, + tags: { propDefinition: [ - app, - "bodyHtml", + shopify, + "tags", ], + optional: true, }, }, + async run({ $ }) { + const response = await this.shopify.updateArticle({ + id: this.articleId, + article: { + title: this.title, + body: this.bodyHtml, + author: this.author && { + name: this.author, + }, + summary: this.summary, + image: this.image && { + url: this.image, + }, + tags: this.tags, + }, + }); + if (response.articleUpdate.userErrors.length > 0) { + throw new Error(response.articleUpdate.userErrors[0].message); + } + $.export("$summary", `Updated article with ID ${response.articleUpdate.article.id}.`); + + return response; + }, }; diff --git a/components/shopify/actions/update-customer/common.mjs b/components/shopify/actions/update-customer/common.mjs deleted file mode 100644 index 1711ccf0cb2cb..0000000000000 --- a/components/shopify/actions/update-customer/common.mjs +++ /dev/null @@ -1,26 +0,0 @@ -export default { - async run({ $ }) { - const metafields = await this.createMetafieldsArray(this.metafields, this.customerId, "customer"); - - const customer = { - first_name: this.firstName, - last_name: this.lastName, - email: this.email, - phone: this.phone, - addresses: [ - { - address1: this.address, - company: this.company, - city: this.city, - province: this.province, - country: this.country, - zip: this.zip, - }, - ], - metafields, - }; - const response = (await this.shopify.updateCustomer(this.customerId, customer)).result; - $.export("$summary", `Updated customer \`${response.email || response.first_name}\` with id \`${response.id}\``); - return response; - }, -}; diff --git a/components/shopify/actions/update-inventory-level/common.mjs b/components/shopify/actions/update-inventory-level/common.mjs deleted file mode 100644 index 5a00731c61037..0000000000000 --- a/components/shopify/actions/update-inventory-level/common.mjs +++ /dev/null @@ -1,33 +0,0 @@ -import { toSingleLineString } from "../common/common.mjs"; - -export default { - props: { - available: { - type: "integer", - label: "Available", - description: "Sets the available inventory quantity", - }, - disconnectIfNecessary: { - type: "boolean", - label: "Disconnect If Necessary", - description: toSingleLineString(` - Whether inventory for any previously connected locations will be set to 0 and the locations disconnected. - This property is ignored when no fulfillment service is involved. - For more information, refer to [Inventory levels and fulfillment service locations](https://shopify.dev/api/admin-rest/2022-01/resources/inventorylevel#inventory-levels-and-fulfillment-service-locations) - `), - optional: true, - }, - }, - async run({ $ }) { - let data = { - location_id: this.locationId, - inventory_item_id: this.inventoryItemId, - available: this.available, - disconnect_if_necessary: this.disconnectIfNecessary, - }; - - let response = (await this.shopify.updateInventoryLevel(data)).result; - $.export("$summary", `Updated inventory level for \`${response.inventory_item_id}\` at \`${response.location_id}\` to \`${response.available}\``); - return response; - }, -}; diff --git a/components/shopify/actions/update-inventory-level/update-inventory-level.mjs b/components/shopify/actions/update-inventory-level/update-inventory-level.mjs index b38d53f28493d..8710267e48448 100644 --- a/components/shopify/actions/update-inventory-level/update-inventory-level.mjs +++ b/components/shopify/actions/update-inventory-level/update-inventory-level.mjs @@ -1,12 +1,11 @@ import shopify from "../../shopify.app.mjs"; -import common from "./common.mjs"; +import { INVENTORY_ADJUSTMENT_REASONS } from "../../common/constants.mjs"; export default { - ...common, key: "shopify-update-inventory-level", name: "Update Inventory Level", - description: "Sets the inventory level for an inventory item at a location. [See the documenation](https://shopify.dev/api/admin-rest/2022-01/resources/inventorylevel#[post]/admin/api/2022-01/inventory_levels/set.json)", - version: "0.0.12", + description: "Sets the inventory level for an inventory item at a location. [See the documenation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventorySetOnHandQuantities)", + version: "0.0.13", type: "action", props: { shopify, @@ -31,6 +30,42 @@ export default { }), ], }, - ...common.props, + available: { + type: "integer", + label: "Available", + description: "Sets the available inventory quantity", + }, + reason: { + type: "string", + label: "Reason", + description: "The reason for the quantity changes", + options: INVENTORY_ADJUSTMENT_REASONS, + }, + referenceDocumentUri: { + type: "string", + label: "Reference Document URI", + description: "A freeform URI that represents why the inventory change happened. This can be the entity adjusting inventory quantities or the Shopify resource that's associated with the inventory adjustment. For example, a unit in a draft order might have been previously reserved, and a merchant later creates an order from the draft order. In this case, the referenceDocumentUri for the inventory adjustment is a URI referencing the order ID.", + optional: true, + }, + }, + async run({ $ }) { + const response = await this.shopify.updateInventoryLevel({ + input: { + reason: this.reason, + referenceDocumentUri: this.referenceDocumentUri, + setQuantities: [ + { + inventoryItemId: this.inventoryItemId, + locationId: this.locationId, + quantity: this.available, + }, + ], + }, + }); + if (response.inventorySetOnHandQuantities.userErrors.length > 0) { + throw new Error(response.inventorySetOnHandQuantities.userErrors[0].message); + } + $.export("$summary", `Updated inventory level for \`${this.inventoryItemId}\` at \`${this.locationId}\` to \`${this.available}\``); + return response; }, }; diff --git a/components/shopify/actions/update-metafield/common.mjs b/components/shopify/actions/update-metafield/common.mjs deleted file mode 100644 index 0412e5d754827..0000000000000 --- a/components/shopify/actions/update-metafield/common.mjs +++ /dev/null @@ -1,57 +0,0 @@ -import consts from "../common/consts.mjs"; - -export default { - async additionalProps() { - const props = await this.getOwnerIdProp(this.ownerResource); - - if (props.ownerId) { - props.ownerId = { - ...props.ownerId, - reloadProps: true, - }; - } - - if (this.ownerResource && this.ownerId) { - props.metafieldId = { - type: "string", - label: "Metafield ID", - description: "The metafield to update", - options: async () => { - return this.shopify.getMetafieldOptions(this.ownerResource, this.ownerId); - }, - reloadProps: true, - }; - } - - if (this.metafieldId) { - const { result } = await this.shopify.getMetafield(this.metafieldId); - props.value = { - type: consts.METAFIELD_TYPES[result.type], - label: "Value", - description: "The data to store in the metafield", - }; - } - - return props; - }, - async run({ $ }) { - const { result } = await this.shopify.getMetafield(this.metafieldId); - - const value = result.type.includes("list.") - ? JSON.stringify(this.value) - : this.value; - - const params = { - metafield_id: this.metafieldId, - owner_id: `${this.ownerId}`, - owner_resource: this.ownerResource, - key: result.key, - type: this.type, - value: value, - namespace: result.namespace, - }; - const response = await this.shopify.updateMetafield(this.metafieldId, params); - $.export("$summary", `Updated metafield for object with ID ${this.ownerId}`); - return response; - }, -}; diff --git a/components/shopify/actions/update-metafield/update-metafield.mjs b/components/shopify/actions/update-metafield/update-metafield.mjs index f7c248b0d0d4f..d4b66c43c58ca 100644 --- a/components/shopify/actions/update-metafield/update-metafield.mjs +++ b/components/shopify/actions/update-metafield/update-metafield.mjs @@ -1,19 +1,67 @@ -import metafieldActions from "../common/metafield-actions.mjs"; -import common from "./common.mjs"; +import common from "../common/metafield-actions.mjs"; export default { ...common, key: "shopify-update-metafield", name: "Update Metafield", - description: "Updates a metafield belonging to a resource. [See the docs](https://shopify.dev/api/admin-rest/2023-01/resources/metafield#put-blogs-blog-id-metafields-metafield-id)", - version: "0.0.10", + description: "Updates a metafield belonging to a resource. [See the documentation]()", + version: "0.0.11", type: "action", - props: { - ...metafieldActions.props, - ...common.props, + async additionalProps() { + const props = await this.getOwnerIdProp(this.ownerResource); + + if (props.ownerId) { + props.ownerId = { + ...props.ownerId, + reloadProps: true, + }; + } + + if (this.ownerResource && this.ownerId) { + props.metafieldId = { + type: "string", + label: "Metafield ID", + description: "The metafield to update", + options: async () => { + const metafields = await this.listMetafields(this.ownerResource, this.ownerId); + return metafields?.map(({ + id: value, key: label, + }) => ({ + value, + label, + })) || []; + }, + reloadProps: true, + }; + } + + if (this.metafieldId) { + props.value = { + type: "string", + label: "Value", + description: "The data to store in the metafield", + }; + } + + return props; }, - methods: { - ...metafieldActions.methods, - ...common.methods, + async run({ $ }) { + const metafields = await this.listMetafields(this.ownerResource, this.ownerId); + const metafield = metafields.find(({ id }) => id === this.metafieldId); + + const response = await this.shopify.updateMetafield({ + metafields: { + ownerId: this.ownerId, + key: metafield.key, + type: metafield.type, + value: this.value, + namespace: metafield.namespace, + }, + }); + if (response.metafieldsSet.userErrors.length > 0) { + throw new Error(response.metafieldsSet.userErrors[0].message); + } + $.export("$summary", `Updated metafield for object with ID ${this.ownerId}`); + return response; }, }; diff --git a/components/shopify/actions/update-metaobject/common.mjs b/components/shopify/actions/update-metaobject/common.mjs deleted file mode 100644 index 5c4b4cc6da232..0000000000000 --- a/components/shopify/actions/update-metaobject/common.mjs +++ /dev/null @@ -1,117 +0,0 @@ -import consts from "../common/consts.mjs"; - -export default { - props: { - metaobject: { - type: "string", - label: "Metaobject", - description: "The metaobject to update", - async options() { - if (!this.type) { - return []; - } - const { data: { metaobjects: { nodes } } } = await this.listMetaobjects({ - type: this.type, - }); - return nodes?.map(({ - id, displayName, - }) => ({ - label: displayName, - value: id, - })) || []; - }, - reloadProps: true, - }, - }, - async additionalProps() { - const props = {}; - if (!this.type) { - return props; - } - try { - return await this.getMetaobjectFields(props); - } - catch { - return await this.getTypeFields(props); - } - }, - methods: { - async getMetaobjectFields(props) { - const { data: { metaobject: { fields } } } = await this.getMetaobject({ - id: this.metaobject, - }); - for (const field of fields) { - const type = consts.METAFIELD_TYPES[field.type]; - props[field.key] = { - type, - label: field.definition.name, - optional: true, - }; - if (field.value) { - if (type === "integer") { - props[field.key].default = parseInt(field.value); - } else if (type === "boolean") { - props[field.key].default = field.value === "true" - ? true - : false; - } else if (type === "object" || type === "string[]") { - props[field.key].default = JSON.parse(field.value); - } else { - props[field.key].default = field.value; - } - } - } - return props; - }, - async getTypeFields(props) { - const { data: { metaobjectDefinitions: { nodes } } } = await this.listMetaobjectDefinitions(); - const { fieldDefinitions } = nodes.find(({ type }) => type === this.type); - for (const def of fieldDefinitions) { - props[def.key] = { - type: consts.METAFIELD_TYPES[def.type.name], - label: def.name, - optional: true, - }; - } - return props; - }, - }, - async run({ $ }) { - const { data: { metaobject: { fields } } } = await this.getMetaobject({ - id: this.metaobject, - }); - - const newFields = []; - for (const field of fields) { - const fieldValue = this[field.key] - ? this[field.key] - : field.value || ""; - newFields.push({ - key: field.key, - value: typeof fieldValue === "string" - ? fieldValue - : JSON.stringify(fieldValue), - }); - } - - const response = await this.updateMetaobject({ - id: this.metaobject, - fields: newFields, - $, - }); - - let errorMessage; - if (response?.errors?.length) { - errorMessage = response.errors[0].message; - } - if (response?.data?.metaobjectUpdate?.userErrors?.length) { - errorMessage = response.data.metaobjectUpdate.userErrors[0].message; - } - if (errorMessage) { - throw new Error(`${errorMessage}`); - } - - $.export("$summary", `Successfully updated metaobject with ID ${this.metaobject}`); - return response; - }, -}; diff --git a/components/shopify/actions/update-metaobject/update-metaobject.mjs b/components/shopify/actions/update-metaobject/update-metaobject.mjs index 07e8b70042776..6cbb9096e6ce4 100644 --- a/components/shopify/actions/update-metaobject/update-metaobject.mjs +++ b/components/shopify/actions/update-metaobject/update-metaobject.mjs @@ -1,32 +1,104 @@ import shopify from "../../shopify.app.mjs"; -import metaobjects from "../common/metaobjects.mjs"; -import common from "./common.mjs"; +import common from "../common/metaobjects.mjs"; +import { MAX_LIMIT } from "../../common/constants.mjs"; export default { - ...metaobjects, ...common, key: "shopify-update-metaobject", name: "Update Metaobject", - description: "Updates a metaobject. [See the documentation](https://shopify.dev/docs/api/admin-graphql/2023-04/mutations/metaobjectUpdate)", - version: "0.0.6", + description: "Updates a metaobject. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/metaobjectUpdate)", + version: "0.0.7", type: "action", - methods: { - ...metaobjects.methods, - ...common.methods, - }, props: { shopify, type: { - type: "string", - label: "Type", - description: "The Metaobject Type", - async options() { - const { data: { metaobjectDefinitions: { nodes } } } - = await this.listMetaobjectDefinitions(); - return nodes?.map(({ type }) => type) || []; - }, + propDefinition: [ + shopify, + "metaobjectType", + ], + reloadProps: true, + }, + metaobject: { + propDefinition: [ + shopify, + "metaobjectId", + (c) => ({ + type: c.type, + }), + ], reloadProps: true, }, - ...common.props, + }, + async additionalProps() { + const props = {}; + if (!this.type) { + return props; + } + try { + return await this.getMetaobjectFields(props); + } + catch { + return await this.getTypeFields(props); + } + }, + methods: { + ...common.methods, + async getMetaobjectFields(props) { + const { metaobject: { fields } } = await this.getMetaobject({ + id: this.metaobject, + }); + for (const field of fields) { + props[field.key] = { + type: "string", + label: field.definition.name, + optional: true, + }; + if (field.value) { + props[field.key].default = field.value; + } + } + return props; + }, + async getTypeFields(props) { + const { metaobjectDefinitions: { nodes } } = await this.shopify.listMetaobjectDefinitions({ + first: MAX_LIMIT, + }); + const { fieldDefinitions } = nodes.find(({ id }) => id === this.type); + for (const def of fieldDefinitions) { + props[def.key] = { + type: "string", + label: def.name, + optional: true, + }; + } + return props; + }, + }, + async run({ $ }) { + const { metaobject: { fields } } = await this.getMetaobject({ + id: this.metaobject, + }); + + const newFields = []; + for (const field of fields) { + newFields.push({ + key: field.key, + value: this[field.key] + ? this[field.key] + : field.value || "", + }); + } + + const response = await this.updateMetaobject({ + id: this.metaobject, + fields: newFields, + }); + + if (response.metaobjectUpdate.userErrors.length > 0) { + throw new Error(response.metaobjectUpdate.userErrors[0].message); + } + + $.export("$summary", `Successfully updated metaobject with ID ${this.metaobject}`); + return response; }, }; diff --git a/components/shopify/actions/update-page/common.mjs b/components/shopify/actions/update-page/common.mjs deleted file mode 100644 index 6f5eb1dd86635..0000000000000 --- a/components/shopify/actions/update-page/common.mjs +++ /dev/null @@ -1,34 +0,0 @@ -export default { - methods: { - updatePage({ - pageId, ...args - } = {}) { - return this.app.put({ - path: `/pages/${pageId}`, - ...args, - }); - }, - }, - async run({ $: step }) { - const { - pageId, - title, - bodyHtml, - } = this; - - const response = await this.updatePage({ - step, - pageId, - data: { - page: { - title, - body_html: bodyHtml, - }, - }, - }); - - step.export("$summary", `Updated page with ID ${response.page.id}`); - - return response; - }, -}; diff --git a/components/shopify/actions/update-page/update-page.mjs b/components/shopify/actions/update-page/update-page.mjs index ada9cffa505eb..ba4e404564996 100644 --- a/components/shopify/actions/update-page/update-page.mjs +++ b/components/shopify/actions/update-page/update-page.mjs @@ -1,33 +1,45 @@ -import app from "../../common/rest-admin.mjs"; -import common from "./common.mjs"; +import shopify from "../../shopify.app.mjs"; export default { - ...common, key: "shopify-update-page", name: "Update Page", - description: "Update an existing page. [See The Documentation](https://shopify.dev/docs/api/admin-rest/2023-04/resources/page#put-pages-page-id)", - version: "0.0.6", + description: "Update an existing page. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/pageUpdate)", + version: "0.0.7", type: "action", props: { - app, + shopify, pageId: { propDefinition: [ - app, + shopify, "pageId", ], }, title: { - description: "The title of the page.", - propDefinition: [ - app, - "title", - ], + type: "string", + label: "Title", + description: "The title of the page", + optional: true, }, - bodyHtml: { - propDefinition: [ - app, - "bodyHtml", - ], + body: { + type: "string", + label: "Body", + description: "The text content of the page, complete with HTML markup", + optional: true, }, }, + async run({ $ }) { + + const response = await this.shopify.updatePage({ + id: this.pageId, + page: { + title: this.title, + body: this.body, + }, + }); + if (response.pageUpdate.userErrors.length > 0) { + throw new Error(response.pageUpdate.userErrors[0].message); + } + $.export("$summary", `Updated page with ID ${response.pageUpdate.page.id}`); + return response; + }, }; diff --git a/components/shopify/actions/update-product-variant/common.mjs b/components/shopify/actions/update-product-variant/common.mjs deleted file mode 100644 index f390352aa72c3..0000000000000 --- a/components/shopify/actions/update-product-variant/common.mjs +++ /dev/null @@ -1,114 +0,0 @@ -import { ConfigurationError } from "@pipedream/platform"; - -export default { - props: { - available: { - type: "string", - label: "Available Quantity", - description: "Sets the available inventory quantity", - optional: true, - }, - barcode: { - type: "string", - label: "Barcode", - description: "The barcode, UPC, or ISBN number for the product", - optional: true, - }, - weight: { - type: "string", - label: "Weight", - description: "The weight of the product variant in the unit system specified with Weight Unit", - optional: true, - }, - weightUnit: { - type: "string", - label: "Weight Unit", - description: "The unit of measurement that applies to the product variant's weight. If you don't specify a value for weight_unit, then the shop's default unit of measurement is applied.", - optional: true, - options: [ - "g", - "kg", - "oz", - "lb", - ], - }, - harmonizedSystemCode: { - type: "integer", - label: "Harmonized System Code", - description: "The general [harmonized system](https://en.wikipedia.org/wiki/Harmonized_System) code for the inventory item", - optional: true, - }, - }, - async run({ $ }) { - const { - productVariantId, - option, - price, - imageId, - metafields: metafieldsArray, - sku, - countryCodeOfOrigin, - harmonizedSystemCode, - locationId, - available, - barcode, - weight, - weightUnit, - } = this; - - if (available && !locationId) { - throw new ConfigurationError("Must enter LocationId to set the available quantity"); - } - - if (!option - && !price - && !imageId - && !metafieldsArray - && !sku - && !barcode - && !weight - && !weightUnit - ) { - throw new ConfigurationError("Must enter one of `title`, `price`, `imageId`, `metafields`, `sku`, `barcode`, `weight`, or `weightUnit`."); - } - - const metafields = await this.createMetafieldsArray(metafieldsArray, productVariantId, "variants"); - - const response = {}; - const { result: productVariant } = await this.shopify.updateProductVariant(productVariantId, { - option1: option, - price, - image_id: imageId, - metafields, - sku, - barcode, - weight, - weight_unit: weightUnit, - }); - response.productVariant = productVariant; - - if (countryCodeOfOrigin || harmonizedSystemCode) { - const { result: inventoryItem } = await this.shopify.updateInventoryItem( - productVariant.inventory_item_id, { - country_code_of_origin: countryCodeOfOrigin, - harmonized_system_code: harmonizedSystemCode, - }, - ); - response.inventoryItem = inventoryItem; - } - - if (available) { - const { result: inventoryLevel } = await this.shopify.updateInventoryLevel({ - inventory_item_id: productVariant.inventory_item_id, - location_id: locationId, - available, - }); - response.inventoryLevel = inventoryLevel; - const { result: updatedVariant } = await this.shopify.getProductVariant(productVariantId); - response.productVariant = updatedVariant; - } - - $.export("$summary", `Updated product variant \`${productVariant.title}\` with id \`${productVariant.id}\``); - return response; - }, -}; diff --git a/components/shopify/actions/update-product-variant/update-product-variant.mjs b/components/shopify/actions/update-product-variant/update-product-variant.mjs index ff1a6f7abbc17..08c29207fccc2 100644 --- a/components/shopify/actions/update-product-variant/update-product-variant.mjs +++ b/components/shopify/actions/update-product-variant/update-product-variant.mjs @@ -1,14 +1,12 @@ import shopify from "../../shopify.app.mjs"; -import metafieldActions from "../common/metafield-actions.mjs"; -import common from "./common.mjs"; +import utils from "../../common/utils.mjs"; +import { MAX_LIMIT } from "../../common/constants.mjs"; export default { - ...common, - ...metafieldActions, key: "shopify-update-product-variant", name: "Update Product Variant", - description: "Update an existing product variant. [See the docs](https://shopify.dev/api/admin-rest/2022-01/resources/product-variant#[put]/admin/api/2022-01/variants/{variant_id}.json)", - version: "0.0.15", + description: "Update an existing product variant. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/productVariantsBulkUpdate)", + version: "0.0.16", type: "action", props: { shopify, @@ -22,30 +20,61 @@ export default { propDefinition: [ shopify, "productVariantId", - (c) => c, + (c) => ({ + productId: c.productId, + }), ], - description: `${shopify.propDefinitions.productVariantId.description} - Option displayed here as the title of the product variant`, }, - option: { + optionIds: { propDefinition: [ shopify, - "option", + "productOptionIds", + (c) => ({ + productId: c.productId, + }), ], optional: true, }, price: { - propDefinition: [ - shopify, - "price", - ], + type: "string", + label: "Price", description: "The price of the product variant", + optional: true, }, - imageId: { - propDefinition: [ - shopify, - "imageId", - (c) => c, + image: { + type: "string", + label: "Image", + description: "The URL of an image to be added to the product variant", + optional: true, + }, + sku: { + type: "string", + label: "Sku", + description: "A unique identifier for the product variant in the shop", + optional: true, + }, + barcode: { + type: "string", + label: "Barcode", + description: "The barcode, UPC, or ISBN number for the product", + optional: true, + }, + weight: { + type: "string", + label: "Weight", + description: "The weight of the product variant in the unit system specified with Weight Unit", + optional: true, + }, + weightUnit: { + type: "string", + label: "Weight Unit", + description: "The unit of measurement that applies to the product variant's weight. If you don't specify a value for weight_unit, then the shop's default unit of measurement is applied.", + optional: true, + options: [ + "g", + "kg", + "oz", + "lb", ], }, metafields: { @@ -54,26 +83,53 @@ export default { "metafields", ], }, - sku: { - propDefinition: [ - shopify, - "sku", - ], - }, - countryCodeOfOrigin: { - propDefinition: [ - shopify, - "country", - ], - description: "The country code [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) of where the item came from", + }, + methods: { + async getOptionValues() { + const { product: { options } } = await this.shopify.getProduct({ + id: this.productId, + first: MAX_LIMIT, + }); + const productOptions = {}; + for (const option of options) { + for (const optionValue of option.optionValues) { + productOptions[optionValue.id] = option.id; + } + } + const optionValues = this.optionIds.map((id) => ({ + id, + optionId: productOptions[id], + })); + return optionValues; }, - locationId: { - propDefinition: [ - shopify, - "locationId", + }, + async run({ $ }) { + const response = await this.shopify.updateProductVariant({ + productId: this.productId, + variants: [ + { + id: this.productVariantId, + optionValues: this.optionIds && await this.getOptionValues(), + price: this.price, + mediaSrc: this.image, + barcode: this.barcode, + inventoryItem: (this.sku || this.weightUnit || this.weight) && { + sku: this.sku, + measurement: (this.weightUnit || this.weight) && { + weight: { + unit: this.weightUnit, + value: this.weight, + }, + }, + }, + metafields: this.metafields && utils.parseJson(this.metafields), + }, ], - optional: true, - }, - ...common.props, + }); + if (response.productVariantsBulkUpdate.userErrors.length > 0) { + throw new Error(response.productVariantsBulkUpdate.userErrors[0].message); + } + $.export("$summary", `Created new product variant \`${response.productVariantsBulkUpdate.productVariants[0].title}\` with ID \`${response.productVariantsBulkUpdate.productVariants[0].id}\``); + return response; }, }; diff --git a/components/shopify/actions/update-product/common.mjs b/components/shopify/actions/update-product/common.mjs deleted file mode 100644 index ceb67849bc111..0000000000000 --- a/components/shopify/actions/update-product/common.mjs +++ /dev/null @@ -1,59 +0,0 @@ -export default { - props: { - handle: { - type: "string", - label: "Handle", - description: "A unique human-friendly string for the product that serves as the URL handle. Automatically generated from the product's title.", - optional: true, - }, - seoTitle: { - type: "string", - label: "SEO Title", - description: "The product title used for search engine optimization", - optional: true, - }, - seoDescription: { - type: "string", - label: "SEO Description", - description: "The product description used for search engine optimization", - optional: true, - }, - }, - async run({ $ }) { - const metafields = await this.createMetafieldsArray(this.metafields, this.productId, "product"); - - const variants = []; - const variantsArray = this.shopify.parseArrayOfJSONStrings(this.variants); - for (const variant of variantsArray) { - if (variant.metafields) { - const variantMetafields = await this.createMetafieldsArray(variant.metafields, variant.id, "variants"); - variants.push({ - ...variant, - metafields: variantMetafields, - }); - continue; - } - variants.push(variant); - } - - const product = { - title: this.title, - body_html: this.productDescription, - vendor: this.vendor, - product_type: this.productType, - status: this.status, - images: this.shopify.parseImages(this.images), - options: this.shopify.parseArrayOfJSONStrings(this.options), - variants, - tags: this.shopify.parseCommaSeparatedStrings(this.tags), - metafields, - metafields_global_title_tag: this.seoTitle, - metafields_global_description_tag: this.seoDescription, - handle: this.handle, - }; - - const response = (await this.shopify.updateProduct(this.productId, product)).result; - $.export("$summary", `Updated product \`${response.title}\` with id \`${response.id}\``); - return response; - }, -}; diff --git a/components/shopify/actions/update-product/update-product.mjs b/components/shopify/actions/update-product/update-product.mjs index 542682cf1fc39..35029f27b5522 100644 --- a/components/shopify/actions/update-product/update-product.mjs +++ b/components/shopify/actions/update-product/update-product.mjs @@ -1,14 +1,12 @@ import shopify from "../../shopify.app.mjs"; -import metafieldActions from "../common/metafield-actions.mjs"; -import common from "./common.mjs"; +import common from "../common/metafield-actions.mjs"; export default { ...common, - ...metafieldActions, key: "shopify-update-product", name: "Update Product", - description: "Update an existing product. [See the docs](https://shopify.dev/api/admin-rest/2022-01/resources/product#[put]/admin/api/2022-01/products/{product_id}.json)", - version: "0.1.4", + description: "Update an existing product. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/productUpdate)", + version: "0.1.5", type: "action", props: { shopify, @@ -19,59 +17,52 @@ export default { ], }, title: { - propDefinition: [ - shopify, - "title", - ], + type: "string", + label: "Title", + description: "The new title of the product", optional: true, }, productDescription: { - propDefinition: [ - shopify, - "productDescription", - ], + type: "string", + label: "Product Description", + description: "A description of the product. Supports HTML formatting. Example: `Good snowboard!`", + optional: true, }, vendor: { - propDefinition: [ - shopify, - "vendor", - ], + type: "string", + label: "Vendor", + description: "The name of the product's vendor", + optional: true, }, productType: { - propDefinition: [ - shopify, - "productType", - ], + type: "string", + label: "Product Type", + description: "A categorization for the product used for filtering and searching products", + optional: true, }, status: { - propDefinition: [ - shopify, - "status", + type: "string", + label: "Status", + description: "The status of the product. `active`: The product is ready to sell and is available to customers on the online store, sales channels, and apps. By default, existing products are set to active. `archived`: The product is no longer being sold and isn't available to customers on sales channels and apps. `draft`: The product isn't ready to sell and is unavailable to customers on sales channels and apps. By default, duplicated and unarchived products are set to draft", + optional: true, + options: [ + "ACTIVE", + "ARCHIVED", + "DRAFT", ], }, images: { - propDefinition: [ - shopify, - "images", - ], - }, - options: { - propDefinition: [ - shopify, - "options", - ], - }, - variants: { - propDefinition: [ - shopify, - "variants", - ], + type: "string[]", + label: "Images", + description: "A list of URLs of images to associate with the new product", + optional: true, }, tags: { propDefinition: [ shopify, "tags", ], + optional: true, }, metafields: { propDefinition: [ @@ -79,6 +70,55 @@ export default { "metafields", ], }, - ...common.props, + handle: { + type: "string", + label: "Handle", + description: "A unique human-friendly string for the product that serves as the URL handle. Automatically generated from the product's title.", + optional: true, + }, + seoTitle: { + type: "string", + label: "SEO Title", + description: "The product title used for search engine optimization", + optional: true, + }, + seoDescription: { + type: "string", + label: "SEO Description", + description: "The product description used for search engine optimization", + optional: true, + }, + }, + async run({ $ }) { + const metafields = await this.createMetafieldsArray(this.metafields, this.productId, "product"); + + const response = await this.shopify.updateProduct({ + input: { + id: this.productId, + title: this.title, + descriptionHtml: this.productDescription, + vendor: this.vendor, + productType: this.productType, + status: this.status, + tags: this.tags, + metafields, + seo: this.seoTitle || this.seoDescription + ? { + title: this.seoTitle, + description: this.seoDescription, + } + : undefined, + handle: this.handle, + }, + media: this.images && this.images.map((image) => ({ + mediaContentType: "IMAGE", + originalSource: image, + })), + }); + if (response.productUpdate.userErrors.length > 0) { + throw new Error(response.productUpdate.userErrors[0].message); + } + $.export("$summary", `Updated product \`${response.productUpdate.product.title}\` with id \`${this.productId}\``); + return response; }, }; diff --git a/components/shopify/common/constants.mjs b/components/shopify/common/constants.mjs index a8707fedde4de..ce45210ee0e70 100644 --- a/components/shopify/common/constants.mjs +++ b/components/shopify/common/constants.mjs @@ -1,9 +1,135 @@ +const API_VERSION = "2025-01"; + const DEFAULT_LIMIT = 20; const MAX_LIMIT = 250; -const API_VERSION = "2025-01"; + +const PRODUCT_SORT_KEY = [ + "CREATED_AT", + "ID", + "INVENTORY_TOTAL", + "PRODUCT_TYPE", + "PUBLISHED_AT", + "RELEVANCE", + "TITLE", + "UPDATED_AT", + "VENDOR", +]; + +const COLLECTION_SORT_KEY = [ + "ID", + "RELEVANCE", + "TITLE", + "UPDATED_AT", +]; + +const COLLECTION_RULE_COLUMNS = [ + "IS_PRICE_REDUCED", + "PRODUCT_CATEGORY_ID", + "PRODUCT_METAFIELD_DEFINITION", + "PRODUCT_TAXONOMY_NODE_ID", + "TAG", + "TITLE", + "TYPE", + "VARIANT_COMPARE_AT_PRICE", + "VARIANT_INVENTORY", + "VARIANT_METAFIELD_DEFINITION", + "VARIANT_PRICE", + "VARIANT_TITLE", + "VARIANT_WEIGHT", + "VENDOR", +]; + +const COLLECTION_RULE_RELATIONS = [ + "CONTAINS", + "ENDS_WITH", + "EQUALS", + "GREATER_THAN", + "IS_NOT_SET", + "IS_SET", + "LESS_THAN", + "NOT_CONTAINS", + "NOT_EQUALS", + "STARTS_WITH", +]; + +const INVENTORY_ADJUSTMENT_REASONS = [ + { + value: "correction", + label: "Used to correct an inventory error or as a general adjustment reason", + }, + { + value: "cycle_count_available", + label: "Used to specify an adjusted inventory count due to a discrepancy between the actual inventory quantity and previously recorded inventory quantity", + }, + { + value: "damaged", + label: "Used to remove units from inventory count due to damage", + }, + { + value: "movement_created", + label: "Used to specify that an inventory transfer or a purchase order has been created", + }, + { + value: "movement_updated", + label: "Used to specify that an inventory transfer or a purchase order has been updated", + }, + { + value: "movement_received", + label: "Used to specify that an inventory transfer or a purchase order has been received", + }, + { + value: "movement_canceled", + label: "Used to specify that an inventory transfer or a purchase order has been canceled", + }, + { + value: "other", + label: "Used to specify an alternate reason for the inventory adjustment", + }, + { + value: "promotion", + label: "Used to remove units from inventory count due to a promotion or donation", + }, + { + value: "quality_control", + label: "Used to specify that on-hand units that aren't sellable because they're currently in inspection for quality purposes", + }, + { + value: "received", + label: "Used to specify inventory that the merchant received", + }, + { + value: "reservation_created", + label: "Used to reserve, or temporarily set aside unavailable units", + }, + { + value: "reservation_deleted", + label: "Used to remove the number of unavailable units that have been reserved", + }, + { + value: "reservation_updated", + label: "Used to update the number of unavailable units that have been reserved", + }, + { + value: "restock", + label: "Used to add a returned unit back to available inventory so the unit can be resold", + }, + { + value: "safety_stock", + label: "Used to specify that on-hand units are being set aside to help guard against overselling", + }, + { + value: "shrinkage", + label: "Used when actual inventory levels are less than recorded due to theft or loss", + }, +]; export { + API_VERSION, DEFAULT_LIMIT, MAX_LIMIT, - API_VERSION, + PRODUCT_SORT_KEY, + COLLECTION_SORT_KEY, + COLLECTION_RULE_COLUMNS, + COLLECTION_RULE_RELATIONS, + INVENTORY_ADJUSTMENT_REASONS, }; diff --git a/components/shopify/common/mutations.mjs b/components/shopify/common/mutations.mjs index b0a7fafd8c8ff..3088df26f8734 100644 --- a/components/shopify/common/mutations.mjs +++ b/components/shopify/common/mutations.mjs @@ -33,7 +33,413 @@ const DELETE_WEBHOOK = ` } `; +const ADD_TAGS = ` + mutation tagsAdd($gid: ID!, $tags: [String!]!) { + tagsAdd(id: $gid, tags: $tags) { + node { + id + } + userErrors { + field + message + } + } + } +`; + +const ADD_PRODUCTS_TO_COLLECTION = ` + mutation collectionAddProductsV2($id: ID!, $productIds: [ID!]!) { + collectionAddProductsV2(id: $id, productIds: $productIds) { + job { + done + id + } + userErrors { + field + message + } + } + } +`; + +const CREATE_ARTICLE = ` + mutation CreateArticle($article: ArticleCreateInput!) { + articleCreate(article: $article) { + article { + id + title + author { + name + } + handle + body + summary + tags + image { + altText + originalSrc + } + } + userErrors { + field + message + } + } + } +`; + +const CREATE_BLOG = ` + mutation CreateBlog($blog: BlogCreateInput!) { + blogCreate(blog: $blog) { + blog { + id + title + handle + templateSuffix + commentPolicy + } + userErrors { + field + message + } + } + } +`; + +const CREATE_COLLECTION = ` + mutation createCollectionMetafields($input: CollectionInput!) { + collectionCreate(input: $input) { + collection { + id + title + metafields(first: 50) { + edges { + node { + id + namespace + key + value + } + } + } + } + userErrors { + message + field + } + } + } +`; + +const CREATE_PAGE = ` + mutation CreatePage($page: PageCreateInput!) { + pageCreate(page: $page) { + page { + id + title + handle + } + userErrors { + field + message + } + } + } +`; + +const CREATE_PRODUCT = ` + mutation ($product: ProductCreateInput, $media: [CreateMediaInput!]) { + productCreate(product: $product, media: $media) { + product { + id + title + options { + id + name + position + values + optionValues { + id + name + hasVariants + } + } + } + userErrors { + field + message + } + } + } +`; + +const CREATE_METAFIELD = ` + mutation MetafieldsSet($metafields: [MetafieldsSetInput!]!) { + metafieldsSet(metafields: $metafields) { + metafields { + key + namespace + value + createdAt + updatedAt + } + userErrors { + field + message + } + } + } +`; + +const CREATE_METAOBJECT = ` + mutation metaobjectCreate($metaobject: MetaobjectCreateInput!) { + metaobjectCreate(metaobject: $metaobject) { + metaobject { + id + handle + type + } + userErrors { + field + message + } + } + } +`; + +const CREATE_PRODUCT_VARIANTS = ` + mutation ProductVariantsCreate($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { + productVariantsBulkCreate(productId: $productId, variants: $variants) { + productVariants { + id + title + selectedOptions { + name + value + } + } + userErrors { + field + message + } + } + } +`; + +const UPDATE_METAFIELD = ` + mutation MetafieldsSet($metafields: [MetafieldsSetInput!]!) { + metafieldsSet(metafields: $metafields) { + metafields { + key + namespace + value + createdAt + updatedAt + } + userErrors { + field + message + } + } + } +`; + +const UPDATE_METAOBJECT = ` + mutation metaobjectUpdate ($id: ID!, $metaobject: MetaobjectUpdateInput!) { + metaobjectUpdate(id: $id, metaobject: $metaobject) { + metaobject { + id + type + } + userErrors { + field + message + } + } + } +`; + +const UPDATE_PRODUCT = ` + mutation UpdateProductWithNewMedia($input: ProductInput!, $media: [CreateMediaInput!]) { + productUpdate(input: $input, media: $media) { + product { + id + title + options { + id + name + position + values + optionValues { + id + name + hasVariants + } + } + } + userErrors { + field + message + } + } + } +`; + +const UPDATE_ARTICLE = ` + mutation UpdateArticle($id: ID!, $article: ArticleUpdateInput!) { + articleUpdate(id: $id, article: $article) { + article { + id + title + handle + body + summary + tags + image { + altText + originalSrc + } + } + userErrors { + field + message + } + } + } +`; + +const UPDATE_PAGE = ` + mutation UpdatePage($id: ID!, $page: PageUpdateInput!) { + pageUpdate(id: $id, page: $page) { + page { + id + title + handle + } + userErrors { + field + message + } + } + } +`; + +const UPDATE_INVENTORY_LEVEL = ` + mutation inventorySetOnHandQuantities($input: InventorySetOnHandQuantitiesInput!) { + inventorySetOnHandQuantities(input: $input) { + inventoryAdjustmentGroup { + createdAt + reason + referenceDocumentUri + changes { + name + delta + } + } + userErrors { + field + message + } + } + } +`; + +const UPDATE_PRODUCT_VARIANT = ` + mutation productVariantsBulkUpdate($productId: ID!, $variants: [ProductVariantsBulkInput!]!) { + productVariantsBulkUpdate(productId: $productId, variants: $variants) { + product { + id + } + productVariants { + id + title + selectedOptions { + name + value + } + } + userErrors { + field + message + } + } + } +`; + +const DELETE_ARTICLE = ` + mutation DeleteArticle($id: ID!) { + articleDelete(id: $id) { + deletedArticleId + userErrors { + field + message + } + } + } +`; + +const DELETE_BLOG = ` + mutation DeleteBlog($id: ID!) { + blogDelete(id: $id) { + deletedBlogId + userErrors { + field + message + } + } + } +`; + +const DELETE_PAGE = ` + mutation DeletePage($id: ID!) { + pageDelete(id: $id) { + deletedPageId + userErrors { + field + message + } + } + } +`; + +const DELETE_METAFIELD = ` + mutation MetafieldsDelete($metafields: [MetafieldIdentifierInput!]!) { + metafieldsDelete(metafields: $metafields) { + deletedMetafields { + key + namespace + ownerId + } + userErrors { + field + message + } + } + } +`; + export default { CREATE_WEBHOOK, DELETE_WEBHOOK, + ADD_TAGS, + ADD_PRODUCTS_TO_COLLECTION, + CREATE_ARTICLE, + CREATE_BLOG, + CREATE_COLLECTION, + CREATE_PAGE, + CREATE_PRODUCT, + CREATE_PRODUCT_VARIANTS, + CREATE_METAFIELD, + CREATE_METAOBJECT, + UPDATE_METAFIELD, + UPDATE_METAOBJECT, + UPDATE_PRODUCT, + UPDATE_ARTICLE, + UPDATE_PAGE, + UPDATE_INVENTORY_LEVEL, + UPDATE_PRODUCT_VARIANT, + DELETE_ARTICLE, + DELETE_BLOG, + DELETE_PAGE, + DELETE_METAFIELD, }; diff --git a/components/shopify/common/queries.mjs b/components/shopify/common/queries.mjs index 709f6973b6652..8fffc59ac91fb 100644 --- a/components/shopify/common/queries.mjs +++ b/components/shopify/common/queries.mjs @@ -1,4 +1,32 @@ -const GET_BLOG = ` +const LIST_ABANDONED_CHECKOUTS = ` + query AbandonedCheckouts($first: Int, $after: String) { + abandonedCheckouts(first: $first, after: $after) { + nodes { + abandonedCheckoutUrl + billingAddress { + country + } + completedAt + createdAt + customer { + firstName + lastName + email + } + id + shippingAddress { + country + } + updatedAt + } + pageInfo { + endCursor + } + } + } +`; + +const LIST_BLOG_ARTICLES = ` query BlogShow($id: ID!, $first: Int, $after: String, $reverse: Boolean) { blog(id: $id) { articles(first: $first, after: $after, reverse: $reverse) { @@ -18,6 +46,15 @@ const GET_BLOG = ` publishedAt summary tags + metafields (first: $first) { + nodes { + id + key + namespace + value + type + } + } } pageInfo { endCursor @@ -33,6 +70,15 @@ const LIST_BLOGS = ` nodes { id title + metafields (first: $first) { + nodes { + id + key + namespace + value + type + } + } } pageInfo { endCursor @@ -58,6 +104,7 @@ const LIST_PAGES = ` key namespace value + type } } publishedAt @@ -71,8 +118,8 @@ const LIST_PAGES = ` `; const LIST_PRODUCTS = ` - query ($first: Int, $after: String, $reverse: Boolean, $sortKey: ProductSortKeys) { - products (first: $first, after: $after, reverse: $reverse, sortKey: $sortKey) { + query ($first: Int, $after: String, $reverse: Boolean, $sortKey: ProductSortKeys, $query: String) { + products (first: $first, after: $after, reverse: $reverse, sortKey: $sortKey, query: $query) { nodes { id title @@ -94,6 +141,7 @@ const LIST_PRODUCTS = ` key namespace value + type } } onlineStoreUrl @@ -110,16 +158,331 @@ const LIST_PRODUCTS = ` } } vendor - } pageInfo { + options (first: $first) { + id + name + } + } + pageInfo { + endCursor + } + } + } +`; + +const LIST_COLLECTIONS = ` + query CustomCollectionList($first: Int, $after: String, $reverse: Boolean, $query: String) { + collections(first: $first, after: $after, reverse: $reverse, query: $query) { + nodes { + id + title + description + descriptionHtml + handle + metafields (first: $first) { + nodes { + id + key + namespace + value + type + } + } + products (first: $first) { + nodes { + id + title + } + } + productsCount { + count + } + seo { + title + description + } + updatedAt + } + pageInfo { endCursor } } } `; +const LIST_PRODUCT_VARIANTS = ` + query ($first: Int, $after: String, $query: String) { + productVariants (first: $first, after: $after, query: $query) { + nodes { + id + title + inventoryItem { + id + } + metafields (first: $first) { + nodes { + id + key + namespace + value + type + } + } + } + pageInfo { + endCursor + } + } + } +`; + +const LIST_METAOBJECTS = ` + query ($type: String!, $first: Int, $after: String) { + metaobjects(type: $type, first: $first, after: $after) { + nodes { + id + handle + type + displayName + updatedAt + fields { + key + type + value + } + } + pageInfo { + endCursor + } + } + } +`; + +const LIST_METAOBJECT_DEFINITIONS = ` + query ($first: Int, $after: String) { + metaobjectDefinitions(first: $first, after: $after) { + nodes { + id + name + type + fieldDefinitions { + key + type { + name + } + name + } + } + pageInfo { + endCursor + } + } + } +`; + +const LIST_LOCATIONS = ` + query ($first: Int, $after: String) { + locations(first: $first, after: $after) { + nodes { + id + name + address { + formatted + } + } + pageInfo { + endCursor + } + } + } +`; + +const GET_PRODUCT = ` + query ($id: ID!, $first: Int, $after: String) { + product (id: $id) { + id + title + media (first: $first, after: $after) { + nodes { + id + } + pageInfo { + endCursor + } + } + metafields (first: $first, after: $after) { + nodes { + id + key + namespace + value + type + } + pageInfo { + endCursor + } + } + options (first: $first) { + id + name + optionValues { + id + name + } + } + }, + } +`; + +const GET_PRODUCT_VARIANT = ` + query ($id: ID!, $first: Int, $after: String) { + productVariant (id: $id) { + id + title + metafields (first: $first, after: $after) { + nodes { + id + key + namespace + value + type + } + pageInfo { + endCursor + } + } + } + } +`; + +const GET_COLLECTION = ` + query ($id: ID!, $first: Int, $after: String) { + collection(id: $id) { + id + title + handle + updatedAt + metafields (first: $first, after: $after) { + nodes { + id + key + namespace + value + type + } + pageInfo { + endCursor + } + } + } + } +`; + +const GET_BLOG = ` + query BlogShow($id: ID!, $first: Int, $after: String) { + blog(id: $id) { + id + title + handle + metafields (first: $first, after: $after) { + nodes { + id + key + namespace + value + type + } + pageInfo { + endCursor + } + } + } + } +`; + +const GET_PAGE = ` + query PageShow($id: ID!, $first: Int, $after: String) { + page(id: $id) { + id + title + handle + metafields (first: $first, after: $after) { + nodes { + id + key + namespace + value + type + } + pageInfo { + endCursor + } + } + } + } +`; + +const GET_ARTICLE = ` + query ArticleShow($id: ID!, $first: Int, $after: String) { + article(id: $id) { + id + author { + name + } + createdAt + handle + metafields (first: $first, after: $after) { + nodes { + id + key + namespace + value + type + } + pageInfo { + endCursor + } + } + } + } +`; + +const GET_METAOBJECT = ` + query ($id: ID!) { + metaobject(id: $id) { + id + handle + type + displayName + updatedAt + fields { + key + type + value + definition { + name + } + } + } + } +`; + export default { - GET_BLOG, + LIST_ABANDONED_CHECKOUTS, + LIST_BLOG_ARTICLES, LIST_BLOGS, LIST_PAGES, LIST_PRODUCTS, + LIST_COLLECTIONS, + LIST_PRODUCT_VARIANTS, + LIST_METAOBJECTS, + LIST_METAOBJECT_DEFINITIONS, + LIST_LOCATIONS, + GET_PRODUCT, + GET_PRODUCT_VARIANT, + GET_COLLECTION, + GET_BLOG, + GET_PAGE, + GET_ARTICLE, + GET_METAOBJECT, }; diff --git a/components/shopify/common/utils.mjs b/components/shopify/common/utils.mjs new file mode 100644 index 0000000000000..e27c96079a459 --- /dev/null +++ b/components/shopify/common/utils.mjs @@ -0,0 +1,33 @@ +function getIdFromGid(gid) { + return gid.split("/").pop(); +} + +function parseJson(obj) { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +} + +export default { + getIdFromGid, + parseJson, +}; diff --git a/components/shopify/shopify.app.mjs b/components/shopify/shopify.app.mjs index e9cad0d2be3d8..db8e45ca6c70f 100644 --- a/components/shopify/shopify.app.mjs +++ b/components/shopify/shopify.app.mjs @@ -4,8 +4,9 @@ import retry from "async-retry"; import mutations from "./common/mutations.mjs"; import queries from "./common/queries.mjs"; import { - DEFAULT_LIMIT, API_VERSION, + DEFAULT_LIMIT, MAX_LIMIT, API_VERSION, } from "./common/constants.mjs"; +import utils from "./common/utils.mjs"; export default { type: "app", @@ -13,25 +14,177 @@ export default { propDefinitions: { blogId: { type: "string", - label: "Blogs", + label: "Blog ID", description: "The identifier of a blog", async options({ prevContext }) { + return this.getPropOptions({ + resourceFn: this.listBlogs, + resourceKeys: [ + "blogs", + ], + prevContext, + }); + }, + }, + articleId: { + type: "string", + label: "Article ID", + description: "The identifier of an article", + async options({ + prevContext, blogId, + }) { + return this.getPropOptions({ + resourceFn: this.listBlogArticles, + resourceKeys: [ + "blog", + "articles", + ], + variables: { + id: blogId, + }, + prevContext, + }); + }, + }, + productId: { + type: "string", + label: "Product ID", + description: "The identifier of a product", + async options({ prevContext }) { + return this.getPropOptions({ + resourceFn: this.listProducts, + resourceKeys: [ + "products", + ], + prevContext, + }); + }, + }, + productVariantId: { + type: "string", + label: "Product Variant ID", + description: "The identifier of a product variant", + async options({ + prevContext, productId, + }) { + return this.getPropOptions({ + resourceFn: this.listProductVariants, + resourceKeys: [ + "productVariants", + ], + variables: { + query: `product_id:${utils.getIdFromGid(productId)}`, + }, + prevContext, + }); + }, + }, + collectionId: { + type: "string", + label: "Collection ID", + description: "The identifier of a collection", + async options({ prevContext }) { + return this.getPropOptions({ + resourceFn: this.listCollections, + resourceKeys: [ + "collections", + ], + prevContext, + }); + }, + }, + pageId: { + type: "string", + label: "Page ID", + description: "The identifier of a page", + async options({ prevContext }) { + return this.getPropOptions({ + resourceFn: this.listPages, + resourceKeys: [ + "pages", + ], + prevContext, + }); + }, + }, + metaobjectType: { + type: "string", + label: "Type", + description: "The metaobject type", + async options({ prevContext }) { + return this.getPropOptions({ + resourceFn: this.listMetaobjectDefinitions, + resourceKeys: [ + "metaobjectDefinitions", + ], + labelKey: "name", + prevContext, + }); + }, + }, + metaobjectId: { + type: "string", + label: "Metaobject ID", + description: "The metaobject to update", + async options({ + prevContext, type, + }) { + const { metaobjectDefinitions: { nodes } } = await this.listMetaobjectDefinitions({ + first: MAX_LIMIT, + }); + const { type: typeName } = nodes.find(({ id }) => id === type); + return this.getPropOptions({ + resourceFn: this.listMetaobjects, + resourceKeys: [ + "metaobjects", + ], + labelKey: "displayName", + variables: { + type: typeName, + }, + prevContext, + }); + }, + }, + locationId: { + type: "string", + label: "Location ID", + description: "The ID of the location that the inventory level belongs to. Options will display the name of the Location ID", + async options({ prevContext }) { + return this.getPropOptions({ + resourceFn: this.listLocations, + resourceKeys: [ + "locations", + ], + labelKey: "name", + prevContext, + }); + }, + }, + inventoryItemId: { + type: "string", + label: "Inventory Item ID", + description: "The ID of the inventory item associated with the inventory level. There is a 1:1 relationship between a product variant and an inventory item. Each product variant includes the ID of its related inventory item. To view a list of Inventory Items, choose a product using the field above", + async options({ + prevContext, productId, + }) { const variables = { + query: `product_id:${utils.getIdFromGid(productId)}`, first: DEFAULT_LIMIT, }; if (prevContext?.after) { variables.after = prevContext.after; } - const { - blogs: { + let { + productVariants: { nodes, pageInfo, }, - } = await this.listBlogs(variables); + } = await this.listProductVariants(variables); return { options: nodes?.map(({ - id: value, title: label, + inventoryItem, title: label, }) => ({ - value, + value: inventoryItem.id, label, })), context: { @@ -40,6 +193,57 @@ export default { }; }, }, + productOptionIds: { + type: "string[]", + label: "Options", + description: "The product options to apply to the product variant", + async options({ productId }) { + const { product: { options } } = await this.getProduct({ + id: productId, + first: MAX_LIMIT, + }); + const productOptions = []; + for (const option of options) { + for (const optionValue of option.optionValues) { + productOptions.push({ + value: optionValue.id, + label: `${option.name} - ${optionValue.name}`, + }); + } + } + return productOptions; + }, + }, + sortKey: { + type: "string", + label: "Sort Key", + description: "The key to sort the results by", + optional: true, + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of results to return", + default: 100, + optional: true, + }, + reverse: { + type: "boolean", + label: "Sort Descending", + description: "Sort the results in descending order. Defaults to ascending.", + optional: true, + }, + tags: { + type: "string[]", + label: "Tags", + description: "An array of tags to add", + }, + metafields: { + type: "string[]", + label: "Metafields", + description: "An array of objects, each one representing a metafield. If adding a new metafield, the object should contain `key`, `value`, `type`, and `namespace`. Example: `{{ [{ \"key\": \"new\", \"value\": \"newvalue\", \"type\": \"single_line_text_field\", \"namespace\": \"global\" }] }}`. To update an existing metafield, use the `id` and `value`. Example: `{{ [{ \"id\": \"28408051400984\", \"value\": \"updatedvalue\" }] }}`", + optional: true, + }, }, methods: { getShopId() { @@ -113,6 +317,33 @@ export default { } }, retryOpts); }, + async getPropOptions({ + resourceFn, resourceKeys = [], variables, valueKey = "id", labelKey = "title", prevContext, + }) { + variables = { + ...variables, + first: DEFAULT_LIMIT, + }; + if (prevContext?.after) { + variables.after = prevContext.after; + } + let results = await resourceFn(variables); + for (const key of resourceKeys) { + results = results[key]; + } + const { + nodes, pageInfo, + } = results; + return { + options: nodes?.map((node) => ({ + value: node[valueKey], + label: node[labelKey], + })), + context: { + after: pageInfo?.endCursor, + }, + }; + }, createWebhook(variables) { return this._makeGraphQlRequest(mutations.CREATE_WEBHOOK, variables); }, @@ -121,8 +352,11 @@ export default { id: webhookId, }); }, - getBlog(variables) { - return this._makeGraphQlRequest(queries.GET_BLOG, variables); + listAbandonedCheckouts(variables) { + return this._makeGraphQlRequest(queries.LIST_ABANDONED_CHECKOUTS, variables); + }, + listBlogArticles(variables) { + return this._makeGraphQlRequest(queries.LIST_BLOG_ARTICLES, variables); }, listBlogs(variables) { return this._makeGraphQlRequest(queries.LIST_BLOGS, variables); @@ -133,5 +367,129 @@ export default { listProducts(variables) { return this._makeGraphQlRequest(queries.LIST_PRODUCTS, variables); }, + listCollections(variables) { + return this._makeGraphQlRequest(queries.LIST_COLLECTIONS, variables); + }, + listProductVariants(variables) { + return this._makeGraphQlRequest(queries.LIST_PRODUCT_VARIANTS, variables); + }, + listMetaobjects(variables) { + return this._makeGraphQlRequest(queries.LIST_METAOBJECTS, variables); + }, + listMetaobjectDefinitions(variables) { + return this._makeGraphQlRequest(queries.LIST_METAOBJECT_DEFINITIONS, variables); + }, + listLocations(variables) { + return this._makeGraphQlRequest(queries.LIST_LOCATIONS, variables); + }, + getProduct(variables) { + return this._makeGraphQlRequest(queries.GET_PRODUCT, variables); + }, + getProductVariant(variables) { + return this._makeGraphQlRequest(queries.GET_PRODUCT_VARIANT, variables); + }, + getCollection(variables) { + return this._makeGraphQlRequest(queries.GET_COLLECTION, variables); + }, + getBlog(variables) { + return this._makeGraphQlRequest(queries.GET_BLOG, variables); + }, + getPage(variables) { + return this._makeGraphQlRequest(queries.GET_PAGE, variables); + }, + getArticle(variables) { + return this._makeGraphQlRequest(queries.GET_ARTICLE, variables); + }, + addTags(variables) { + return this._makeGraphQlRequest(mutations.ADD_TAGS, variables); + }, + addProductsToCollection(variables) { + return this._makeGraphQlRequest(mutations.ADD_PRODUCTS_TO_COLLECTION, variables); + }, + createArticle(variables) { + return this._makeGraphQlRequest(mutations.CREATE_ARTICLE, variables); + }, + createBlog(variables) { + return this._makeGraphQlRequest(mutations.CREATE_BLOG, variables); + }, + createCollection(variables) { + return this._makeGraphQlRequest(mutations.CREATE_COLLECTION, variables); + }, + createPage(variables) { + return this._makeGraphQlRequest(mutations.CREATE_PAGE, variables); + }, + createProduct(variables) { + return this._makeGraphQlRequest(mutations.CREATE_PRODUCT, variables); + }, + createProductVariants(variables) { + return this._makeGraphQlRequest(mutations.CREATE_PRODUCT_VARIANTS, variables); + }, + createMetafield(variables) { + return this._makeGraphQlRequest(mutations.CREATE_METAFIELD, variables); + }, + updateMetafield(variables) { + return this._makeGraphQlRequest(mutations.UPDATE_METAFIELD, variables); + }, + updateProduct(variables) { + return this._makeGraphQlRequest(mutations.UPDATE_PRODUCT, variables); + }, + updateArticle(variables) { + return this._makeGraphQlRequest(mutations.UPDATE_ARTICLE, variables); + }, + updatePage(variables) { + return this._makeGraphQlRequest(mutations.UPDATE_PAGE, variables); + }, + updateInventoryLevel(variables) { + return this._makeGraphQlRequest(mutations.UPDATE_INVENTORY_LEVEL, variables); + }, + updateProductVariant(variables) { + return this._makeGraphQlRequest(mutations.UPDATE_PRODUCT_VARIANT, variables); + }, + deleteArticle(variables) { + return this._makeGraphQlRequest(mutations.DELETE_ARTICLE, variables); + }, + deleteBlog(variables) { + return this._makeGraphQlRequest(mutations.DELETE_BLOG, variables); + }, + deletePage(variables) { + return this._makeGraphQlRequest(mutations.DELETE_PAGE, variables); + }, + deleteMetafield(variables) { + return this._makeGraphQlRequest(mutations.DELETE_METAFIELD, variables); + }, + async *paginate({ + resourceFn, resourceKeys = [], variables = {}, max, + }) { + variables = { + ...variables, + first: MAX_LIMIT, + }; + let after, count = 0; + do { + let results = await resourceFn(variables); + for (const key of resourceKeys) { + results = results[key]; + } + const { + nodes, pageInfo, + } = results; + for (const node of nodes) { + yield node; + if (max && ++count >= max) { + return; + } + } + after = pageInfo.endCursor; + variables.after = after; + } while (after); + }, + async getPaginated(opts) { + let results = []; + const items = this.paginate(opts); + for await (const item of items) { + results.push(item); + } + return results; + }, }, }; diff --git a/components/shopify/sources/new-abandoned-cart/new-abandoned-cart.mjs b/components/shopify/sources/new-abandoned-cart/new-abandoned-cart.mjs new file mode 100644 index 0000000000000..a9aa10a856b6b --- /dev/null +++ b/components/shopify/sources/new-abandoned-cart/new-abandoned-cart.mjs @@ -0,0 +1,32 @@ +import common from "../common/polling.mjs"; +import { MAX_LIMIT } from "../../common/constants.mjs"; + +export default { + ...common, + key: "shopify-new-abandoned-cart", + name: "New Abandoned Cart", + description: "Emit new event each time a user abandons their cart.", + version: "0.0.20", + type: "source", + dedupe: "unique", + methods: { + ...common.methods, + async getResults() { + const { abandonedCheckouts: { nodes } } = await this.app.listAbandonedCheckouts({ + first: MAX_LIMIT, + reverse: true, + }); + return nodes; + }, + getTsField() { + return "createdAt"; + }, + generateMeta(checkout) { + return { + id: checkout.id, + summary: `New Abandoned Cart: ${checkout.id}`, + ts: Date.parse(checkout[this.getTsField()]), + }; + }, + }, +}; diff --git a/components/shopify/sources/new-article/new-article.mjs b/components/shopify/sources/new-article/new-article.mjs index 28999609153b7..7dac9adcaaf8c 100644 --- a/components/shopify/sources/new-article/new-article.mjs +++ b/components/shopify/sources/new-article/new-article.mjs @@ -21,7 +21,7 @@ export default { methods: { ...common.methods, async getResults() { - const { blog: { articles: { nodes } } } = await this.app.getBlog({ + const { blog: { articles: { nodes } } } = await this.app.listBlogArticles({ id: this.blogId, first: MAX_LIMIT, reverse: true, From 970252e1dd181bbb4a12af9333c7877bfb96f0d5 Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Wed, 5 Mar 2025 16:04:47 -0500 Subject: [PATCH 03/12] pnpm-lock.yaml --- pnpm-lock.yaml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e9868ffb25490..8941fba938b8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3596,8 +3596,7 @@ importers: components/dokan: {} - components/domain_group: - specifiers: {} + components/domain_group: {} components/domo: {} @@ -4842,8 +4841,7 @@ importers: components/geckoboard: {} - components/gem: - specifiers: {} + components/gem: {} components/gemini_public: dependencies: @@ -4920,8 +4918,7 @@ importers: components/getswift: {} - components/getty_images: - specifiers: {} + components/getty_images: {} components/ghost_org_admin_api: dependencies: @@ -34039,6 +34036,8 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) + transitivePeerDependencies: + - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: From c932b1dd320c9d732c6335686bac3457511aa469 Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Mon, 10 Mar 2025 14:57:51 -0400 Subject: [PATCH 04/12] updates per QA --- .../create-metafield/create-metafield.mjs | 57 +++++++++++-------- .../create-product-variant.mjs | 17 +++--- .../get-metaobjects/get-metaobjects.mjs | 3 +- .../search-product-variant.mjs | 21 ++++--- .../search-products/search-products.mjs | 5 +- .../update-product-variant.mjs | 18 +++--- components/shopify/common/constants.mjs | 8 +++ components/shopify/common/mutations.mjs | 18 +++++- components/shopify/shopify.app.mjs | 3 + .../collection-updated/collection-updated.mjs | 2 +- 10 files changed, 100 insertions(+), 52 deletions(-) diff --git a/components/shopify/actions/create-metafield/create-metafield.mjs b/components/shopify/actions/create-metafield/create-metafield.mjs index 24bb5b1b4b60b..11d415f811240 100644 --- a/components/shopify/actions/create-metafield/create-metafield.mjs +++ b/components/shopify/actions/create-metafield/create-metafield.mjs @@ -1,15 +1,28 @@ -import common from "../common/metafield-actions.mjs"; +import shopify from "../../shopify.app.mjs"; import constants from "../common/constants.mjs"; export default { - ...common, key: "shopify-create-metafield", name: "Create Metafield", - description: "Creates a metafield belonging to a resource. [See the documentation](https://shopify.dev/docs/api/admin-graphql/unstable/mutations/metafieldsSet)", + description: "Creates a metafield belonging to a resource. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/metafieldDefinitionCreate)", version: "0.0.11", type: "action", props: { - ...common.props, + shopify, + ownerResource: { + type: "string", + label: "Resource Type", + description: "Filter by the resource type on which the metafield is attached to", + options: constants.RESOURCE_TYPES.map((type) => ({ + ...type, + value: type.value.toUpperCase(), + })), + }, + name: { + type: "string", + label: "Name", + description: "The human-readable name for the metafield definition", + }, namespace: { type: "string", label: "Namespace", @@ -25,36 +38,30 @@ export default { label: "Type", description: "The type of data that the metafield stores in the `value` field. Refer to the list of [supported types](https://shopify.dev/apps/custom-data/metafields/types).", options: Object.keys(constants.METAFIELD_TYPES), - reloadProps: true, }, - }, - async additionalProps() { - const props = await this.getOwnerIdProp(this.ownerResource); - - if (this.type) { - props.value = { - type: "string", - label: "Value", - description: "The data to store in the metafield", - }; - } - - return props; + pin: { + type: "boolean", + label: "Pin", + description: "Whether to pin the metafield definition", + default: false, + optional: true, + }, }, async run({ $ }) { const response = await this.shopify.createMetafield({ - metafields: { + definition: { + ownerType: this.ownerResource, + name: this.name, + namespace: this.namespace, key: this.key, type: this.type, - value: this.value, - namespace: this.namespace, - ownerId: this.ownerId, + pin: this.pin, }, }); - if (response.metafieldsSet.userErrors.length > 0) { - throw new Error(response.metafieldsSet.userErrors[0].message); + if (response.metafieldDefinitionCreate.userErrors.length > 0) { + throw new Error(response.metafieldDefinitionCreate.userErrors[0].message); } - $.export("$summary", `Created metafield for object with ID ${this.ownerId}`); + $.export("$summary", `Created metafield ${this.name} for object type ${this.type}`); return response; }, }; diff --git a/components/shopify/actions/create-product-variant/create-product-variant.mjs b/components/shopify/actions/create-product-variant/create-product-variant.mjs index 0b60d1f53032d..c36277be23e9c 100644 --- a/components/shopify/actions/create-product-variant/create-product-variant.mjs +++ b/components/shopify/actions/create-product-variant/create-product-variant.mjs @@ -1,6 +1,8 @@ import shopify from "../../shopify.app.mjs"; import utils from "../../common/utils.mjs"; -import { MAX_LIMIT } from "../../common/constants.mjs"; +import { + MAX_LIMIT, WEIGHT_UNITS, +} from "../../common/constants.mjs"; import { ConfigurationError } from "@pipedream/platform"; export default { @@ -74,12 +76,7 @@ export default { label: "Weight Unit", description: "The unit of measurement that applies to the product variant's weight. If you don't specify a value for weight_unit, then the shop's default unit of measurement is applied.", optional: true, - options: [ - "g", - "kg", - "oz", - "lb", - ], + options: WEIGHT_UNITS, }, metafields: { propDefinition: [ @@ -112,6 +109,10 @@ export default { throw new ConfigurationError("Must enter LocationId to set the available quantity"); } + if ((this.weightUnit && !this.weight) || (!this.weightUnit && this.weight)) { + throw new ConfigurationError("Must enter both Weight and Weight Unit to set weight"); + } + const response = await this.shopify.createProductVariants({ productId: this.productId, variants: [ @@ -125,7 +126,7 @@ export default { measurement: (this.weightUnit || this.weight) && { weight: { unit: this.weightUnit, - value: this.weight, + value: +this.weight, }, }, }, diff --git a/components/shopify/actions/get-metaobjects/get-metaobjects.mjs b/components/shopify/actions/get-metaobjects/get-metaobjects.mjs index d1a62938d47fd..d2400215ed3d8 100644 --- a/components/shopify/actions/get-metaobjects/get-metaobjects.mjs +++ b/components/shopify/actions/get-metaobjects/get-metaobjects.mjs @@ -16,11 +16,12 @@ export default { shopify, "metaobjectType", ], + withLabel: true, }, }, async run({ $ }) { const response = await this.shopify.listMetaobjects({ - type: this.type, + type: this.type.label, first: MAX_LIMIT, }); diff --git a/components/shopify/actions/search-product-variant/search-product-variant.mjs b/components/shopify/actions/search-product-variant/search-product-variant.mjs index 4d18a08d7cdd8..9dd2c7cff8a33 100644 --- a/components/shopify/actions/search-product-variant/search-product-variant.mjs +++ b/components/shopify/actions/search-product-variant/search-product-variant.mjs @@ -110,13 +110,20 @@ export default { }); } - const title = response.productVariants - ? response.productVariants.nodes[0].title - : response.productVariant.title; - const id = response.productVariants - ? response.productVariants.nodes[0].id - : response.productVariant.id; - $.export("$summary", `Found product variant \`${title}\` with ID \`${id}\``); + const variant = response?.productVariants?.nodes?.length + ? response.productVariants.nodes[0] + : response?.productVariant + ? response.productVariant + : {}; + + const title = variant?.title; + const id = variant?.id; + + if (title && id) { + $.export("$summary", `Found product variant \`${title}\` with ID \`${id}\``); + } else { + $.export("$summary", "No product variant found"); + } return response; } catch (err) { if (!this.createIfNotFound) { diff --git a/components/shopify/actions/search-products/search-products.mjs b/components/shopify/actions/search-products/search-products.mjs index 62beb302157ae..cb430446d7549 100644 --- a/components/shopify/actions/search-products/search-products.mjs +++ b/components/shopify/actions/search-products/search-products.mjs @@ -29,6 +29,8 @@ export default { "productId", ], type: "string[]", + label: "Product IDs", + description: "Select multiple Product IDs or provide Product IDs as a JSON array. For example: `{{ [\"gid://shopify/Product/1\", \"gid://shopify/Product/2\"] }}`", optional: true, }, collectionId: { @@ -61,6 +63,7 @@ export default { shopify, "sortKey", ], + description: "The key to sort the results by. Keys `RELEVANCE` and `INVENTORY_TOTAL` not for use with `Collection ID`.", options: PRODUCT_SORT_KEY, }, reverse: { @@ -92,7 +95,7 @@ export default { const query = queryArray.length ? queryArray.join(" AND ") : undefined; - + console.log(query); let products = await this.shopify.getPaginated({ resourceFn: this.shopify.listProducts, resourceKeys: [ diff --git a/components/shopify/actions/update-product-variant/update-product-variant.mjs b/components/shopify/actions/update-product-variant/update-product-variant.mjs index 08c29207fccc2..13873077c9d72 100644 --- a/components/shopify/actions/update-product-variant/update-product-variant.mjs +++ b/components/shopify/actions/update-product-variant/update-product-variant.mjs @@ -1,6 +1,9 @@ import shopify from "../../shopify.app.mjs"; import utils from "../../common/utils.mjs"; -import { MAX_LIMIT } from "../../common/constants.mjs"; +import { + MAX_LIMIT, WEIGHT_UNITS, +} from "../../common/constants.mjs"; +import { ConfigurationError } from "@pipedream/platform"; export default { key: "shopify-update-product-variant", @@ -70,12 +73,7 @@ export default { label: "Weight Unit", description: "The unit of measurement that applies to the product variant's weight. If you don't specify a value for weight_unit, then the shop's default unit of measurement is applied.", optional: true, - options: [ - "g", - "kg", - "oz", - "lb", - ], + options: WEIGHT_UNITS, }, metafields: { propDefinition: [ @@ -104,6 +102,10 @@ export default { }, }, async run({ $ }) { + if ((this.weightUnit && !this.weight) || (!this.weightUnit && this.weight)) { + throw new ConfigurationError("Must enter both Weight and Weight Unit to set weight"); + } + const response = await this.shopify.updateProductVariant({ productId: this.productId, variants: [ @@ -118,7 +120,7 @@ export default { measurement: (this.weightUnit || this.weight) && { weight: { unit: this.weightUnit, - value: this.weight, + value: +this.weight, }, }, }, diff --git a/components/shopify/common/constants.mjs b/components/shopify/common/constants.mjs index ce45210ee0e70..6bdb264f6de9b 100644 --- a/components/shopify/common/constants.mjs +++ b/components/shopify/common/constants.mjs @@ -52,6 +52,13 @@ const COLLECTION_RULE_RELATIONS = [ "STARTS_WITH", ]; +const WEIGHT_UNITS = [ + "KILOGRAMS", + "GRAMS", + "POUNDS", + "OUNCES", +]; + const INVENTORY_ADJUSTMENT_REASONS = [ { value: "correction", @@ -131,5 +138,6 @@ export { COLLECTION_SORT_KEY, COLLECTION_RULE_COLUMNS, COLLECTION_RULE_RELATIONS, + WEIGHT_UNITS, INVENTORY_ADJUSTMENT_REASONS, }; diff --git a/components/shopify/common/mutations.mjs b/components/shopify/common/mutations.mjs index 3088df26f8734..16c99101e0a02 100644 --- a/components/shopify/common/mutations.mjs +++ b/components/shopify/common/mutations.mjs @@ -173,7 +173,7 @@ const CREATE_PRODUCT = ` } `; -const CREATE_METAFIELD = ` +const SET_METAFIELD = ` mutation MetafieldsSet($metafields: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $metafields) { metafields { @@ -191,6 +191,21 @@ const CREATE_METAFIELD = ` } `; +const CREATE_METAFIELD = ` + mutation CreateMetafieldDefinition($definition: MetafieldDefinitionInput!) { + metafieldDefinitionCreate(definition: $definition) { + createdDefinition { + id + name + } + userErrors { + field + message + } + } + } +`; + const CREATE_METAOBJECT = ` mutation metaobjectCreate($metaobject: MetaobjectCreateInput!) { metaobjectCreate(metaobject: $metaobject) { @@ -429,6 +444,7 @@ export default { CREATE_PAGE, CREATE_PRODUCT, CREATE_PRODUCT_VARIANTS, + SET_METAFIELD, CREATE_METAFIELD, CREATE_METAOBJECT, UPDATE_METAFIELD, diff --git a/components/shopify/shopify.app.mjs b/components/shopify/shopify.app.mjs index db8e45ca6c70f..22d4e01f7e1b9 100644 --- a/components/shopify/shopify.app.mjs +++ b/components/shopify/shopify.app.mjs @@ -427,6 +427,9 @@ export default { createMetafield(variables) { return this._makeGraphQlRequest(mutations.CREATE_METAFIELD, variables); }, + setMetafield(variables) { + return this._makeGraphQlRequest(mutations.SET_METAFIELD, variables); + }, updateMetafield(variables) { return this._makeGraphQlRequest(mutations.UPDATE_METAFIELD, variables); }, diff --git a/components/shopify/sources/collection-updated/collection-updated.mjs b/components/shopify/sources/collection-updated/collection-updated.mjs index 2aeb15eedd1f9..2cb16d123e55f 100644 --- a/components/shopify/sources/collection-updated/collection-updated.mjs +++ b/components/shopify/sources/collection-updated/collection-updated.mjs @@ -15,7 +15,7 @@ export default { return "COLLECTIONS_UPDATE"; }, generateMeta(collection) { - const ts = Date.parse(collection.updatedAt); + const ts = Date.parse(collection.updated_at); return { id: `${collection.id}${ts}`, summary: `Collection Updated: ${collection.title}`, From 1d2bd08224314410ecfd3a1293005e7f16be1e16 Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Mon, 10 Mar 2025 15:07:25 -0400 Subject: [PATCH 05/12] remove console.log --- components/shopify/actions/search-products/search-products.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/shopify/actions/search-products/search-products.mjs b/components/shopify/actions/search-products/search-products.mjs index cb430446d7549..e79a724ebd528 100644 --- a/components/shopify/actions/search-products/search-products.mjs +++ b/components/shopify/actions/search-products/search-products.mjs @@ -95,7 +95,7 @@ export default { const query = queryArray.length ? queryArray.join(" AND ") : undefined; - console.log(query); + let products = await this.shopify.getPaginated({ resourceFn: this.shopify.listProducts, resourceKeys: [ From c0bfd44a782cf92f495bad3e90fa15856d77e331 Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Mon, 10 Mar 2025 15:12:33 -0400 Subject: [PATCH 06/12] fix typo --- components/shopify/actions/common/constants.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/shopify/actions/common/constants.mjs b/components/shopify/actions/common/constants.mjs index 639fc61472752..e4bac97bda1d6 100644 --- a/components/shopify/actions/common/constants.mjs +++ b/components/shopify/actions/common/constants.mjs @@ -59,7 +59,7 @@ const METAFIELD_TYPES = { "list.number_integer": "string[]", "list.number_decimal": "string[]", "list.page_reference": "string[]", - "list.product_rererence": "string[]", + "list.product_reference": "string[]", "list.rating": "string[]", "list.single_line_text_field": "string[]", "list.url": "string[]", From 80e0527572f510ce8f6528437b530c8eb5474e8c Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Mon, 10 Mar 2025 15:14:06 -0400 Subject: [PATCH 07/12] remove duplicate mutation --- components/shopify/common/mutations.mjs | 19 ------------------- components/shopify/shopify.app.mjs | 3 --- 2 files changed, 22 deletions(-) diff --git a/components/shopify/common/mutations.mjs b/components/shopify/common/mutations.mjs index 16c99101e0a02..6190d6e2b0404 100644 --- a/components/shopify/common/mutations.mjs +++ b/components/shopify/common/mutations.mjs @@ -173,24 +173,6 @@ const CREATE_PRODUCT = ` } `; -const SET_METAFIELD = ` - mutation MetafieldsSet($metafields: [MetafieldsSetInput!]!) { - metafieldsSet(metafields: $metafields) { - metafields { - key - namespace - value - createdAt - updatedAt - } - userErrors { - field - message - } - } - } -`; - const CREATE_METAFIELD = ` mutation CreateMetafieldDefinition($definition: MetafieldDefinitionInput!) { metafieldDefinitionCreate(definition: $definition) { @@ -444,7 +426,6 @@ export default { CREATE_PAGE, CREATE_PRODUCT, CREATE_PRODUCT_VARIANTS, - SET_METAFIELD, CREATE_METAFIELD, CREATE_METAOBJECT, UPDATE_METAFIELD, diff --git a/components/shopify/shopify.app.mjs b/components/shopify/shopify.app.mjs index 22d4e01f7e1b9..db8e45ca6c70f 100644 --- a/components/shopify/shopify.app.mjs +++ b/components/shopify/shopify.app.mjs @@ -427,9 +427,6 @@ export default { createMetafield(variables) { return this._makeGraphQlRequest(mutations.CREATE_METAFIELD, variables); }, - setMetafield(variables) { - return this._makeGraphQlRequest(mutations.SET_METAFIELD, variables); - }, updateMetafield(variables) { return this._makeGraphQlRequest(mutations.UPDATE_METAFIELD, variables); }, From 0c6c374f4a95b9fdf4e9bbeb51b52fe18051efa4 Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Tue, 11 Mar 2025 13:27:21 -0400 Subject: [PATCH 08/12] updates --- .../search-product-variant.mjs | 12 ++++++------ components/shopify/shopify.app.mjs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/shopify/actions/search-product-variant/search-product-variant.mjs b/components/shopify/actions/search-product-variant/search-product-variant.mjs index 9dd2c7cff8a33..ae03c9ce51a4a 100644 --- a/components/shopify/actions/search-product-variant/search-product-variant.mjs +++ b/components/shopify/actions/search-product-variant/search-product-variant.mjs @@ -96,8 +96,8 @@ export default { throw new ConfigurationError("Required field missing: Fill in `Product Variant ID` or `Title`"); } + let response; try { - let response; if (this.productVariantId) { response = await this.shopify.getProductVariant({ id: this.productVariantId, @@ -122,15 +122,15 @@ export default { if (title && id) { $.export("$summary", `Found product variant \`${title}\` with ID \`${id}\``); } else { - $.export("$summary", "No product variant found"); + throw new Error("No product variant found"); } - return response; } catch (err) { if (!this.createIfNotFound) { - throw err; + $.export("$summary", "No product variant found"); + return; } - let response = await this.shopify.createProductVariants({ + response = await this.shopify.createProductVariants({ productId: this.productId, variants: [ { @@ -144,7 +144,7 @@ export default { throw new Error(response.productVariantsBulkCreate.userErrors[0].message); } $.export("$summary", `Created new product variant \`${response.productVariantsBulkCreate.productVariants.title}\` with ID \`${response.productVariantsBulkCreate.productVariants.id}\``); - return response; } + return response; }, }; diff --git a/components/shopify/shopify.app.mjs b/components/shopify/shopify.app.mjs index db8e45ca6c70f..fc261c0952cbe 100644 --- a/components/shopify/shopify.app.mjs +++ b/components/shopify/shopify.app.mjs @@ -117,7 +117,7 @@ export default { resourceKeys: [ "metaobjectDefinitions", ], - labelKey: "name", + labelKey: "type", prevContext, }); }, From c5e00aa8408e5e4440b29604a5ac3ab5083ccd94 Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Tue, 11 Mar 2025 13:38:59 -0400 Subject: [PATCH 09/12] pnpm-lock.yaml --- pnpm-lock.yaml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5cea28a530c6..4601e556743d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5368,8 +5368,7 @@ importers: specifier: ^3.0.3 version: 3.0.3 - components/google_marketplace: - specifiers: {} + components/google_marketplace: {} components/google_meet: dependencies: @@ -14836,7 +14835,7 @@ importers: version: 3.1.7 ts-jest: specifier: ^29.2.5 - version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.7.2) + version: 29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.7.2) tsup: specifier: ^8.3.6 version: 8.3.6(@microsoft/api-extractor@7.47.12(@types/node@20.17.6))(jiti@1.21.6)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.6.1) @@ -46984,7 +46983,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.7.2): + ts-jest@29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.7.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 @@ -46998,10 +46997,10 @@ snapshots: typescript: 5.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.26.0 + '@babel/core': 8.0.0-alpha.13 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.26.0) + babel-jest: 29.7.0(@babel/core@8.0.0-alpha.13) esbuild: 0.24.2 ts-jest@29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.6.3): From c0ed016ac8922abec2be5ef4d0ab384b4bf94d15 Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Tue, 11 Mar 2025 13:40:52 -0400 Subject: [PATCH 10/12] pnpm-lock.yaml --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4601e556743d1..f6a3747e45265 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14835,7 +14835,7 @@ importers: version: 3.1.7 ts-jest: specifier: ^29.2.5 - version: 29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.7.2) + version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.7.2) tsup: specifier: ^8.3.6 version: 8.3.6(@microsoft/api-extractor@7.47.12(@types/node@20.17.6))(jiti@1.21.6)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.6.1) @@ -46983,7 +46983,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.7.2): + ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.7.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 @@ -46997,10 +46997,10 @@ snapshots: typescript: 5.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 8.0.0-alpha.13 + '@babel/core': 7.26.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@8.0.0-alpha.13) + babel-jest: 29.7.0(@babel/core@7.26.0) esbuild: 0.24.2 ts-jest@29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.6.3): From 21027eb2bdaddf66407bcc9b6ac0e39b313b207b Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Tue, 11 Mar 2025 13:44:31 -0400 Subject: [PATCH 11/12] package.json --- components/shopify/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/shopify/package.json b/components/shopify/package.json index ecd3be4e51914..cca078f6bd712 100644 --- a/components/shopify/package.json +++ b/components/shopify/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/shopify", - "version": "0.6.9", + "version": "0.7.0", "description": "Pipedream Shopify Components", "main": "shopify.app.mjs", "keywords": [ From f7f8775fa3821f6cc9d2dbd950eaee965a0d14bd Mon Sep 17 00:00:00 2001 From: michelle0927 Date: Tue, 11 Mar 2025 13:48:54 -0400 Subject: [PATCH 12/12] bulk-import version --- components/shopify/actions/bulk-import/bulk-import.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/shopify/actions/bulk-import/bulk-import.mjs b/components/shopify/actions/bulk-import/bulk-import.mjs index 8110ef2dc061d..ff563b205a926 100644 --- a/components/shopify/actions/bulk-import/bulk-import.mjs +++ b/components/shopify/actions/bulk-import/bulk-import.mjs @@ -7,7 +7,7 @@ export default { key: "shopify-bulk-import", name: "Bulk Import", description: "Execute bulk mutations by uploading a JSONL file containing mutation variables. [See the documentation](https://shopify.dev/docs/api/admin-graphql/latest/mutations/bulkoperationrunmutation)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { shopify,