├── .eslintignore ├── .gitignore ├── README.md ├── utils └── convertCosmicObjToAlgoliaObj │ ├── parseDate │ └── index.js │ ├── parseText │ └── index.js │ ├── parseFile │ └── index.js │ ├── parseNumber │ └── index.js │ ├── parseMarkdown │ └── index.js │ ├── parseCheckBoxes │ └── index.js │ ├── parseHtmlTextArea │ └── index.js │ ├── parseObject │ └── index.js │ ├── parsePlainTextArea │ └── index.js │ ├── parseRadioButtons │ └── index.js │ ├── parseSwitch │ └── index.js │ ├── parseSelectDropdown │ └── index.js │ ├── parseObjects │ └── index.js │ └── index.js ├── .eslintrc.js ├── vercel.json ├── package.json └── server.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | now.json 3 | .env 4 | 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Installation 2 | 3 | Run `npm i && npm start` in terminal. 4 | -------------------------------------------------------------------------------- /utils/convertCosmicObjToAlgoliaObj/parseDate/index.js: -------------------------------------------------------------------------------- 1 | module.exports = date => date && date.value; 2 | -------------------------------------------------------------------------------- /utils/convertCosmicObjToAlgoliaObj/parseText/index.js: -------------------------------------------------------------------------------- 1 | module.exports = text => text && text.value; 2 | -------------------------------------------------------------------------------- /utils/convertCosmicObjToAlgoliaObj/parseFile/index.js: -------------------------------------------------------------------------------- 1 | module.exports = file => file && file.imgix_url; 2 | -------------------------------------------------------------------------------- /utils/convertCosmicObjToAlgoliaObj/parseNumber/index.js: -------------------------------------------------------------------------------- 1 | module.exports = number => number && number.value; 2 | -------------------------------------------------------------------------------- /utils/convertCosmicObjToAlgoliaObj/parseMarkdown/index.js: -------------------------------------------------------------------------------- 1 | module.exports = markdown => markdown && markdown.value; 2 | -------------------------------------------------------------------------------- /utils/convertCosmicObjToAlgoliaObj/parseCheckBoxes/index.js: -------------------------------------------------------------------------------- 1 | module.exports = checkBoxes => checkBoxes && checkBoxes.value; 2 | -------------------------------------------------------------------------------- /utils/convertCosmicObjToAlgoliaObj/parseHtmlTextArea/index.js: -------------------------------------------------------------------------------- 1 | module.exports = htmlTextArea => htmlTextArea && htmlTextArea.value; 2 | -------------------------------------------------------------------------------- /utils/convertCosmicObjToAlgoliaObj/parseObject/index.js: -------------------------------------------------------------------------------- 1 | module.exports = object => object && object.object && object.object.title; 2 | -------------------------------------------------------------------------------- /utils/convertCosmicObjToAlgoliaObj/parsePlainTextArea/index.js: -------------------------------------------------------------------------------- 1 | module.exports = plainTextArea => plainTextArea && plainTextArea.value; 2 | -------------------------------------------------------------------------------- /utils/convertCosmicObjToAlgoliaObj/parseRadioButtons/index.js: -------------------------------------------------------------------------------- 1 | module.exports = radioButtons => radioButtons && radioButtons.value; 2 | -------------------------------------------------------------------------------- /utils/convertCosmicObjToAlgoliaObj/parseSwitch/index.js: -------------------------------------------------------------------------------- 1 | module.exports = switchMetafield => switchMetafield && switchMetafield.value; 2 | -------------------------------------------------------------------------------- /utils/convertCosmicObjToAlgoliaObj/parseSelectDropdown/index.js: -------------------------------------------------------------------------------- 1 | module.exports = selectDropdown => selectDropdown && selectDropdown.value; 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | commonjs: true, 5 | }, 6 | parser: 'babel-eslint', 7 | extends: 'airbnb-base', 8 | rules: { 9 | 'camelcase': ['off'], 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /utils/convertCosmicObjToAlgoliaObj/parseObjects/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (objects) => { 2 | if (!objects || !objects.objects) return undefined; 3 | return objects.objects.map(object => object.title).filter(title => !!title); 4 | }; 5 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "server.js", 6 | "use": "@now/node" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "server.js" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cosmic-algolia-extension-app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "eslint": "eslint . --ext .js,.jsx", 8 | "eslint:watch": "esw . --ext .js,.jsx --watch", 9 | "start": "NODE_ENV=production node server.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/cosmicjs/algolia-webhook-listener.git" 14 | }, 15 | "author": "Chris Overstreet", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/cosmicjs/algolia-webhook-listener/issues" 19 | }, 20 | "homepage": "https://github.com/cosmicjs/algolia-webhook-listener#readme", 21 | "dependencies": { 22 | "algoliasearch": "^3.29.0", 23 | "body-parser": "^1.18.3", 24 | "corser": "^2.0.1", 25 | "cosmicjs": "^3.2.12", 26 | "dotenv": "^6.0.0", 27 | "express": "^4.16.3" 28 | }, 29 | "devDependencies": { 30 | "babel-eslint": "^8.2.5", 31 | "eslint": "^4.19.1", 32 | "eslint-config-airbnb-base": "^13.0.0", 33 | "eslint-plugin-import": "^2.13.0", 34 | "eslint-watch": "^4.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /utils/convertCosmicObjToAlgoliaObj/index.js: -------------------------------------------------------------------------------- 1 | const parseDate = require('./parseDate'); 2 | const parseFile = require('./parseFile'); 3 | const parseHtmlTextArea = require('./parseHtmlTextArea'); 4 | const parseObject = require('./parseObject'); 5 | const parseObjects = require('./parseObjects'); 6 | const parseRadioButtons = require('./parseRadioButtons'); 7 | const parseSelectDropdown = require('./parseSelectDropdown'); 8 | const parsePlainTextArea = require('./parsePlainTextArea'); 9 | const parseText = require('./parseText'); 10 | const parseSwitch = require('./parseSwitch'); 11 | const parseNumber = require('./parseNumber'); 12 | const parseCheckBoxes = require('./parseCheckBoxes'); 13 | const parseMarkdown = require('./parseMarkdown'); 14 | 15 | module.exports = (cosmicObject) => { 16 | const { 17 | _id, 18 | content, 19 | created_at, 20 | metafields, 21 | published_at, 22 | slug, 23 | title, 24 | type_slug, 25 | locale, 26 | } = cosmicObject; 27 | const algoliaObject = { 28 | objectID: _id, 29 | content, 30 | created_at: new Date(created_at).valueOf(), 31 | published_at: new Date(published_at).valueOf(), 32 | slug, 33 | title, 34 | type_slug, 35 | locale, 36 | }; 37 | 38 | metafields.forEach((metafield) => { 39 | switch (metafield.type) { 40 | case 'date': 41 | algoliaObject[metafield.key] = parseDate(metafield); 42 | break; 43 | case 'file': 44 | algoliaObject[metafield.key] = parseFile(metafield); 45 | break; 46 | case 'html-textarea': 47 | algoliaObject[metafield.key] = parseHtmlTextArea(metafield); 48 | break; 49 | case 'radio-buttons': 50 | algoliaObject[metafield.key] = parseRadioButtons(metafield); 51 | break; 52 | case 'select-dropdown': 53 | algoliaObject[metafield.key] = parseSelectDropdown(metafield); 54 | break; 55 | case 'text': 56 | algoliaObject[metafield.key] = parseText(metafield); 57 | break; 58 | case 'textarea': 59 | algoliaObject[metafield.key] = parsePlainTextArea(metafield); 60 | break; 61 | case 'object': 62 | algoliaObject[metafield.key] = parseObject(metafield); 63 | break; 64 | case 'objects': 65 | algoliaObject[metafield.key] = parseObjects(metafield); 66 | break; 67 | case 'switch': 68 | algoliaObject[metafield.key] = parseSwitch(metafield); 69 | break; 70 | case 'number': 71 | algoliaObject[metafield.key] = parseNumber(metafield); 72 | break; 73 | case 'check-boxes': 74 | algoliaObject[metafield.key] = parseCheckBoxes(metafield); 75 | break; 76 | case 'markdown': 77 | algoliaObject[metafield.key] = parseMarkdown(metafield); 78 | break; 79 | default: 80 | if (process.env.NODE_ENV !== 'production') { 81 | // eslint-disable-next-line no-console 82 | console.log(`Metafield type, ${metafield.type}, not implemented yet.`); 83 | } 84 | } 85 | }); 86 | 87 | return algoliaObject; 88 | }; 89 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const corser = require('corser'); 4 | const algoliasearch = require('algoliasearch'); 5 | const cosmic = require('cosmicjs'); 6 | const convertCosmicObjToAlgoliaObj = require('./utils/convertCosmicObjToAlgoliaObj'); 7 | 8 | const Cosmic = cosmic(); 9 | const port = parseInt(process.env.PORT, 10) || 3000; 10 | 11 | const server = express(); 12 | 13 | server.use(corser.create()); 14 | server.use(bodyParser.json()); 15 | 16 | server.post('/api/addBucketSlug', async (req, res) => { 17 | try { 18 | const { id, slug } = req.body; 19 | if (!id || !slug) { 20 | throw new Error('Must provide bucket id and slug'); 21 | } 22 | 23 | const searchBucket = Cosmic.bucket({ slug: 'algolia-search' }); 24 | 25 | await searchBucket.addObject({ 26 | content: slug, 27 | slug: id, 28 | title: id, 29 | type_slug: 'bucket-slugs', 30 | }); 31 | 32 | return res.status(200).send(); 33 | } catch (e) { 34 | console.error(e); // eslint-disable-line no-console 35 | return res.status(400).json({ error: e.message }); 36 | } 37 | }); 38 | 39 | server.post('/api/removeBucketSlug/:id', async (req, res) => { 40 | try { 41 | const { id } = req.params; 42 | if (!id) { 43 | throw new Error('No id provided'); 44 | } 45 | 46 | const searchBucket = Cosmic.bucket({ slug: 'algolia-search' }); 47 | await searchBucket.deleteObject({ slug: id }).catch(() => undefined); 48 | return res.status(200).send(); 49 | } catch (e) { 50 | console.error(e); // eslint-disable-line no-console 51 | return res.status(400).json({ error: e.message }); 52 | } 53 | }); 54 | 55 | server.post('/api/create', async (req, res) => { 56 | try { 57 | const { data } = req.body; 58 | const { read_key } = req.query; 59 | const { bucket, type_slug } = data; 60 | 61 | const algoliaObject = convertCosmicObjToAlgoliaObj(data); 62 | const searchBucket = Cosmic.bucket({ slug: 'algolia-search' }); 63 | const bucketSlugRes = await searchBucket.getObject({ slug: bucket }); 64 | const projectBucketSlug = bucketSlugRes.object.content; 65 | 66 | // Fetch algolia application id & admin api key 67 | const projectBucket = Cosmic.bucket({ slug: projectBucketSlug, read_key }); 68 | const getKeysRes = await Promise.all([ 69 | projectBucket.getObject({ slug: 'algolia-info-application-id' }).catch(() => undefined), 70 | projectBucket.getObject({ slug: 'algolia-info-admin-api-key' }).catch(() => undefined), 71 | ]); 72 | const applicationId = getKeysRes[0] && getKeysRes[0].object && getKeysRes[0].object.content; 73 | const adminApiKey = getKeysRes[1] && getKeysRes[1].object && getKeysRes[1].object.content; 74 | 75 | const client = algoliasearch(applicationId, adminApiKey); 76 | const index = client.initIndex(type_slug); 77 | const addRes = await index.addObject(algoliaObject); 78 | const { taskID } = addRes; 79 | await index.waitTask(taskID); 80 | res.status(200).send(); 81 | } catch (e) { 82 | console.error(e); // eslint-disable-line no-console 83 | res.status(200).send(); 84 | } 85 | }); 86 | 87 | server.post('/api/edit', async (req, res) => { 88 | try { 89 | const { data } = req.body; 90 | const { read_key } = req.query; 91 | let { bucket, type_slug } = data; 92 | // Map objects for unpublished 93 | let algoliaObjects = []; 94 | if (Array.isArray(data)) { 95 | for (object of data) { 96 | algoliaObjects.push(convertCosmicObjToAlgoliaObj(object)); 97 | } 98 | bucket = data[0].bucket; 99 | type_slug = data[0].type_slug; 100 | } else { 101 | algoliaObjects = [convertCosmicObjToAlgoliaObj(data)]; 102 | } 103 | const searchBucket = Cosmic.bucket({ slug: 'algolia-search' }); 104 | const bucketSlugRes = await searchBucket.getObject({ slug: bucket }); 105 | const projectBucketSlug = bucketSlugRes.object.content; 106 | 107 | // Fetch algolia application id & admin api key 108 | const projectBucket = Cosmic.bucket({ 109 | slug: projectBucketSlug, 110 | read_key, 111 | }); 112 | const getKeysRes = await Promise.all([ 113 | projectBucket.getObject({ slug: 'algolia-info-application-id' }).catch(() => undefined), 114 | projectBucket.getObject({ slug: 'algolia-info-admin-api-key' }).catch(() => undefined), 115 | ]); 116 | const applicationId = getKeysRes[0] && getKeysRes[0].object && getKeysRes[0].object.content; 117 | const adminApiKey = getKeysRes[1] && getKeysRes[1].object && getKeysRes[1].object.content; 118 | 119 | const client = algoliasearch(applicationId, adminApiKey); 120 | const index = client.initIndex(type_slug); 121 | for (const algoliaObject of algoliaObjects) { 122 | const addRes = await index.addObject(algoliaObject); 123 | const { taskID } = addRes; 124 | await index.waitTask(taskID); 125 | } 126 | res.status(200).send(); 127 | } catch (e) { 128 | console.error(e); // eslint-disable-line no-console 129 | res.status(200).send(); 130 | } 131 | }); 132 | 133 | server.post('/api/delete', async (req, res) => { 134 | try { 135 | const { data, type } = req.body; 136 | const { bucket, read_key, types } = req.query; 137 | let ids = data; 138 | // Map ids for unpublished 139 | if (type === 'object.edited.unpublished') { 140 | if (Array.isArray(data)) { ids = data.map(item => item._id); } else { ids = [data._id]; } 141 | } 142 | const searchBucket = Cosmic.bucket({ slug: 'algolia-search' }); 143 | const bucketSlugRes = await searchBucket.getObject({ slug: bucket }); 144 | const projectBucketSlug = bucketSlugRes.object.content; 145 | 146 | // Fetch algolia application id & admin api key 147 | const projectBucket = Cosmic.bucket({ 148 | slug: projectBucketSlug, 149 | read_key, 150 | }); 151 | const getKeysRes = await Promise.all([ 152 | projectBucket.getObject({ slug: 'algolia-info-application-id' }).catch(() => undefined), 153 | projectBucket.getObject({ slug: 'algolia-info-admin-api-key' }).catch(() => undefined), 154 | ]); 155 | 156 | const applicationId = getKeysRes[0] && getKeysRes[0].object && getKeysRes[0].object.content; 157 | const adminApiKey = getKeysRes[1] && getKeysRes[1].object && getKeysRes[1].object.content; 158 | 159 | const client = algoliasearch(applicationId, adminApiKey); 160 | const types_array = types.split(','); 161 | for (const object_type of types_array) { 162 | const index = client.initIndex(object_type); 163 | const addRes = await index.deleteObjects(ids); 164 | const { taskID } = addRes; 165 | await index.waitTask(taskID); 166 | } 167 | res.status(200).send(); 168 | } catch (e) { 169 | console.log(req.body); 170 | console.error(e); // eslint-disable-line no-console 171 | res.status(200).send(); 172 | } 173 | }); 174 | 175 | server.listen(port, (err) => { 176 | if (err) throw err; 177 | // eslint-disable-next-line no-console 178 | console.log(`√ Ready at http://localhost:${port}`); 179 | }); 180 | --------------------------------------------------------------------------------