├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json ├── src └── migrator.js └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | SOURCE_SHOPIFY_STORE=your-source-store 2 | SOURCE_SHOPIFY_API_KEY=GENERATED_API_KEY 3 | SOURCE_SHOPIFY_API_PASSWORD=GENERATED_API_PASSWORD 4 | DESTINATION_SHOPIFY_STORE=your-dest-store 5 | DESTINATION_SHOPIFY_API_KEY=GENERATED_API_KEY 6 | DESTINATION_SHOPIFY_API_PASSWORD=GENERATED_API_PASSWORD -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | /data -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 213a Studio Créatif, Inc 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shopify Store Duplicator 2 | This tool makes it easy to duplicate a store's content onto another, either to spin up a staging env or to simply test stuff out without risking anything in production. 3 | 4 | ## What it supports 5 | With this tool, you can sync/duplicate the following resources: 6 | - Products (and their variants, including images, alt tags and images being tied to specific variants as well as metafields) 7 | - Smart Collections (Along with its image, filters and metafields) 8 | - Custom Collections (Along with its image, products (mapped using their handle) and metafields) 9 | - Shop-level metafields 10 | - Blogs (along with their metafields) 11 | - Articles (mapped to a blog of the same handle. Comes along with their metafields) 12 | - Pages (along with their metafields) 13 | 14 | ## Setting it all up 15 | You'll first need to [create 2 private apps](https://help.shopify.com/en/manual/apps/private-apps#generate-credentials-from-the-shopify-admin "Read Shopify's documentation on how to create a private app"). One needs read access on the source store, the other needs read/write on the destination store. 16 | 17 | Here are the access copes that will be required: 18 | - Store content like articles, blogs, comments, pages, and redirects 19 | - Products, variants and collections 20 | 21 | Then, you will need to create a `.env` file (copy it from `.env.example`) and fill it out with the right api information you will have gathered from the private apps process. 22 | 23 | ## Usage 24 | 25 | By default, simply running `yarn start` will validate that each store is able to be read from. It will not do anything unless specifically told to using flags. 26 | 27 | ### Available flags 28 | 29 | - `--products` copies over products (and variants, images & metafields) 30 | - `--delete-products` will override pre-existing products. 31 | - `--collections` copies over collections 32 | - `--delete-collections` will override pre-existing collections. 33 | - `--pages` copies over pages (along with metafields) 34 | - `--delete-pages` will override pre-existing pages. 35 | - `--blogs` copies over blogs 36 | - `--delete-blogs` will override pre-existing articles. 37 | - `--articles` copies over articles 38 | - `--delete-articles` will override pre-existing articles. 39 | - `--metafields` copies over shop metafields 40 | - `--delete-metafields` will override pre-existing shop metafields. 41 | - `--all` will sync everything. 42 | 43 | 44 | ### Examples 45 | 46 | - Copying only product: run `yarn start --products` 47 | - Copying only pages: run `yarn start --pages` 48 | - Copying only articles: run `yarn start --articles` 49 | - Copying products & articles: run `yarn start --products --articles` 50 | - Copying products, pages & articles: run `yarn start --products --articles --pages` 51 | - Just copy everything you can: run `yarn start --all` 52 | 53 | ## Issues and bugs 54 | Create a new issue, or issue a new PR on this repo if you've found an issue and would like it fixed. 55 | 56 | ## License 57 | MIT. Do whatever you like with this stuff ✌️. 58 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Shopify = require('shopify-api-node'); 2 | require('dotenv').config() 3 | const { program } = require('commander'); 4 | const Migrator = require('./src/migrator.js') 5 | 6 | program.version('1.0.0'); 7 | program 8 | .option('--all', 'Migrate everything') 9 | .option('--metafields', 'Run the migration for shop\'s metafields') 10 | .option('--delete-metafields', 'Delete(replace) shop metafields with the same namespace and key') 11 | .option('--products', 'Run the migration for products') 12 | .option('--delete-products', 'Delete(replace) products with the same handles') 13 | .option('--collections', 'Run the migration for collections') 14 | .option('--delete-collections', 'Delete(replace) collections with the same handles') 15 | .option('--articles', 'Run the migration for articles') 16 | .option('--delete-articles', 'Delete(replace) articles with the same handles') 17 | .option('--blogs', 'Run the migration for blogs') 18 | .option('--delete-blogs', 'Delete(replace) with the same handles') 19 | .option('--collections', 'Run the migration for collections') 20 | .option('--pages', 'Run the migration for pages') 21 | .option('--delete-pages', 'Delete(replace) pages with the same handles') 22 | .option('--save-data', 'Save every source data as json files under a `data/{type}` folder. For example, `data/products/123456.json`') 23 | .option('-v, --verbosity', 'Verbosity level. Defaults to 4, as talkative as my MIL.') 24 | 25 | program.parse(process.argv); 26 | 27 | const start = async () => { 28 | const sourceStore = { 29 | shopName: process.env.SOURCE_SHOPIFY_STORE, 30 | accessToken: process.env.SOURCE_SHOPIFY_API_PASSWORD, 31 | apiVersion: '2023-10' 32 | } 33 | const destinationStore = { 34 | shopName: process.env.DESTINATION_SHOPIFY_STORE, 35 | accessToken: process.env.DESTINATION_SHOPIFY_API_PASSWORD, 36 | apiVersion: '2023-10' 37 | } 38 | const migration = new Migrator(sourceStore, destinationStore, (program.verbosity && program.verbosity * 1)|| 4, program.saveData) 39 | try { 40 | await migration.testConnection() 41 | migration.log('Store configuration looks correct.') 42 | } catch (e) { 43 | migration.error('Could not validate proper store setup', e.message) 44 | process.exit() 45 | } 46 | try { 47 | if (program.all || program.pages) { 48 | await migration.migratePages(program.deletePages) 49 | } 50 | if (program.all || program.blogs) { 51 | await migration.migrateBlogs(program.deleteBlogs) 52 | } 53 | if (program.all || program.articles) { 54 | await migration.migrateArticles(program.deleteArticles) 55 | } 56 | if (program.all || program.products) { 57 | await migration.migrateProducts(program.deleteProducts) 58 | } 59 | if (program.all || program.collections) { 60 | await migration.migrateSmartCollections(program.deleteCollections) 61 | await migration.migrateCustomCollections(program.deleteCollections) 62 | } 63 | if (program.all || program.metafields) { 64 | await migration.migrateMetafields(program.deleteMetafields) 65 | } 66 | } catch (e) { 67 | console.error(e); 68 | console.log(e.response) 69 | } 70 | } 71 | start() -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storeduplicator", 3 | "version": "1.0.0", 4 | "description": "Shopify Store Duplicator. Duplicates Products, pages, blogs & articles", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "213a Creative Studio ", 11 | "license": "MIT", 12 | "dependencies": { 13 | "commander": "^5.1.0", 14 | "dotenv": "^8.2.0", 15 | "shopify-api-node": "^3.3.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/migrator.js: -------------------------------------------------------------------------------- 1 | const Shopify = require('shopify-api-node'); 2 | const fs = require('fs'); 3 | class Migrator { 4 | constructor(sourceStore, destinationStore, verbosity = 4, saveData) { 5 | this.config = { 6 | source: sourceStore, 7 | destination: destinationStore 8 | } 9 | this.saveData = !!saveData 10 | this.verbosity = verbosity 11 | this.source = new Shopify(sourceStore); 12 | this.destination = new Shopify(destinationStore); 13 | if (this.saveData) { 14 | const types = ['products', 'pages', 'metafields', 'collections', 'articles', 'blogs'] 15 | types.forEach(type => { 16 | const dir = `data/${type}` 17 | if (fs.existsSync(dir)) { 18 | return 19 | } 20 | try { 21 | fs.mkdirSync(dir, { recursive: true}) 22 | } catch (e) { 23 | this.error(`Could not adequately create folder ${dir}`) 24 | } 25 | }) 26 | } 27 | this.requiredScopes = { 28 | source: [ // For source, we only need read access, but won't discriminate against write access. 29 | ['read_content', 'write_content'], // Blogs, Articles, Pages 30 | ['read_products', 'write_products'], // Products, Variants, Collections 31 | ], 32 | destination: [ // Destionation obviously requires write access 33 | ['write_content'], // Blogs, Articles, Pages 34 | ['write_products'], // Products, Variants, Collections 35 | ] 36 | }; 37 | } 38 | info() { 39 | if (this.verbosity > 3) { 40 | console.info.apply(this, arguments) 41 | } 42 | } 43 | log() { 44 | if (this.verbosity > 2) { 45 | console.log.apply(this, arguments) 46 | } 47 | } 48 | warn() { 49 | if (this.verbosity > 1) { 50 | console.warn.apply(this, arguments) 51 | } 52 | } 53 | error() { 54 | console.error.apply(this, arguments) 55 | } 56 | async testConnection() { 57 | const sourceScopes = await this.source.accessScope.list() 58 | // console.log('xx') 59 | const destinationScopes = await this.destination.accessScope.list() 60 | this.requiredScopes.source.forEach((scopes) => { 61 | const scopeFound = !!sourceScopes.find(scope => scopes.indexOf(scope.handle) !== -1) 62 | if (!scopeFound) { 63 | const message = `Source store does not have proper access scope: ${scopes[0]}` 64 | this.error(message) 65 | throw new Error(message) 66 | } 67 | }) 68 | this.requiredScopes.destination.forEach((scopes) => { 69 | const scopeFound = !!destinationScopes.find(scope => scopes.indexOf(scope.handle) !== -1) 70 | if (!scopeFound) { 71 | const message = `Destination store does not have proper access scope: ${scopes[0]}` 72 | this.error(message) 73 | throw new Error(message) 74 | } 75 | }) 76 | } 77 | async asyncForEach(array, callback, concurrency = 1) { 78 | const promises = []; 79 | 80 | for (let index = 0; index < array.length; index++) { 81 | promises.push(async () => await callback(array[index], index, array)); 82 | } 83 | for (let i = 0; i < promises.length; i += concurrency) { 84 | const chunk = promises.slice(i, i + concurrency); 85 | await Promise.all(chunk.map(f => f())); 86 | } 87 | } 88 | async _getMetafields(resource = null, id = null) { 89 | let params = { limit: 250 } 90 | if (resource && id) { 91 | params.metafield = { 92 | owner_resource: resource, 93 | owner_id: id 94 | } 95 | } 96 | const metafields = [] 97 | do { 98 | const resourceMetafields = await this.source.metafield.list(params) 99 | resourceMetafields.forEach(m => metafields.push(m)) 100 | params = resourceMetafields.nextPageParameters; 101 | } while (params !== undefined); 102 | return metafields 103 | } 104 | async _migratePage(page) { 105 | this.info(`[PAGE ${page.id}] ${page.handle} started...`) 106 | const metafields = await this._getMetafields('page', page.id) 107 | this.info(`[PAGE ${page.id}] has ${metafields.length} metafields...`) 108 | const newPage = await this.destination.page.create(page) 109 | this.info(`[PAGE ${page.id}] duplicated. New id is ${newPage.id}.`) 110 | await this.asyncForEach(metafields, async (metafield) => { 111 | delete metafield.id 112 | metafield.owner_resource = 'page' 113 | metafield.owner_id = newPage.id 114 | this.info(`[PAGE ${page.id}] Metafield ${metafield.namespace}.${metafield.key} started`) 115 | await this.destination.metafield.create(metafield) 116 | this.info(`[PAGE ${page.id}] Metafield ${metafield.namespace}.${metafield.key} done!`) 117 | }) 118 | } 119 | 120 | async _migrateBlog(blog) { 121 | this.info(`[BLOG ${blog.id}] ${blog.handle} started...`) 122 | const metafields = await this._getMetafields('blog', blog.id) 123 | this.info(`[BLOG ${blog.id}] has ${metafields.length} metafields...`) 124 | const newBlog = await this.destination.blog.create(blog) 125 | this.info(`[BLOG ${blog.id}] duplicated. New id is ${newBlog.id}.`) 126 | await this.asyncForEach(metafields, async (metafield) => { 127 | delete metafield.id 128 | metafield.owner_resource = 'blog' 129 | metafield.owner_id = newBlog.id 130 | this.info(`[BLOG ${blog.id}] Metafield ${metafield.namespace}.${metafield.key} started`) 131 | await this.destination.metafield.create(metafield) 132 | this.info(`[BLOG ${blog.id}] Metafield ${metafield.namespace}.${metafield.key} done!`) 133 | }) 134 | } 135 | 136 | async _migrateSmartCollection(collection) { 137 | this.info(`[SMART COLLECTION ${collection.id}] ${collection.handle} started...`) 138 | const metafields = await this._getMetafields('smart_collection', collection.id) 139 | this.info(`[SMART COLLECTION ${collection.id}] has ${metafields.length} metafields...`) 140 | delete collection.publications 141 | const newCollection = await this.destination.smartCollection.create(collection) 142 | this.info(`[SMART COLLECTION ${collection.id}] duplicated. New id is ${newCollection.id}.`) 143 | await this.asyncForEach(metafields, async (metafield) => { 144 | delete metafield.id 145 | metafield.owner_resource = 'smart_collection' 146 | metafield.owner_id = newCollection.id 147 | this.info(`[SMART COLLECTION ${collection.id}] Metafield ${metafield.namespace}.${metafield.key} started`) 148 | await this.destination.metafield.create(metafield) 149 | this.info(`[SMART COLLECTION ${collection.id}] Metafield ${metafield.namespace}.${metafield.key} done!`) 150 | }) 151 | } 152 | 153 | async _migrateCustomCollection(collection, productMap = {}) { 154 | this.info(`[CUSTOM COLLECTION ${collection.id}] ${collection.handle} started...`) 155 | const metafields = await this._getMetafields('custom_collection', collection.id) 156 | const products = [] 157 | let params = { limit: 250 } 158 | do { 159 | const sourceProducts = await this.source.collection.products(collection.id, params) 160 | sourceProducts.forEach(p => products.push(p)) 161 | params = sourceProducts.nextPageParameters; 162 | } while (params !== undefined); 163 | this.info(`[CUSTOM COLLECTION ${collection.id}] has ${products.length} products...`) 164 | this.info(`[CUSTOM COLLECTION ${collection.id}] has ${metafields.length} metafields...`) 165 | delete collection.publications 166 | collection.collects = products.map(p => productMap[p.id] || null).filter(p => p).map((p) => { 167 | return { 168 | product_id: p 169 | } 170 | }) 171 | const newCollection = await this.destination.customCollection.create(collection) 172 | this.info(`[CUSTOM COLLECTION ${collection.id}] duplicated. New id is ${newCollection.id}.`) 173 | await this.asyncForEach(metafields, async (metafield) => { 174 | delete metafield.id 175 | metafield.owner_resource = 'custom_collection' 176 | metafield.owner_id = newCollection.id 177 | this.info(`[CUSTOM COLLECTION ${collection.id}] Metafield ${metafield.namespace}.${metafield.key} started`) 178 | await this.destination.metafield.create(metafield) 179 | this.info(`[CUSTOM COLLECTION ${collection.id}] Metafield ${metafield.namespace}.${metafield.key} done!`) 180 | }) 181 | } 182 | 183 | async _migrateProduct(product) { 184 | this.info(`[PRODUCT ${product.id}] ${product.handle} started...`) 185 | const metafields = await (await this._getMetafields('product', product.id)).filter(m => m.namespace.indexOf('app--') !== 0) 186 | this.info(`[PRODUCT ${product.id}] has ${metafields.length} metafields...`) 187 | product.metafields = metafields.filter(v => v && v.value && v.value.indexOf && v.value.indexOf('gid://shopify/') === -1).filter(v => v.namespace.indexOf('app--') !== 0); 188 | const images = (product.images || []).map(v => v) 189 | delete product.images; 190 | (product.variants || []).forEach((variant, i) => { 191 | if (variant.compare_at_price && (variant.compare_at_price * 1) <= (variant.price * 1)) { 192 | delete product.variants[i].compare_at_price 193 | } 194 | /*reset fulfillment services to shopify*/ 195 | delete variant.fulfillment_service 196 | variant.inventory_management = 'shopify' 197 | delete product.variants[i].image_id 198 | }) 199 | if (product.metafields) { 200 | product.metafields = product.metafields.filter(m => m.namespace.indexOf('app--') !== 0) 201 | } 202 | const newProduct = await this.destination.product.create(product) 203 | this.info(`[PRODUCT ${product.id}] duplicated. New id is ${newProduct.id}.`) 204 | this.info(`[PRODUCT ${product.id}] Creating ${images && images.length || 0} images...`) 205 | if (images && images.length) { 206 | const newImages = images.map((image) => { 207 | image.product_id = newProduct.id 208 | image.variant_ids = image.variant_ids.map((oldId) => { 209 | const oldVariant = product.variants.find(v => v.id === oldId) 210 | const newVariant = newProduct.variants.find(v => v.title === oldVariant.title) 211 | return newVariant.id 212 | }) 213 | return image 214 | }) 215 | await this.asyncForEach(newImages, async (image) => { 216 | try { 217 | await this.destination.productImage.create(newProduct.id, image) 218 | } catch (e) { 219 | this.warn(e.message, 'Retrying.') 220 | await this.destination.productImage.create(newProduct.id, image) 221 | } 222 | }) 223 | } 224 | } 225 | 226 | async _migrateArticle(blogId, article) { 227 | this.info(`[ARTICLE ${article.id}] ${article.handle} started...`) 228 | const metafields = await this._getMetafields('article', article.id) 229 | this.info(`[ARTICLE ${article.id}] has ${metafields.length} metafields...`) 230 | delete article.user_id 231 | delete article.created_at 232 | delete article.deleted_at 233 | article.published_at = article.created_at 234 | article.blog_id = blogId 235 | const newArticle = await this.destination.article.create(blogId, article) 236 | this.info(`[ARTICLE ${article.id}] duplicated. New id is ${newArticle.id}.`) 237 | await this.asyncForEach(metafields, async (metafield) => { 238 | delete metafield.id 239 | metafield.owner_resource = 'article' 240 | metafield.owner_id = newArticle.id 241 | this.info(`[ARTICLE ${article.id}] Metafield ${metafield.namespace}.${metafield.key} started`) 242 | await this.destination.metafield.create(metafield) 243 | this.info(`[ARTICLE ${article.id}] Metafield ${metafield.namespace}.${metafield.key} done!`) 244 | }) 245 | } 246 | 247 | async migratePages(deleteFirst = false, skipExisting = true) { 248 | this.log('Page migration started...') 249 | let params = { limit: 250 } 250 | const destinationPages = {} 251 | do { 252 | const pages = await this.destination.page.list(params) 253 | await this.asyncForEach(pages, async (page) => { 254 | destinationPages[page.handle] = page.id 255 | }) 256 | params = pages.nextPageParameters; 257 | } while (params !== undefined); 258 | params = { limit: 250 } 259 | do { 260 | const pages = await this.source.page.list(params) 261 | await this.asyncForEach(pages, async (page) => { 262 | this.saveData && fs.writeFileSync(`data/pages/${page.id}.json`, JSON.stringify(page)); 263 | 264 | if (destinationPages[page.handle] && deleteFirst) { 265 | this.log(`[DUPLICATE PAGE] Deleting destination page ${page.handle}`) 266 | await this.destination.page.delete(destinationPages[page.handle]) 267 | } 268 | if (destinationPages[page.handle] && skipExisting && !deleteFirst) { 269 | this.log(`[EXISTING PAGE] Skipping ${page.handle}`) 270 | return 271 | } 272 | await this._migratePage(page) 273 | }) 274 | params = pages.nextPageParameters; 275 | } while (params !== undefined); 276 | this.log('Page migration finished!') 277 | } 278 | 279 | async migrateProducts(deleteFirst = false, skipExisting = true) { 280 | this.log('Product migration started...') 281 | let params = { limit: 250 } 282 | const destinationProducts = {} 283 | do { 284 | const products = await this.destination.product.list(params) 285 | await this.asyncForEach(products, async (product) => { 286 | destinationProducts[product.handle] = product.id 287 | }) 288 | params = products.nextPageParameters; 289 | } while (params !== undefined); 290 | params = { limit: 250 } 291 | do { 292 | const products = await this.source.product.list(params) 293 | await this.asyncForEach(products, async (product) => { 294 | if (destinationProducts[product.handle] && deleteFirst) { 295 | this.log(`[DUPLICATE PRODUCT] Deleting destination product ${product.handle}`) 296 | await this.destination.product.delete(destinationProducts[product.handle]) 297 | } 298 | if (destinationProducts[product.handle] && skipExisting && !deleteFirst) { 299 | this.log(`[EXISTING PRODUCT] Skipping ${product.handle}`) 300 | return 301 | } 302 | try { 303 | this.saveData && fs.writeFileSync(`data/products/${product.id}.json`, JSON.stringify(product)); 304 | await this._migrateProduct(product) 305 | } catch (e) { 306 | this.error(`[PRODUCT] ${product.handle} FAILED TO BE CREATED PROPERLY.`,e, e.response, product.metafields) 307 | } 308 | }, 15) 309 | params = products.nextPageParameters; 310 | } while (params !== undefined); 311 | this.log('Product migration finished!') 312 | } 313 | async migrateMetafields(deleteFirst = false, skipExisting = true) { 314 | this.log('Shop Metafields migration started...') 315 | const sourceMetafields = [] 316 | const destinationMetafields = [] 317 | let params = { limit: 250 } 318 | do { 319 | const metafields = await this.source.metafield.list(params) 320 | metafields.forEach(m => sourceMetafields.push(m)) 321 | params = metafields.nextPageParameters; 322 | } while (params !== undefined); 323 | 324 | params = { limit: 250 } 325 | do { 326 | const metafields = await this.destination.metafield.list(params) 327 | metafields.forEach(m => destinationMetafields.push(m)) 328 | params = metafields.nextPageParameters; 329 | } while (params !== undefined); 330 | await this.asyncForEach(sourceMetafields, async (metafield) => { 331 | this.saveData && fs.writeFileSync(`data/metafields/${metafield.id}.json`, JSON.stringify(metafield)); 332 | const destinationMetafield = destinationMetafields.find(f => f.key === metafield.key && f.namespace === metafield.namespace) 333 | if (destinationMetafield && deleteFirst) { 334 | this.log(`[DUPLICATE METAFIELD] Deleting destination metafield ${metafield.namespace}.${metafield.key}`) 335 | await this.destination.metafield.delete(destinationMetafield.id) 336 | } 337 | if (destinationMetafield && skipExisting && !deleteFirst) { 338 | this.log(`[EXISTING METAFIELD] Skipping ${metafield.namespace}.${metafield.key}`) 339 | return 340 | } 341 | try { 342 | delete metafield.owner_id 343 | delete metafield.owner_resource 344 | await this.destination.metafield.create(metafield) 345 | } catch (e) { 346 | this.error(`[METAFIELD] ${metafield.namespace}.${metafield.key} FAILED TO BE CREATED PROPERLY.`) 347 | } 348 | }) 349 | this.log('Shop Metafields migration finished!') 350 | } 351 | 352 | async migrateSmartCollections(deleteFirst = false, skipExisting = true) { 353 | this.log('Smart Collections migration started...') 354 | let params = { limit: 250 } 355 | const destinationCollections = {} 356 | do { 357 | const collections = await this.destination.smartCollection.list(params) 358 | await this.asyncForEach(collections, async (collection) => { 359 | destinationCollections[collection.handle] = collection.id 360 | }) 361 | params = collections.nextPageParameters; 362 | } while (params !== undefined); 363 | params = { limit: 250 } 364 | do { 365 | const collections = await this.source.smartCollection.list(params) 366 | await this.asyncForEach(collections, async (collection) => { 367 | this.saveData && fs.writeFileSync(`data/collections/${collection.id}.json`, JSON.stringify(collection)); 368 | if (destinationCollections[collection.handle] && deleteFirst) { 369 | this.log(`[DUPLICATE COLLECTION] Deleting destination collection ${collection.handle}`) 370 | await this.destination.smartCollection.delete(destinationCollections[collection.handle]) 371 | } 372 | if (destinationCollections[collection.handle] && skipExisting && !deleteFirst) { 373 | this.log(`[EXISTING COLLECTION] Skipping ${collection.handle}`) 374 | return 375 | } 376 | try { 377 | await this._migrateSmartCollection(collection) 378 | } catch (e) { 379 | this.error(`[COLLECTION] ${collection.handle} FAILED TO BE CREATED PROPERLY.`) 380 | } 381 | }) 382 | params = collections.nextPageParameters; 383 | } while (params !== undefined); 384 | this.log('Smart Collection migration finished!') 385 | } 386 | 387 | async migrateCustomCollections(deleteFirst = false, skipExisting = true) { 388 | this.log('Custom Collections migration started...') 389 | let params = { limit: 250 } 390 | const destinationCollections = {} 391 | const productMap = {} 392 | const sourceProducts = [] 393 | const destinationProducts = [] 394 | 395 | do { 396 | const products = await this.source.product.list(params) 397 | products.forEach(p => sourceProducts.push(p)) 398 | params = products.nextPageParameters; 399 | } while (params !== undefined); 400 | 401 | params = { limit: 250 } 402 | do { 403 | const products = await this.destination.product.list(params) 404 | products.forEach(p => destinationProducts.push(p)) 405 | params = products.nextPageParameters; 406 | } while (params !== undefined); 407 | 408 | destinationProducts.forEach(p => { 409 | const sourceProduct = sourceProducts.find(s => s.handle === p.handle) 410 | if (sourceProduct) { 411 | productMap[sourceProduct.id] = p.id 412 | } 413 | }) 414 | 415 | params = { limit: 250 } 416 | do { 417 | const collections = await this.destination.smartCollection.list(params) 418 | await this.asyncForEach(collections, async (collection) => { 419 | destinationCollections[collection.handle] = collection.id 420 | }) 421 | params = collections.nextPageParameters; 422 | } while (params !== undefined); 423 | params = { limit: 250 } 424 | 425 | 426 | do { 427 | const collections = await this.destination.customCollection.list(params) 428 | await this.asyncForEach(collections, async (collection) => { 429 | destinationCollections[collection.handle] = collection.id 430 | }) 431 | params = collections.nextPageParameters; 432 | } while (params !== undefined); 433 | params = { limit: 250 } 434 | do { 435 | const collections = await this.source.customCollection.list(params) 436 | await this.asyncForEach(collections, async (collection) => { 437 | this.saveData && fs.writeFileSync(`data/collections/${collection.id}.json`, JSON.stringify(collection)); 438 | if (destinationCollections[collection.handle] && deleteFirst) { 439 | this.log(`[DUPLICATE COLLECTION] Deleting destination collection ${collection.handle}`) 440 | await this.destination.customCollection.delete(destinationCollections[collection.handle]) 441 | } 442 | if (destinationCollections[collection.handle] && skipExisting && !deleteFirst) { 443 | this.log(`[EXISTING COLLECTION] Skipping ${collection.handle}`) 444 | return 445 | } 446 | try { 447 | await this._migrateCustomCollection(collection, productMap) 448 | } catch (e) { 449 | this.error(`[COLLECTION] ${collection.handle} FAILED TO BE CREATED PROPERLY.`, e) 450 | } 451 | }) 452 | params = collections.nextPageParameters; 453 | } while (params !== undefined); 454 | this.log('Custom Collection migration finished!') 455 | } 456 | 457 | async migrateBlogs(deleteFirst = false, skipExisting = true) { 458 | this.log('Blog migration started...') 459 | let params = { limit: 250 } 460 | const destinationBlogs = {} 461 | do { 462 | const blogs = await this.destination.blog.list(params) 463 | await this.asyncForEach(blogs, async (blog) => { 464 | destinationBlogs[blog.handle] = blog.id 465 | }) 466 | params = blogs.nextPageParameters; 467 | } while (params !== undefined); 468 | params = { limit: 250 } 469 | do { 470 | const blogs = await this.source.blog.list(params) 471 | await this.asyncForEach(blogs, async (blog) => { 472 | this.saveData && fs.writeFileSync(`data/blogs/${blog.id}.json`, JSON.stringify(blog)); 473 | 474 | if (destinationBlogs[blog.handle] && deleteFirst) { 475 | this.log(`[DUPLICATE blog] Deleting destination blog ${blog.handle}`) 476 | await this.destination.blog.delete(destinationBlogs[blog.handle]) 477 | } 478 | if (destinationBlogs[blog.handle] && skipExisting && !deleteFirst) { 479 | this.log(`[EXISTING BLOG] Skipping ${blog.handle}`) 480 | return 481 | } 482 | await this._migrateBlog(blog) 483 | }) 484 | params = blogs.nextPageParameters; 485 | } while (params !== undefined); 486 | this.log('Blog migration finished!') 487 | } 488 | 489 | async migrateArticles(deleteFirst = false, skipExisting = true) { 490 | const blogParams = {limit: 250} 491 | const sourceBlogs = await this.source.blog.list(blogParams) 492 | const destinationBlogs = await this.destination.blog.list(blogParams) 493 | const matchingBlogs = sourceBlogs.filter((sourceBlog) => { 494 | return destinationBlogs.find(destinationBlog => destinationBlog.handle === sourceBlog.handle) 495 | }) 496 | this.log(`Migrating articles for ${matchingBlogs.length} matching blog(s): ${matchingBlogs.map(b => b.handle).join(', ')}`) 497 | 498 | this.asyncForEach(matchingBlogs, async (blog) => { 499 | const destinationBlog = destinationBlogs.find(b => b.handle === blog.handle) 500 | let params = { limit: 250 } 501 | const destinationArticles = {} 502 | do { 503 | const articles = await this.destination.article.list(destinationBlog.id, params) 504 | await this.asyncForEach(articles, async (article) => { 505 | destinationArticles[article.handle] = article.id 506 | }) 507 | params = articles.nextPageParameters; 508 | } while (params !== undefined); 509 | 510 | params = { limit: 250 } 511 | do { 512 | const articles = await this.source.article.list(blog.id, params) 513 | await this.asyncForEach(articles, async (article) => { 514 | this.saveData && fs.writeFileSync(`data/articles/${article.id}.json`, JSON.stringify(article)); 515 | if (destinationArticles[article.handle] && deleteFirst) { 516 | this.log(`[DUPLICATE article] Deleting destination article ${article.handle}`) 517 | await this.destination.article.delete(destinationBlog.id, destinationArticles[article.handle]) 518 | } 519 | if (destinationArticles[article.handle] && skipExisting && !deleteFirst) { 520 | this.log(`[EXISTING ARTICLE] Skipping ${article.handle}`) 521 | return 522 | } 523 | await this._migrateArticle(destinationBlog.id, article) 524 | }) 525 | params = articles.nextPageParameters; 526 | } while (params !== undefined); 527 | }) 528 | } 529 | } 530 | 531 | module.exports = Migrator 532 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@sindresorhus/is@^2.0.0": 6 | version "2.1.1" 7 | resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-2.1.1.tgz#ceff6a28a5b4867c2dd4a1ba513de278ccbe8bb1" 8 | integrity sha512-/aPsuoj/1Dw/kzhkgz+ES6TxG0zfTMGLwuK2ZG00k/iJzYHTLCE8mVU8EPqEOp/lmxPoq1C1C9RYToRKb2KEfg== 9 | 10 | "@szmarczak/http-timer@^4.0.0": 11 | version "4.0.5" 12 | resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152" 13 | integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ== 14 | dependencies: 15 | defer-to-connect "^2.0.0" 16 | 17 | "@types/cacheable-request@^6.0.1": 18 | version "6.0.1" 19 | resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" 20 | integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ== 21 | dependencies: 22 | "@types/http-cache-semantics" "*" 23 | "@types/keyv" "*" 24 | "@types/node" "*" 25 | "@types/responselike" "*" 26 | 27 | "@types/http-cache-semantics@*": 28 | version "4.0.0" 29 | resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" 30 | integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== 31 | 32 | "@types/keyv@*", "@types/keyv@^3.1.1": 33 | version "3.1.1" 34 | resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" 35 | integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== 36 | dependencies: 37 | "@types/node" "*" 38 | 39 | "@types/node@*": 40 | version "13.13.4" 41 | resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.4.tgz#1581d6c16e3d4803eb079c87d4ac893ee7501c2c" 42 | integrity sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA== 43 | 44 | "@types/responselike@*": 45 | version "1.0.0" 46 | resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" 47 | integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== 48 | dependencies: 49 | "@types/node" "*" 50 | 51 | cacheable-lookup@^2.0.0: 52 | version "2.0.1" 53 | resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-2.0.1.tgz#87be64a18b925234875e10a9bb1ebca4adce6b38" 54 | integrity sha512-EMMbsiOTcdngM/K6gV/OxF2x0t07+vMOWxZNSCRQMjO2MY2nhZQ6OYhOOpyQrbhqsgtvKGI7hcq6xjnA92USjg== 55 | dependencies: 56 | "@types/keyv" "^3.1.1" 57 | keyv "^4.0.0" 58 | 59 | cacheable-request@^7.0.1: 60 | version "7.0.1" 61 | resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58" 62 | integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw== 63 | dependencies: 64 | clone-response "^1.0.2" 65 | get-stream "^5.1.0" 66 | http-cache-semantics "^4.0.0" 67 | keyv "^4.0.0" 68 | lowercase-keys "^2.0.0" 69 | normalize-url "^4.1.0" 70 | responselike "^2.0.0" 71 | 72 | clone-response@^1.0.2: 73 | version "1.0.2" 74 | resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" 75 | integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= 76 | dependencies: 77 | mimic-response "^1.0.0" 78 | 79 | commander@^5.1.0: 80 | version "5.1.0" 81 | resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" 82 | integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== 83 | 84 | decompress-response@^5.0.0: 85 | version "5.0.0" 86 | resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-5.0.0.tgz#7849396e80e3d1eba8cb2f75ef4930f76461cb0f" 87 | integrity sha512-TLZWWybuxWgoW7Lykv+gq9xvzOsUjQ9tF09Tj6NSTYGMTCHNXzrPnD6Hi+TgZq19PyTAGH4Ll/NIM/eTGglnMw== 88 | dependencies: 89 | mimic-response "^2.0.0" 90 | 91 | defer-to-connect@^2.0.0: 92 | version "2.0.0" 93 | resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.0.tgz#83d6b199db041593ac84d781b5222308ccf4c2c1" 94 | integrity sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg== 95 | 96 | dotenv@^8.2.0: 97 | version "8.2.0" 98 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" 99 | integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== 100 | 101 | duplexer3@^0.1.4: 102 | version "0.1.4" 103 | resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" 104 | integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= 105 | 106 | end-of-stream@^1.1.0: 107 | version "1.4.4" 108 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" 109 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== 110 | dependencies: 111 | once "^1.4.0" 112 | 113 | get-stream@^5.0.0, get-stream@^5.1.0: 114 | version "5.1.0" 115 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9" 116 | integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw== 117 | dependencies: 118 | pump "^3.0.0" 119 | 120 | got@^10.1.0: 121 | version "10.7.0" 122 | resolved "https://registry.yarnpkg.com/got/-/got-10.7.0.tgz#62889dbcd6cca32cd6a154cc2d0c6895121d091f" 123 | integrity sha512-aWTDeNw9g+XqEZNcTjMMZSy7B7yE9toWOFYip7ofFTLleJhvZwUxxTxkTpKvF+p1SAA4VHmuEy7PiHTHyq8tJg== 124 | dependencies: 125 | "@sindresorhus/is" "^2.0.0" 126 | "@szmarczak/http-timer" "^4.0.0" 127 | "@types/cacheable-request" "^6.0.1" 128 | cacheable-lookup "^2.0.0" 129 | cacheable-request "^7.0.1" 130 | decompress-response "^5.0.0" 131 | duplexer3 "^0.1.4" 132 | get-stream "^5.0.0" 133 | lowercase-keys "^2.0.0" 134 | mimic-response "^2.1.0" 135 | p-cancelable "^2.0.0" 136 | p-event "^4.0.0" 137 | responselike "^2.0.0" 138 | to-readable-stream "^2.0.0" 139 | type-fest "^0.10.0" 140 | 141 | http-cache-semantics@^4.0.0: 142 | version "4.1.0" 143 | resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" 144 | integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== 145 | 146 | json-buffer@3.0.1: 147 | version "3.0.1" 148 | resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" 149 | integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== 150 | 151 | keyv@^4.0.0: 152 | version "4.0.0" 153 | resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.0.tgz#2d1dab694926b2d427e4c74804a10850be44c12f" 154 | integrity sha512-U7ioE8AimvRVLfw4LffyOIRhL2xVgmE8T22L6i0BucSnBUyv4w+I7VN/zVZwRKHOI6ZRUcdMdWHQ8KSUvGpEog== 155 | dependencies: 156 | json-buffer "3.0.1" 157 | 158 | lodash@^4.17.10: 159 | version "4.17.21" 160 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 161 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 162 | 163 | lowercase-keys@^2.0.0: 164 | version "2.0.0" 165 | resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" 166 | integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== 167 | 168 | mimic-response@^1.0.0: 169 | version "1.0.1" 170 | resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" 171 | integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== 172 | 173 | mimic-response@^2.0.0, mimic-response@^2.1.0: 174 | version "2.1.0" 175 | resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" 176 | integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== 177 | 178 | normalize-url@^4.1.0: 179 | version "4.5.1" 180 | resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" 181 | integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== 182 | 183 | once@^1.3.1, once@^1.4.0: 184 | version "1.4.0" 185 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 186 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 187 | dependencies: 188 | wrappy "1" 189 | 190 | p-cancelable@^2.0.0: 191 | version "2.0.0" 192 | resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e" 193 | integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg== 194 | 195 | p-event@^4.0.0: 196 | version "4.1.0" 197 | resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.1.0.tgz#e92bb866d7e8e5b732293b1c8269d38e9982bf8e" 198 | integrity sha512-4vAd06GCsgflX4wHN1JqrMzBh/8QZ4j+rzp0cd2scXRwuBEv+QR3wrVA5aLhWDLw4y2WgDKvzWF3CCLmVM1UgA== 199 | dependencies: 200 | p-timeout "^2.0.1" 201 | 202 | p-finally@^1.0.0: 203 | version "1.0.0" 204 | resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" 205 | integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= 206 | 207 | p-timeout@^2.0.1: 208 | version "2.0.1" 209 | resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" 210 | integrity sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA== 211 | dependencies: 212 | p-finally "^1.0.0" 213 | 214 | pump@^3.0.0: 215 | version "3.0.0" 216 | resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" 217 | integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== 218 | dependencies: 219 | end-of-stream "^1.1.0" 220 | once "^1.3.1" 221 | 222 | qs@^6.5.2: 223 | version "6.9.3" 224 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" 225 | integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== 226 | 227 | responselike@^2.0.0: 228 | version "2.0.0" 229 | resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723" 230 | integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw== 231 | dependencies: 232 | lowercase-keys "^2.0.0" 233 | 234 | shopify-api-node@^3.3.1: 235 | version "3.3.1" 236 | resolved "https://registry.yarnpkg.com/shopify-api-node/-/shopify-api-node-3.3.1.tgz#7aee8ecd54f23b482907232bfc4f76cf20c15997" 237 | integrity sha512-wHTeNwaYOlc6OhjxHs9hPqcKfve3rMikW3/tmJ8rOfLHvgUNjEkZ9aSt++vef1DgsNTusRJgecPRpbvTN4jlvQ== 238 | dependencies: 239 | got "^10.1.0" 240 | lodash "^4.17.10" 241 | qs "^6.5.2" 242 | stopcock "^1.0.0" 243 | 244 | stopcock@^1.0.0: 245 | version "1.1.0" 246 | resolved "https://registry.yarnpkg.com/stopcock/-/stopcock-1.1.0.tgz#e0c875d98b819c0baa0a0edf3bcba2d7dc643175" 247 | integrity sha512-SNTAH55X9Ra5uE1JIxiPT3WwZiNMTcdCup+7qWOULNVUqiqi62qctNJ+x1R4znNudtkyu8LGc7Ok6Ldt+8N5iQ== 248 | 249 | to-readable-stream@^2.0.0: 250 | version "2.1.0" 251 | resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-2.1.0.tgz#82880316121bea662cdc226adb30addb50cb06e8" 252 | integrity sha512-o3Qa6DGg1CEXshSdvWNX2sN4QHqg03SPq7U6jPXRahlQdl5dK8oXjkU/2/sGrnOZKeGV1zLSO8qPwyKklPPE7w== 253 | 254 | type-fest@^0.10.0: 255 | version "0.10.0" 256 | resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.10.0.tgz#7f06b2b9fbfc581068d1341ffabd0349ceafc642" 257 | integrity sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw== 258 | 259 | wrappy@1: 260 | version "1.0.2" 261 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 262 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 263 | --------------------------------------------------------------------------------