UNPKG

@gridsome/source-wordpress

Version:
323 lines (256 loc) 8.92 kB
const pMap = require('p-map') const axios = require('axios') const camelCase = require('camelcase') const { mapKeys, isPlainObject, trimEnd, trimStart } = require('lodash') const TYPE_AUTHOR = 'author' const TYPE_ATTACHEMENT = 'attachment' class WordPressSource { static defaultOptions () { return { baseUrl: '', apiBase: 'wp-json', perPage: 100, concurrent: 10, typeName: 'WordPress' } } constructor (api, options) { this.options = options this.restBases = { posts: {}, taxonomies: {}} if (!options.typeName) { throw new Error(`Missing typeName option.`) } if (options.perPage > 100 || options.perPage < 1) { throw new Error(`${options.typeName}: perPage cannot be more than 100 or less than 1.`) } this.customEndpoints = this.sanitizeCustomEndpoints() const baseUrl = trimEnd(options.baseUrl, '/') this.client = axios.create({ baseURL: `${baseUrl}/${options.apiBase}` }) this.routes = this.options.routes || {} api.loadSource(async actions => { this.store = actions console.log(`Loading data from ${baseUrl}`) await this.getPostTypes(actions) await this.getUsers(actions) await this.getTaxonomies(actions) await this.getPosts(actions) await this.getCustomEndpoints(actions) }) } async getPostTypes (actions) { const { data } = await this.fetch('wp/v2/types', {}, {}) const addCollection = actions.addCollection || actions.addContentType for (const type in data) { const options = data[type] this.restBases.posts[type] = trimStart(options.rest_base, '/') addCollection({ typeName: this.createTypeName(type), route: this.routes[type] }) } } async getUsers (actions) { const { data } = await this.fetch('wp/v2/users') const addCollection = actions.addCollection || actions.addContentType const authors = addCollection({ typeName: this.createTypeName(TYPE_AUTHOR), route: this.routes.author }) for (const author of data) { const fields = this.normalizeFields(author) const avatars = mapKeys(author.avatar_urls, (v, key) => `avatar${key}`) authors.addNode({ ...fields, id: author.id, title: author.name, avatars }) } } async getTaxonomies (actions) { const { data } = await this.fetch('wp/v2/taxonomies', {}, {}) const addCollection = actions.addCollection || actions.addContentType for (const type in data) { const options = data[type] const taxonomy = addCollection({ typeName: this.createTypeName(type), route: this.routes[type] }) this.restBases.taxonomies[type] = trimStart(options.rest_base, '/') const terms = await this.fetchPaged(`wp/v2/${options.rest_base}`) for (const term of terms) { taxonomy.addNode({ id: term.id, title: term.name, slug: term.slug, content: term.description, meta: term.meta, count: term.count }) } } } async getPosts (actions) { const { createReference } = actions const getCollection = actions.getCollection || actions.getContentType const AUTHOR_TYPE_NAME = this.createTypeName(TYPE_AUTHOR) const ATTACHEMENT_TYPE_NAME = this.createTypeName(TYPE_ATTACHEMENT) for (const type in this.restBases.posts) { const restBase = this.restBases.posts[type] const typeName = this.createTypeName(type) const posts = getCollection(typeName) const data = await this.fetchPaged(`wp/v2/${restBase}`) for (const post of data) { const fields = this.normalizeFields(post) fields.author = createReference(AUTHOR_TYPE_NAME, post.author || '0') if (post.type !== TYPE_ATTACHEMENT) { fields.featuredMedia = createReference(ATTACHEMENT_TYPE_NAME, post.featured_media) } // add references if post has any taxonomy rest bases as properties for (const type in this.restBases.taxonomies) { const propName = this.restBases.taxonomies[type] if (post.hasOwnProperty(propName)) { const typeName = this.createTypeName(type) const key = camelCase(propName) fields[key] = Array.isArray(post[propName]) ? post[propName].map(id => createReference(typeName, id)) : createReference(typeName, post[propName]) } } posts.addNode({ ...fields, id: post.id }) } } } async getCustomEndpoints (actions) { for (const endpoint of this.customEndpoints) { const makeCollection = actions.addCollection || actions.addContentType const cepCollection = makeCollection({ typeName: endpoint.typeName }) const { data } = await this.fetch(endpoint.route, {}, {}) for (let item of data) { if (endpoint.normalize) { item = this.normalizeFields(item) } cepCollection.addNode({ ...item, id: item.id || item.slug }) } } } async fetch (url, params = {}, fallbackData = []) { let res try { res = await this.client.request({ url, params }) } catch ({ response, code, config }) { if (!response && code) { throw new Error(`${code} - ${config.url}`) } if ([401, 403].includes(response.status)) { console.warn(`Error: Status ${response.status} - ${config.url}`) return { ...response, data: fallbackData } } else { throw new Error(`${response.status} - ${config.url}`) } } return res } async fetchPaged (path) { const { perPage, concurrent } = this.options return new Promise(async (resolve, reject) => { let res try { res = await this.fetch(path, { per_page: perPage }) } catch (err) { return reject(err) } const totalItems = parseInt(res.headers['x-wp-total'], 10) const totalPages = parseInt(res.headers['x-wp-totalpages'], 10) try { res.data = ensureArrayData(path, res.data) } catch (err) { return reject(err) } if (!totalItems || totalPages <= 1) { return resolve(res.data) } const queue = [] for (let page = 2; page <= totalPages; page++) { queue.push({ per_page: perPage, page }) } await pMap(queue, async params => { try { const { data } = await this.fetch(path, params) res.data.push(...ensureArrayData(path, data)) } catch (err) { console.log(err.message) } }, { concurrency: concurrent }) resolve(res.data) }) } sanitizeCustomEndpoints () { if (!this.options.customEndpoints) return [] if (!Array.isArray(this.options.customEndpoints)) throw Error('customEndpoints must be an array') this.options.customEndpoints.forEach(endpoint => { if (!endpoint.typeName) { throw Error('Please provide a typeName option for all customEndpoints\n') } if (!endpoint.route) { throw Error(`No route option in endpoint: ${endpoint.typeName}\n Ex: 'apiName/versionNumber/endpointObject'`) } }) return this.options.customEndpoints ? this.options.customEndpoints : [] } normalizeFields (fields) { const res = {} for (const key in fields) { if (key.startsWith('_')) continue // skip links and embeds etc res[camelCase(key)] = this.normalizeFieldValue(fields[key]) } return res } normalizeFieldValue (value) { if (value === null) return null if (value === undefined) return null if (Array.isArray(value)) { return value.map(v => this.normalizeFieldValue(v)) } if (isPlainObject(value)) { if (value.post_type && (value.ID || value.id)) { const typeName = this.createTypeName(value.post_type) const id = value.ID || value.id return this.store.createReference(typeName, id) } else if (value.filename && (value.ID || value.id)) { const typeName = this.createTypeName(TYPE_ATTACHEMENT) const id = value.ID || value.id return this.store.createReference(typeName, id) } else if (value.hasOwnProperty('rendered')) { return value.rendered } return this.normalizeFields(value) } return value } createTypeName (name = '') { return camelCase(`${this.options.typeName} ${name}`, { pascalCase: true }) } } function ensureArrayData (url, data) { if (!Array.isArray(data)) { try { data = JSON.parse(data) } catch (err) { throw new Error( `Failed to fetch ${url}\n` + `Expected JSON response but received:\n` + `${data.trim().substring(0, 150)}...\n` ) } } return data } module.exports = WordPressSource