gridsome-source-mysql
Version:
Gridsome Source Plugin to load data directly from MySQL Database
269 lines (221 loc) • 7.71 kB
JavaScript
const os = require('os')
const readline = require('readline')
const mysql = require('mysql')
const pMap = require('p-map')
const file = require('./file.js')
let cpus = os.cpus().length
cpus = cpus > 2 ? cpus : 2
let ISDEV = process.env.NODE_ENV === 'development'
/* Capitalize string */
function capitalize(str) {
str = str.trim()
return str.trim().charAt(0).toUpperCase() + str.slice(1)
}
class MySQLSource {
static defaultOptions () {
return {
debug: false,
ignoreImages: false,
imageDirectory: 'sql_images',
regex: false,
queries: [],
connection: {
host: 'localhost',
port: 3306,
user: 'root',
password: 'secret',
database: 'my_db',
connectionLimit : 10
}
}
}
constructor (api, options = MySQLSource.defaultOptions()) {
const opts = {
...MySQLSource.defaultOptions(),
...options
}
this.pool = mysql.createPool({
...opts.connection
});
ISDEV = opts.debug
this.cTypes = {}
this.paths = {}
this.queries = opts.queries || []
if (!this.queries.length) throw new Error('No queries to load')
this.loadImages = false
this.regex = opts.regex
this.images = opts.ignoreImages ? false : {}
this.imageDirectory = opts.imageDirectory
api.loadSource(async (store) => {
this.store = store
this.checkQNames(this.queries)
await this.fetchQueries(this.queries)
this.pool.end(function(err) {
ISDEV && console.log('MySQL Connections Closed')
})
if (this.images && this.loadImages) await this.downloadImages()
})
}
checkQNames(queries) {
Array.isArray(queries) && queries.forEach((Q) => {
Q.name = capitalize(Q.name)
if (this.cTypes[Q.name]) console.warn(`You should not have two queries with the same name. ${Q.name}`)
else this.cTypes[Q.name] = true
if (Q.subs) this.checkQNames(Q.subs)
})
}
async fetchQueries(queries, parentQuery, parentRow) {
const { slugify, addContentType, makeUid, createReference } = this.store
await Promise.all(queries.map(async (Q) => {
const args = (typeof Q.args === 'function' ? Q.args(parentRow) : Q.args) || null
const sql = mysql.format(Q.sql, args)
const cType = this.cTypes[Q.name] = addContentType({
typeName: Q.name,
route: Q.route
})
const rels = []
const rows = await new Promise((resolve, reject) => {
this.pool.query(sql, (error, results, fields) => {
if (error) throw new Error(error)
/* Find relationship fields */
let hasIdField = false
for (const f in fields) {
const field = fields[f].name
hasIdField = field === 'id' || hasIdField
const matches = field.match(/^(.+)_(ids?$)/)
if (matches && matches.length > 2) {
const qname = matches[1]
const qtype = capitalize(qname)
if (this.cTypes[qtype]) {
rels.push({
type: qtype,
name: qname,
field,
isArray: matches[2] === 'ids'
})
} else {
console.warn(`No query with name "${qname}" exists. Not creating relation`)
}
}
}
if (!hasIdField) throw new Error('Rows must have id field')
resolve(results)
})
})
if (!Array.isArray(rows)) rows = []
console.log(`${Q.name}: retrieved ${rows.length} results`)
let PathFn = Q.path
if (typeof PathFn !== 'function') {
/* Default path function */
PathFn = (slugify, row, parent) => {
let slug = `/${Q.name}/${row.id}`
if (typeof Q.path === 'object') {
slug = Q.path.prefix || ''
slug += `/${row[Q.path.field] ? slugify(row[Q.path.field]) : row.id}`
slug += Q.path.suffix || ''
} else if (typeof Q.path === 'string' && row[Q.path]) {
slug = slugify(row[Q.path]) || slug
}
return slug
}
}
return Promise.all(rows.map(async (row, i) => {
row.mysqlId = row.id
row.id = makeUid(`${Q.name}–${row.id}`)
row.path = PathFn(slugify, row, parentRow)
if (this.paths[row.path]) {
row.path = `${row.path}-${this.paths[row.path]++}`
} else this.paths[row.path] = 1
if (parentQuery && parentRow)
row._parent = createReference(parentQuery.name, parentRow.id)
/* Parse JSON fields */
if (Array.isArray(Q.json)) {
Q.json.forEach(jsonField => {
try {
row[jsonField] = JSON.parse(row[jsonField])
} catch (e) {
row[jsonField] = null
}
})
}
/* Check for images */
if (this.images && Array.isArray(Q.images)) {
await pMap(Q.images, async imgField => {
if (typeof imgField === 'function') {
await imgField(row, (url) => this.addImage(url))
}
if (Array.isArray(imgField)) {
if (imgField.length !== 1) throw new Error('MySQL query image array should contain exactly 1 field')
row[imgField[0]] = String(row[imgField[0]]).split(',').map((url, i) => ({
index: i,
image: this.addImage(url)
})).filter(image => !!image)
} else {
row[imgField] = this.addImage(row[imgField])
}
})
}
/* Check for relationships */
rels.forEach(rel => {
if (rel.isArray) {
const ids = String(row[rel.field]).split(',')
row[rel.name] = ids.map(id => createReference(rel.type,
makeUid(`${rel.type}–${id}`)))
} else {
row[rel.name] = createReference(rel.type, makeUid(`${rel.type}–${row[rel.field]}`))
}
})
cType.addNode(row)
/* Check sub queries to execute with parent */
if (Array.isArray(Q.subs)) return this.fetchQueries(Q.subs, Q, row)
return row
}))
}))
}
addImage(url) {
if (url && String(url).match(/^https:\/\/.*\/.*\.(jpg|png|svg|jpeg)($|\?)/i)) {
const filename = file.getFilename(url, this.regex)
const id = this.store.makeUid(filename)
const filepath = file.getFullPath(this.imageDirectory, filename)
if (!this.images[id]) this.images[id] = {
filename,
url,
filepath
}
this.loadImages = true
return filepath
}
return null
}
async downloadImages() {
file.createDirectory(this.imageDirectory)
let exists = 0
const download = []
Object.keys(this.images).forEach(async (id) => {
const { filename, filepath } = this.images[id]
if (!file.exists(filepath)) {
download.push(this.images[id])
} else exists++
})
const total = download.length
let progress = 0
function status(msg) {
readline.clearLine(process.stdout, 0)
readline.cursorTo(process.stdout, 0, null)
process.stdout.write(msg)
}
console.log(`${exists} images already exists with ${total} images to download`)
if (total) {
await pMap(download, async ({ filename, url, filepath }) => {
await file.download(url, filepath)
status(`${Math.round((++progress)*100/total)}% – Downloaded ${filename}`)
}, {
concurrency: cpus * 2
})
status('100% – ')
console.log(`${total} images downloaded`)
}
this.loadImages = false
}
}
module.exports = MySQLSource