Adopt Prettier (but keep Standard) via ESLint (#48)

This commit is contained in:
Brian Beck 2017-11-06 21:54:56 -08:00 committed by GitHub
parent 50888c9fb9
commit 8c0a9f44ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 1830 additions and 1487 deletions

2
.eslintignore Normal file
View file

@ -0,0 +1,2 @@
/coverage
/lib

18
.eslintrc.js Normal file
View file

@ -0,0 +1,18 @@
module.exports = {
extends: [
'standard',
'prettier',
'prettier/standard'
],
env: {
es6: true,
node: true
},
plugins: ['prettier'],
rules: {
'prettier/prettier': ['error', {
singleQuote: true,
semi: false
}]
}
}

View file

@ -30,8 +30,9 @@
"clean": "npm run clean:lib", "clean": "npm run clean:lib",
"clean:lib": "rm -rf lib", "clean:lib": "rm -rf lib",
"deploy": "./scripts/deploy.sh", "deploy": "./scripts/deploy.sh",
"lint": "standard --verbose | snazzy", "format": "npm run lint:fix",
"lint:fix": "standard --verbose --fix", "lint": "eslint .",
"lint:fix": "eslint --fix .",
"postinstall": "postinstall-build lib --script build:lib", "postinstall": "postinstall-build lib --script build:lib",
"prepublish": "npm run clean:lib && npm run build:lib", "prepublish": "npm run clean:lib && npm run build:lib",
"preversion": "npm run update-schema && npm run build:docs && git add schema.json docs", "preversion": "npm run update-schema && npm run build:docs && git add schema.json docs",
@ -100,15 +101,22 @@
"coveralls": "^3.0.0", "coveralls": "^3.0.0",
"cross-env": "^5.1.1", "cross-env": "^5.1.1",
"doctoc": "^1.3.0", "doctoc": "^1.3.0",
"eslint": "^4.10.0",
"eslint-config-prettier": "^2.7.0",
"eslint-config-standard": "^10.2.1",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-markdown": "^1.0.0-beta.6", "eslint-plugin-markdown": "^1.0.0-beta.6",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-prettier": "^2.3.1",
"eslint-plugin-promise": "^3.6.0",
"eslint-plugin-standard": "^3.0.1",
"graphql-markdown": "^3.2.0", "graphql-markdown": "^3.2.0",
"nodemon": "^1.11.0", "nodemon": "^1.11.0",
"nyc": "^11.1.0", "nyc": "^11.1.0",
"prettier": "^1.8.0",
"rimraf": "^2.6.1", "rimraf": "^2.6.1",
"sepia": "^2.0.2", "sepia": "^2.0.2",
"sinon": "^4.0.2", "sinon": "^4.0.2"
"snazzy": "^7.0.0",
"standard": "^10.0.3"
}, },
"standard": { "standard": {
"parser": "babel-eslint" "parser": "babel-eslint"

View file

@ -10,40 +10,43 @@ const extensionModules = [
'the-audio-db' 'the-audio-db'
] ]
function getSchemaJSON (schema) { function getSchemaJSON(schema) {
return graphql(schema, introspectionQuery).then(result => result.data) return graphql(schema, introspectionQuery).then(result => result.data)
} }
Promise.all(extensionModules.map(extensionModule => { Promise.all(
const extension = require(`../src/extensions/${extensionModule}`).default extensionModules.map(extensionModule => {
console.log(`Generating docs for “${extension.name}” extension...`) const extension = require(`../src/extensions/${extensionModule}`).default
const schema = createSchema(baseSchema, { extensions: [extension] }) console.log(`Generating docs for “${extension.name}” extension...`)
return Promise.all([ const schema = createSchema(baseSchema, { extensions: [extension] })
getSchemaJSON(baseSchema), return Promise.all([getSchemaJSON(baseSchema), getSchemaJSON(schema)]).then(
getSchemaJSON(schema) ([baseSchemaJSON, schemaJSON]) => {
]).then(([baseSchemaJSON, schemaJSON]) => { const outputSchema = diffSchema(baseSchemaJSON, schemaJSON, {
const outputSchema = diffSchema(baseSchemaJSON, schemaJSON, { processTypeDiff(type) {
processTypeDiff (type) { if (type.description === undefined) {
if (type.description === undefined) { type.description =
type.description = ':small_blue_diamond: *This type has been extended. See the ' +
':small_blue_diamond: *This type has been extended. See the ' + '[base schema](../types.md)\nfor a description and additional ' +
'[base schema](../types.md)\nfor a description and additional ' + 'fields.*'
'fields.*' }
} return type
return type }
})
const outputPath = path.resolve(
__dirname,
`../docs/extensions/${extensionModule}.md`
)
return updateSchema(outputPath, outputSchema, {
unknownTypeURL: '../types.md',
headingLevel: 2
})
} }
})
const outputPath = path.resolve(
__dirname,
`../docs/extensions/${extensionModule}.md`
) )
return updateSchema(outputPath, outputSchema, {
unknownTypeURL: '../types.md',
headingLevel: 2
})
}) })
})).then((extensions) => { )
console.log(`Built docs for ${extensions.length} extension(s).`) .then(extensions => {
}).catch(err => { console.log(`Built docs for ${extensions.length} extension(s).`)
console.log('Error:', err) })
}) .catch(err => {
console.log('Error:', err)
})

View file

@ -2,11 +2,13 @@ import { graphql, introspectionQuery, printSchema } from 'graphql'
import schema from '../src/schema' import schema from '../src/schema'
if (process.argv[2] === '--json') { if (process.argv[2] === '--json') {
graphql(schema, introspectionQuery).then(result => { graphql(schema, introspectionQuery)
console.log(JSON.stringify(result, null, 2)) .then(result => {
}).catch(err => { console.log(JSON.stringify(result, null, 2))
console.error(err) })
}) .catch(err => {
console.error(err)
})
} else { } else {
console.log(printSchema(schema)) console.log(printSchema(schema))
} }

View file

@ -21,33 +21,35 @@ const RETRY_CODES = {
} }
export class ClientError extends ExtendableError { export class ClientError extends ExtendableError {
constructor (message, statusCode) { constructor(message, statusCode) {
super(message) super(message)
this.statusCode = statusCode this.statusCode = statusCode
} }
} }
export default class Client { export default class Client {
constructor ({ constructor(
baseURL, {
userAgent = `${pkg.name}/${pkg.version} ` + baseURL,
`( ${pkg.homepage || pkg.author.url || pkg.author.email} )`, userAgent = `${pkg.name}/${pkg.version} ` +
extraHeaders = {}, `( ${pkg.homepage || pkg.author.url || pkg.author.email} )`,
errorClass = ClientError, extraHeaders = {},
timeout = 60000, errorClass = ClientError,
limit = 1, timeout = 60000,
period = 1000, limit = 1,
concurrency = 10, period = 1000,
retries = 10, concurrency = 10,
// It's OK for `retryDelayMin` to be less than one second, even 0, because retries = 10,
// `RateLimit` will already make sure we don't exceed the API rate limit. // It's OK for `retryDelayMin` to be less than one second, even 0, because
// We're not doing exponential backoff because it will help with being // `RateLimit` will already make sure we don't exceed the API rate limit.
// rate limited, but rather to be chill in case MusicBrainz is returning // We're not doing exponential backoff because it will help with being
// some other error or our network is failing. // rate limited, but rather to be chill in case MusicBrainz is returning
retryDelayMin = 100, // some other error or our network is failing.
retryDelayMax = 60000, retryDelayMin = 100,
randomizeRetry = true retryDelayMax = 60000,
} = {}) { randomizeRetry = true
} = {}
) {
this.baseURL = baseURL this.baseURL = baseURL
this.userAgent = userAgent this.userAgent = userAgent
this.extraHeaders = extraHeaders this.extraHeaders = extraHeaders
@ -67,14 +69,14 @@ export default class Client {
* Retry any 5XX response from MusicBrainz, as well as any error in * Retry any 5XX response from MusicBrainz, as well as any error in
* `RETRY_CODES`. * `RETRY_CODES`.
*/ */
shouldRetry (err) { shouldRetry(err) {
if (err instanceof this.errorClass) { if (err instanceof this.errorClass) {
return err.statusCode >= 500 && err.statusCode < 600 return err.statusCode >= 500 && err.statusCode < 600
} }
return RETRY_CODES[err.code] || false return RETRY_CODES[err.code] || false
} }
parseErrorMessage (response, body) { parseErrorMessage(response, body) {
return typeof body === 'string' && body ? body : `${response.statusCode}` return typeof body === 'string' && body ? body : `${response.statusCode}`
} }
@ -82,7 +84,7 @@ export default class Client {
* Send a request without any retrying or rate limiting. * Send a request without any retrying or rate limiting.
* Use `get` instead. * Use `get` instead.
*/ */
_get (path, options = {}, info = {}) { _get(path, options = {}, info = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
options = { options = {
baseUrl: this.baseURL, baseUrl: this.baseURL,
@ -122,7 +124,7 @@ export default class Client {
/** /**
* Send a request with retrying and rate limiting. * Send a request with retrying and rate limiting.
*/ */
get (path, options = {}) { get(path, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const fn = this._get.bind(this) const fn = this._get.bind(this)
const operation = retry.operation(this.retryOptions) const operation = retry.operation(this.retryOptions)
@ -130,7 +132,8 @@ export default class Client {
// This will increase the priority in our `RateLimit` queue for each // This will increase the priority in our `RateLimit` queue for each
// retry, so that newer requests don't delay this one further. // retry, so that newer requests don't delay this one further.
const priority = currentAttempt const priority = currentAttempt
this.limiter.enqueue(fn, [path, options, { currentAttempt }], priority) this.limiter
.enqueue(fn, [path, options, { currentAttempt }], priority)
.then(resolve) .then(resolve)
.catch(err => { .catch(err => {
if (!this.shouldRetry(err) || !operation.retry(err)) { if (!this.shouldRetry(err) || !operation.retry(err)) {

View file

@ -1,7 +1,3 @@
import MusicBrainz, { MusicBrainzError } from './musicbrainz' import MusicBrainz, { MusicBrainzError } from './musicbrainz'
export { export { MusicBrainz as default, MusicBrainz, MusicBrainzError }
MusicBrainz as default,
MusicBrainz,
MusicBrainzError
}

View file

@ -4,30 +4,33 @@ import Client, { ClientError } from './client'
export class MusicBrainzError extends ClientError {} export class MusicBrainzError extends ClientError {}
export default class MusicBrainz extends Client { export default class MusicBrainz extends Client {
constructor ({ constructor(
baseURL = process.env.MUSICBRAINZ_BASE_URL || 'http://musicbrainz.org/ws/2/', {
errorClass = MusicBrainzError, baseURL = process.env.MUSICBRAINZ_BASE_URL ||
// MusicBrainz API requests are limited to an *average* of 1 req/sec. 'http://musicbrainz.org/ws/2/',
// That means if, for example, we only need to make a few API requests to errorClass = MusicBrainzError,
// fulfill a query, we might as well make them all at once - as long as // MusicBrainz API requests are limited to an *average* of 1 req/sec.
// we then wait a few seconds before making more. In practice this can // That means if, for example, we only need to make a few API requests to
// seemingly be set to about 5 requests every 5 seconds before we're // fulfill a query, we might as well make them all at once - as long as
// considered to exceed the rate limit. // we then wait a few seconds before making more. In practice this can
limit = 5, // seemingly be set to about 5 requests every 5 seconds before we're
period = 5500, // considered to exceed the rate limit.
...options limit = 5,
} = {}) { period = 5500,
...options
} = {}
) {
super({ baseURL, errorClass, limit, period, ...options }) super({ baseURL, errorClass, limit, period, ...options })
} }
parseErrorMessage (response, body) { parseErrorMessage(response, body) {
if (body && body.error) { if (body && body.error) {
return body.error return body.error
} }
return super.parseErrorMessage(response, body) return super.parseErrorMessage(response, body)
} }
stringifyParams (params) { stringifyParams(params) {
if (Array.isArray(params.inc)) { if (Array.isArray(params.inc)) {
params = { params = {
...params, ...params,
@ -48,41 +51,41 @@ export default class MusicBrainz extends Client {
} }
return qs.stringify(params, { return qs.stringify(params, {
skipNulls: true, skipNulls: true,
filter: (key, value) => value === '' ? undefined : value filter: (key, value) => (value === '' ? undefined : value)
}) })
} }
getURL (path, params) { getURL(path, params) {
const query = params ? this.stringifyParams(params) : '' const query = params ? this.stringifyParams(params) : ''
return query ? `${path}?${query}` : path return query ? `${path}?${query}` : path
} }
getLookupURL (entity, id, params) { getLookupURL(entity, id, params) {
if (id == null) { if (id == null) {
return this.getBrowseURL(entity, params) return this.getBrowseURL(entity, params)
} }
return this.getURL(`${entity}/${id}`, params) return this.getURL(`${entity}/${id}`, params)
} }
lookup (entity, id, params = {}) { lookup(entity, id, params = {}) {
const url = this.getLookupURL(entity, id, params) const url = this.getLookupURL(entity, id, params)
return this.get(url, { json: true, qs: { fmt: 'json' } }) return this.get(url, { json: true, qs: { fmt: 'json' } })
} }
getBrowseURL (entity, params) { getBrowseURL(entity, params) {
return this.getURL(entity, params) return this.getURL(entity, params)
} }
browse (entity, params = {}) { browse(entity, params = {}) {
const url = this.getBrowseURL(entity, params) const url = this.getBrowseURL(entity, params)
return this.get(url, { json: true, qs: { fmt: 'json' } }) return this.get(url, { json: true, qs: { fmt: 'json' } })
} }
getSearchURL (entity, query, params) { getSearchURL(entity, query, params) {
return this.getURL(entity, { ...params, query }) return this.getURL(entity, { ...params, query })
} }
search (entity, query, params = {}) { search(entity, query, params = {}) {
const url = this.getSearchURL(entity, query, params) const url = this.getSearchURL(entity, query, params)
return this.get(url, { json: true, qs: { fmt: 'json' } }) return this.get(url, { json: true, qs: { fmt: 'json' } })
} }

View file

@ -2,22 +2,26 @@ import createLoaders from './loaders'
const debug = require('debug')('graphbrainz:context') const debug = require('debug')('graphbrainz:context')
export function extendContext (extension, context, options) { export function extendContext(extension, context, options) {
if (extension.extendContext) { if (extension.extendContext) {
if (typeof extension.extendContext === 'function') { if (typeof extension.extendContext === 'function') {
debug(`Extending context via a function from the “${extension.name}” extension.`) debug(
`Extending context via a function from the “${
extension.name
} extension.`
)
context = extension.extendContext(context, options) context = extension.extendContext(context, options)
} else { } else {
throw new Error( throw new Error(
`Extension “${extension.name}” contains an invalid \`extendContext\` ` + `Extension “${extension.name}” contains an invalid \`extendContext\` ` +
`value: ${extension.extendContext}` `value: ${extension.extendContext}`
) )
} }
} }
return context return context
} }
export function createContext (options = {}) { export function createContext(options = {}) {
const { client } = options const { client } = options
const loaders = createLoaders(client) const loaders = createLoaders(client)
const context = { client, loaders } const context = { client, loaders }

View file

@ -1,19 +1,22 @@
import Client from '../../api/client' import Client from '../../api/client'
export default class CoverArtArchiveClient extends Client { export default class CoverArtArchiveClient extends Client {
constructor ({ constructor(
baseURL = process.env.COVER_ART_ARCHIVE_BASE_URL || 'http://coverartarchive.org/', {
limit = 10, baseURL = process.env.COVER_ART_ARCHIVE_BASE_URL ||
period = 1000, 'http://coverartarchive.org/',
...options limit = 10,
} = {}) { period = 1000,
...options
} = {}
) {
super({ baseURL, limit, period, ...options }) super({ baseURL, limit, period, ...options })
} }
/** /**
* Sinfully attempt to parse HTML responses for the error message. * Sinfully attempt to parse HTML responses for the error message.
*/ */
parseErrorMessage (response, body) { parseErrorMessage(response, body) {
if (typeof body === 'string' && body.startsWith('<!')) { if (typeof body === 'string' && body.startsWith('<!')) {
const heading = /<h1>([^<]+)<\/h1>/i.exec(body) const heading = /<h1>([^<]+)<\/h1>/i.exec(body)
const message = /<p>([^<]+)<\/p>/i.exec(body) const message = /<p>([^<]+)<\/p>/i.exec(body)
@ -22,16 +25,17 @@ export default class CoverArtArchiveClient extends Client {
return super.parseErrorMessage(response, body) return super.parseErrorMessage(response, body)
} }
images (entityType, mbid) { images(entityType, mbid) {
return this.get(`${entityType}/${mbid}`, { json: true }) return this.get(`${entityType}/${mbid}`, { json: true })
} }
imageURL (entityType, mbid, typeOrID = 'front', size) { imageURL(entityType, mbid, typeOrID = 'front', size) {
let url = `${entityType}/${mbid}/${typeOrID}` let url = `${entityType}/${mbid}/${typeOrID}`
if (size != null) { if (size != null) {
url += `-${size}` url += `-${size}`
} }
return this.get(url, { method: 'HEAD', followRedirect: false }) return this.get(url, { method: 'HEAD', followRedirect: false }).then(
.then(headers => headers.location) headers => headers.location
)
} }
} }

View file

@ -8,14 +8,18 @@ export default {
name: 'Cover Art Archive', name: 'Cover Art Archive',
description: `Retrieve cover art images for releases from the [Cover Art description: `Retrieve cover art images for releases from the [Cover Art
Archive](https://coverartarchive.org/).`, Archive](https://coverartarchive.org/).`,
extendContext (context, { coverArtClient, coverArtArchive = {} } = {}) { extendContext(context, { coverArtClient, coverArtArchive = {} } = {}) {
const client = coverArtClient || new CoverArtArchiveClient(coverArtArchive) const client = coverArtClient || new CoverArtArchiveClient(coverArtArchive)
const cacheSize = parseInt( const cacheSize = parseInt(
process.env.COVER_ART_ARCHIVE_CACHE_SIZE || process.env.GRAPHBRAINZ_CACHE_SIZE || 8192, process.env.COVER_ART_ARCHIVE_CACHE_SIZE ||
process.env.GRAPHBRAINZ_CACHE_SIZE ||
8192,
10 10
) )
const cacheTTL = parseInt( const cacheTTL = parseInt(
process.env.COVER_ART_ARCHIVE_CACHE_TTL || process.env.GRAPHBRAINZ_CACHE_TTL || ONE_DAY, process.env.COVER_ART_ARCHIVE_CACHE_TTL ||
process.env.GRAPHBRAINZ_CACHE_TTL ||
ONE_DAY,
10 10
) )
return { return {

View file

@ -3,12 +3,12 @@ import LRUCache from 'lru-cache'
const debug = require('debug')('graphbrainz:extensions/cover-art-archive') const debug = require('debug')('graphbrainz:extensions/cover-art-archive')
export default function createLoaders (options) { export default function createLoaders(options) {
const { client } = options const { client } = options
const cache = LRUCache({ const cache = LRUCache({
max: options.cacheSize, max: options.cacheSize,
maxAge: options.cacheTTL, maxAge: options.cacheTTL,
dispose (key) { dispose(key) {
debug(`Removed from cache. key=${key}`) debug(`Removed from cache. key=${key}`)
} }
}) })
@ -17,37 +17,50 @@ export default function createLoaders (options) {
cache.clear = cache.reset cache.clear = cache.reset
return { return {
coverArtArchive: new DataLoader(keys => { coverArtArchive: new DataLoader(
return Promise.all(keys.map(key => { keys => {
const [ entityType, id ] = key return Promise.all(
return client.images(entityType, id) keys.map(key => {
.catch(err => { const [entityType, id] = key
if (err.statusCode === 404) { return client
return { images: [] } .images(entityType, id)
} .catch(err => {
throw err if (err.statusCode === 404) {
}).then(coverArt => ({ return { images: [] }
...coverArt, }
_entityType: entityType, throw err
_id: id, })
_releaseID: coverArt.release && coverArt.release.split('/').pop() .then(coverArt => ({
})) ...coverArt,
})) _entityType: entityType,
}, { _id: id,
cacheKeyFn: ([ entityType, id ]) => `${entityType}/${id}`, _releaseID:
cacheMap: cache coverArt.release && coverArt.release.split('/').pop()
}), }))
coverArtArchiveURL: new DataLoader(keys => { })
return Promise.all(keys.map(key => { )
const [ entityType, id, type, size ] = key
return client.imageURL(entityType, id, type, size)
}))
}, {
cacheKeyFn: ([ entityType, id, type, size ]) => {
const key = `${entityType}/${id}/${type}`
return size ? `${key}-${size}` : key
}, },
cacheMap: cache {
}) cacheKeyFn: ([entityType, id]) => `${entityType}/${id}`,
cacheMap: cache
}
),
coverArtArchiveURL: new DataLoader(
keys => {
return Promise.all(
keys.map(key => {
const [entityType, id, type, size] = key
return client.imageURL(entityType, id, type, size)
})
)
},
{
cacheKeyFn: ([entityType, id, type, size]) => {
const key = `${entityType}/${id}/${type}`
return size ? `${key}-${size}` : key
},
cacheMap: cache
}
)
} }
} }

View file

@ -9,7 +9,7 @@ const SIZES = new Map([
['LARGE', 500] ['LARGE', 500]
]) ])
function resolveImage (coverArt, args, { loaders }, info) { function resolveImage(coverArt, args, { loaders }, info) {
// Since migrating the schema to an extension, we lost custom enum values // Since migrating the schema to an extension, we lost custom enum values
// for the time being. Translate any incoming `size` arg to the old enum // for the time being. Translate any incoming `size` arg to the old enum
// values. // values.

View file

@ -1,23 +1,26 @@
import Client from '../../api/client' import Client from '../../api/client'
export default class FanArtClient extends Client { export default class FanArtClient extends Client {
constructor ({ constructor(
apiKey = process.env.FANART_API_KEY, {
baseURL = process.env.FANART_BASE_URL || 'http://webservice.fanart.tv/v3/', apiKey = process.env.FANART_API_KEY,
limit = 10, baseURL = process.env.FANART_BASE_URL ||
period = 1000, 'http://webservice.fanart.tv/v3/',
...options limit = 10,
} = {}) { period = 1000,
...options
} = {}
) {
super({ baseURL, limit, period, ...options }) super({ baseURL, limit, period, ...options })
this.apiKey = apiKey this.apiKey = apiKey
} }
get (path, options = {}) { get(path, options = {}) {
const ClientError = this.errorClass const ClientError = this.errorClass
if (!this.apiKey) { if (!this.apiKey) {
return Promise.reject(new ClientError( return Promise.reject(
'No API key was configured for the fanart.tv client.' new ClientError('No API key was configured for the fanart.tv client.')
)) )
} }
options = { options = {
json: true, json: true,
@ -30,7 +33,7 @@ export default class FanArtClient extends Client {
return super.get(path, options) return super.get(path, options)
} }
musicEntity (entityType, mbid) { musicEntity(entityType, mbid) {
const ClientError = this.errorClass const ClientError = this.errorClass
switch (entityType) { switch (entityType) {
case 'artist': case 'artist':
@ -40,21 +43,21 @@ export default class FanArtClient extends Client {
case 'release-group': case 'release-group':
return this.musicAlbum(mbid) return this.musicAlbum(mbid)
default: default:
return Promise.reject(new ClientError( return Promise.reject(
`Entity type unsupported: ${entityType}` new ClientError(`Entity type unsupported: ${entityType}`)
)) )
} }
} }
musicArtist (mbid) { musicArtist(mbid) {
return this.get(`music/${mbid}`) return this.get(`music/${mbid}`)
} }
musicAlbum (mbid) { musicAlbum(mbid) {
return this.get(`music/albums/${mbid}`) return this.get(`music/albums/${mbid}`)
} }
musicLabel (mbid) { musicLabel(mbid) {
return this.get(`music/${mbid}`) return this.get(`music/${mbid}`)
} }
} }

View file

@ -8,14 +8,18 @@ export default {
name: 'fanart.tv', name: 'fanart.tv',
description: `Retrieve high quality artwork for artists, releases, and labels description: `Retrieve high quality artwork for artists, releases, and labels
from [fanart.tv](https://fanart.tv/).`, from [fanart.tv](https://fanart.tv/).`,
extendContext (context, { fanArt = {} } = {}) { extendContext(context, { fanArt = {} } = {}) {
const client = new FanArtClient(fanArt) const client = new FanArtClient(fanArt)
const cacheSize = parseInt( const cacheSize = parseInt(
process.env.FANART_CACHE_SIZE || process.env.GRAPHBRAINZ_CACHE_SIZE || 8192, process.env.FANART_CACHE_SIZE ||
process.env.GRAPHBRAINZ_CACHE_SIZE ||
8192,
10 10
) )
const cacheTTL = parseInt( const cacheTTL = parseInt(
process.env.FANART_CACHE_TTL || process.env.GRAPHBRAINZ_CACHE_TTL || ONE_DAY, process.env.FANART_CACHE_TTL ||
process.env.GRAPHBRAINZ_CACHE_TTL ||
ONE_DAY,
10 10
) )
return { return {

View file

@ -3,12 +3,12 @@ import LRUCache from 'lru-cache'
const debug = require('debug')('graphbrainz:extensions/fanart-tv') const debug = require('debug')('graphbrainz:extensions/fanart-tv')
export default function createLoader (options) { export default function createLoader(options) {
const { client } = options const { client } = options
const cache = LRUCache({ const cache = LRUCache({
max: options.cacheSize, max: options.cacheSize,
maxAge: options.cacheTTL, maxAge: options.cacheTTL,
dispose (key) { dispose(key) {
debug(`Removed from cache. key=${key}`) debug(`Removed from cache. key=${key}`)
} }
}) })
@ -16,37 +16,48 @@ export default function createLoader (options) {
cache.delete = cache.del cache.delete = cache.del
cache.clear = cache.reset cache.clear = cache.reset
const loader = new DataLoader(keys => { const loader = new DataLoader(
return Promise.all(keys.map(key => { keys => {
const [ entityType, id ] = key return Promise.all(
return client.musicEntity(entityType, id) keys.map(key => {
.catch(err => { const [entityType, id] = key
if (err.statusCode === 404) { return client
// 404s are OK, just return empty data. .musicEntity(entityType, id)
return { .catch(err => {
artistbackground: [], if (err.statusCode === 404) {
artistthumb: [], // 404s are OK, just return empty data.
musiclogo: [], return {
hdmusiclogo: [], artistbackground: [],
musicbanner: [], artistthumb: [],
musiclabel: [], musiclogo: [],
albums: {} hdmusiclogo: [],
} musicbanner: [],
} musiclabel: [],
throw err albums: {}
}).then(body => { }
if (entityType === 'artist') { }
const releaseGroupIDs = Object.keys(body.albums) throw err
debug(`Priming album cache with ${releaseGroupIDs.length} album(s).`) })
releaseGroupIDs.forEach(key => loader.prime(['release-group', key], body)) .then(body => {
} if (entityType === 'artist') {
return body const releaseGroupIDs = Object.keys(body.albums)
debug(
`Priming album cache with ${releaseGroupIDs.length} album(s).`
)
releaseGroupIDs.forEach(key =>
loader.prime(['release-group', key], body)
)
}
return body
})
}) })
})) )
}, { },
cacheKeyFn: ([ entityType, id ]) => `${entityType}/${id}`, {
cacheMap: cache cacheKeyFn: ([entityType, id]) => `${entityType}/${id}`,
}) cacheMap: cache
}
)
return loader return loader
} }

View file

@ -56,7 +56,8 @@ export default {
}, },
ReleaseGroup: { ReleaseGroup: {
fanArt: (releaseGroup, args, context) => { fanArt: (releaseGroup, args, context) => {
return context.loaders.fanArt.load(['release-group', releaseGroup.id]) return context.loaders.fanArt
.load(['release-group', releaseGroup.id])
.then(artist => artist.albums[releaseGroup.id]) .then(artist => artist.albums[releaseGroup.id])
} }
} }

View file

@ -2,22 +2,20 @@ import URL from 'url'
import Client from '../../api/client' import Client from '../../api/client'
export default class MediaWikiClient extends Client { export default class MediaWikiClient extends Client {
constructor ({ constructor({ limit = 10, period = 1000, ...options } = {}) {
limit = 10,
period = 1000,
...options
} = {}) {
super({ limit, period, ...options }) super({ limit, period, ...options })
} }
imageInfo (page) { imageInfo(page) {
const pageURL = URL.parse(page, true) const pageURL = URL.parse(page, true)
const ClientError = this.errorClass const ClientError = this.errorClass
if (!pageURL.pathname.startsWith('/wiki/')) { if (!pageURL.pathname.startsWith('/wiki/')) {
return Promise.reject(new ClientError( return Promise.reject(
`MediaWiki page URL does not have the expected /wiki/ prefix: ${page}` new ClientError(
)) `MediaWiki page URL does not have the expected /wiki/ prefix: ${page}`
)
)
} }
const apiURL = URL.format({ const apiURL = URL.format({
@ -34,21 +32,20 @@ export default class MediaWikiClient extends Client {
} }
}) })
return this.get(apiURL, { json: true }) return this.get(apiURL, { json: true }).then(body => {
.then(body => { const pageIDs = Object.keys(body.query.pages)
const pageIDs = Object.keys(body.query.pages) if (pageIDs.length !== 1) {
if (pageIDs.length !== 1) { throw new ClientError(
throw new ClientError( `Query returned multiple pages: [${pageIDs.join(', ')}]`
`Query returned multiple pages: [${pageIDs.join(', ')}]` )
) }
} const imageInfo = body.query.pages[pageIDs[0]].imageinfo
const imageInfo = body.query.pages[pageIDs[0]].imageinfo if (imageInfo.length !== 1) {
if (imageInfo.length !== 1) { throw new ClientError(
throw new ClientError( `Query returned info for ${imageInfo.length} images, expected 1.`
`Query returned info for ${imageInfo.length} images, expected 1.` )
) }
} return imageInfo[0]
return imageInfo[0] })
})
} }
} }

View file

@ -8,14 +8,18 @@ export default {
name: 'MediaWiki', name: 'MediaWiki',
description: `Retrieve information from MediaWiki image pages, like the actual description: `Retrieve information from MediaWiki image pages, like the actual
image file URL and EXIF metadata.`, image file URL and EXIF metadata.`,
extendContext (context, { mediaWiki = {} } = {}) { extendContext(context, { mediaWiki = {} } = {}) {
const client = new MediaWikiClient(mediaWiki) const client = new MediaWikiClient(mediaWiki)
const cacheSize = parseInt( const cacheSize = parseInt(
process.env.MEDIAWIKI_CACHE_SIZE || process.env.GRAPHBRAINZ_CACHE_SIZE || 8192, process.env.MEDIAWIKI_CACHE_SIZE ||
process.env.GRAPHBRAINZ_CACHE_SIZE ||
8192,
10 10
) )
const cacheTTL = parseInt( const cacheTTL = parseInt(
process.env.MEDIAWIKI_CACHE_TTL || process.env.GRAPHBRAINZ_CACHE_TTL || ONE_DAY, process.env.MEDIAWIKI_CACHE_TTL ||
process.env.GRAPHBRAINZ_CACHE_TTL ||
ONE_DAY,
10 10
) )
return { return {

View file

@ -3,12 +3,12 @@ import LRUCache from 'lru-cache'
const debug = require('debug')('graphbrainz:extensions/mediawiki') const debug = require('debug')('graphbrainz:extensions/mediawiki')
export default function createLoader (options) { export default function createLoader(options) {
const { client } = options const { client } = options
const cache = LRUCache({ const cache = LRUCache({
max: options.cacheSize, max: options.cacheSize,
maxAge: options.cacheTTL, maxAge: options.cacheTTL,
dispose (key) { dispose(key) {
debug(`Removed from cache. key=${key}`) debug(`Removed from cache. key=${key}`)
} }
}) })
@ -16,7 +16,10 @@ export default function createLoader (options) {
cache.delete = cache.del cache.delete = cache.del
cache.clear = cache.reset cache.clear = cache.reset
return new DataLoader(keys => { return new DataLoader(
return Promise.all(keys.map(key => client.imageInfo(key))) keys => {
}, { cacheMap: cache }) return Promise.all(keys.map(key => client.imageInfo(key)))
},
{ cacheMap: cache }
)
} }

View file

@ -1,22 +1,25 @@
import URL from 'url' import URL from 'url'
function resolveMediaWikiImages (source, args, { loaders }) { function resolveMediaWikiImages(source, args, { loaders }) {
const isURL = (relation) => relation['target-type'] === 'url' const isURL = relation => relation['target-type'] === 'url'
let rels = source.relations ? source.relations.filter(isURL) : [] let rels = source.relations ? source.relations.filter(isURL) : []
if (!rels.length) { if (!rels.length) {
rels = loaders.lookup.load([source._type, source.id, { inc: 'url-rels' }]) rels = loaders.lookup
.load([source._type, source.id, { inc: 'url-rels' }])
.then(source => source.relations.filter(isURL)) .then(source => source.relations.filter(isURL))
} }
return Promise.resolve(rels).then(rels => { return Promise.resolve(rels).then(rels => {
const pages = rels.filter(rel => { const pages = rels
if (rel.type === args.type) { .filter(rel => {
const url = URL.parse(rel.url.resource) if (rel.type === args.type) {
if (url.pathname.match(/^\/wiki\/(File|Image):/)) { const url = URL.parse(rel.url.resource)
return true if (url.pathname.match(/^\/wiki\/(File|Image):/)) {
return true
}
} }
} return false
return false })
}).map(rel => rel.url.resource) .map(rel => rel.url.resource)
return loaders.mediaWiki.loadMany(pages) return loaders.mediaWiki.loadMany(pages)
}) })
} }
@ -57,13 +60,14 @@ export default {
const data = imageInfo.extmetadata.LicenseUrl const data = imageInfo.extmetadata.LicenseUrl
return data ? data.value : null return data ? data.value : null
}, },
metadata: imageInfo => Object.keys(imageInfo.extmetadata).map(key => { metadata: imageInfo =>
const data = imageInfo.extmetadata[key] Object.keys(imageInfo.extmetadata).map(key => {
return { ...data, name: key } const data = imageInfo.extmetadata[key]
}) return { ...data, name: key }
})
}, },
MediaWikiImageMetadata: { MediaWikiImageMetadata: {
value: obj => obj.value == null ? obj.value : `${obj.value}` value: obj => (obj.value == null ? obj.value : `${obj.value}`)
}, },
Artist: { Artist: {
mediaWikiImages: resolveMediaWikiImages mediaWikiImages: resolveMediaWikiImages

View file

@ -1,28 +1,31 @@
import Client from '../../api/client' import Client from '../../api/client'
export default class TheAudioDBClient extends Client { export default class TheAudioDBClient extends Client {
constructor ({ constructor(
apiKey = process.env.THEAUDIODB_API_KEY, {
baseURL = process.env.THEAUDIODB_BASE_URL || 'http://www.theaudiodb.com/api/v1/json/', apiKey = process.env.THEAUDIODB_API_KEY,
limit = 10, baseURL = process.env.THEAUDIODB_BASE_URL ||
period = 1000, 'http://www.theaudiodb.com/api/v1/json/',
...options limit = 10,
} = {}) { period = 1000,
...options
} = {}
) {
super({ baseURL, limit, period, ...options }) super({ baseURL, limit, period, ...options })
this.apiKey = apiKey this.apiKey = apiKey
} }
get (path, options = {}) { get(path, options = {}) {
const ClientError = this.errorClass const ClientError = this.errorClass
if (!this.apiKey) { if (!this.apiKey) {
return Promise.reject(new ClientError( return Promise.reject(
'No API key was configured for TheAudioDB client.' new ClientError('No API key was configured for TheAudioDB client.')
)) )
} }
return super.get(`${this.apiKey}/${path}`, { json: true, ...options }) return super.get(`${this.apiKey}/${path}`, { json: true, ...options })
} }
entity (entityType, mbid) { entity(entityType, mbid) {
const ClientError = this.errorClass const ClientError = this.errorClass
switch (entityType) { switch (entityType) {
case 'artist': case 'artist':
@ -32,39 +35,36 @@ export default class TheAudioDBClient extends Client {
case 'recording': case 'recording':
return this.track(mbid) return this.track(mbid)
default: default:
return Promise.reject(new ClientError( return Promise.reject(
`Entity type unsupported: ${entityType}` new ClientError(`Entity type unsupported: ${entityType}`)
)) )
} }
} }
artist (mbid) { artist(mbid) {
return this.get('artist-mb.php', { qs: { i: mbid } }) return this.get('artist-mb.php', { qs: { i: mbid } }).then(body => {
.then(body => { if (body.artists && body.artists.length === 1) {
if (body.artists && body.artists.length === 1) { return body.artists[0]
return body.artists[0] }
} return null
return null })
})
} }
album (mbid) { album(mbid) {
return this.get('album-mb.php', { qs: { i: mbid } }) return this.get('album-mb.php', { qs: { i: mbid } }).then(body => {
.then(body => { if (body.album && body.album.length === 1) {
if (body.album && body.album.length === 1) { return body.album[0]
return body.album[0] }
} return null
return null })
})
} }
track (mbid) { track(mbid) {
return this.get('track-mb.php', { qs: { i: mbid } }) return this.get('track-mb.php', { qs: { i: mbid } }).then(body => {
.then(body => { if (body.track && body.track.length === 1) {
if (body.track && body.track.length === 1) { return body.track[0]
return body.track[0] }
} return null
return null })
})
} }
} }

View file

@ -8,14 +8,18 @@ export default {
name: 'TheAudioDB', name: 'TheAudioDB',
description: `Retrieve images and information about artists, releases, and description: `Retrieve images and information about artists, releases, and
recordings from [TheAudioDB.com](http://www.theaudiodb.com/).`, recordings from [TheAudioDB.com](http://www.theaudiodb.com/).`,
extendContext (context, { theAudioDB = {} } = {}) { extendContext(context, { theAudioDB = {} } = {}) {
const client = new TheAudioDBClient(theAudioDB) const client = new TheAudioDBClient(theAudioDB)
const cacheSize = parseInt( const cacheSize = parseInt(
process.env.THEAUDIODB_CACHE_SIZE || process.env.GRAPHBRAINZ_CACHE_SIZE || 8192, process.env.THEAUDIODB_CACHE_SIZE ||
process.env.GRAPHBRAINZ_CACHE_SIZE ||
8192,
10 10
) )
const cacheTTL = parseInt( const cacheTTL = parseInt(
process.env.THEAUDIODB_CACHE_TTL || process.env.GRAPHBRAINZ_CACHE_TTL || ONE_DAY, process.env.THEAUDIODB_CACHE_TTL ||
process.env.GRAPHBRAINZ_CACHE_TTL ||
ONE_DAY,
10 10
) )
return { return {

View file

@ -3,12 +3,12 @@ import LRUCache from 'lru-cache'
const debug = require('debug')('graphbrainz:extensions/the-audio-db') const debug = require('debug')('graphbrainz:extensions/the-audio-db')
export default function createLoader (options) { export default function createLoader(options) {
const { client } = options const { client } = options
const cache = LRUCache({ const cache = LRUCache({
max: options.cacheSize, max: options.cacheSize,
maxAge: options.cacheTTL, maxAge: options.cacheTTL,
dispose (key) { dispose(key) {
debug(`Removed from cache. key=${key}`) debug(`Removed from cache. key=${key}`)
} }
}) })
@ -16,13 +16,18 @@ export default function createLoader (options) {
cache.delete = cache.del cache.delete = cache.del
cache.clear = cache.reset cache.clear = cache.reset
return new DataLoader(keys => { return new DataLoader(
return Promise.all(keys.map(key => { keys => {
const [ entityType, id ] = key return Promise.all(
return client.entity(entityType, id) keys.map(key => {
})) const [entityType, id] = key
}, { return client.entity(entityType, id)
cacheKeyFn: ([ entityType, id ]) => `${entityType}/${id}`, })
cacheMap: cache )
}) },
{
cacheKeyFn: ([entityType, id]) => `${entityType}/${id}`,
cacheMap: cache
}
)
} }

View file

@ -1,6 +1,6 @@
function handleImageSize (resolver) { function handleImageSize(resolver) {
return (source, args, context, info) => { return (source, args, context, info) => {
const getURL = (url) => args.size === 'PREVIEW' ? `${url}/preview` : url const getURL = url => (args.size === 'PREVIEW' ? `${url}/preview` : url)
const url = resolver(source, args, context, info) const url = resolver(source, args, context, info)
if (!url) { if (!url) {
return null return null

View file

@ -8,24 +8,26 @@ import { createContext } from './context'
const debug = require('debug')('graphbrainz') const debug = require('debug')('graphbrainz')
const formatError = (err) => ({ const formatError = err => ({
message: err.message, message: err.message,
locations: err.locations, locations: err.locations,
stack: err.stack stack: err.stack
}) })
const middleware = ({ const middleware = (
client = new MusicBrainz(), {
extensions = process.env.GRAPHBRAINZ_EXTENSIONS client = new MusicBrainz(),
? JSON.parse(process.env.GRAPHBRAINZ_EXTENSIONS) extensions = process.env.GRAPHBRAINZ_EXTENSIONS
: [ ? JSON.parse(process.env.GRAPHBRAINZ_EXTENSIONS)
'./extensions/cover-art-archive', : [
'./extensions/fanart-tv', './extensions/cover-art-archive',
'./extensions/mediawiki', './extensions/fanart-tv',
'./extensions/the-audio-db' './extensions/mediawiki',
], './extensions/the-audio-db'
...middlewareOptions ],
} = {}) => { ...middlewareOptions
} = {}
) => {
debug(`Loading ${extensions.length} extension(s).`) debug(`Loading ${extensions.length} extension(s).`)
const options = { const options = {
client, client,
@ -52,7 +54,7 @@ const middleware = ({
export default middleware export default middleware
export function start (options) { export function start(options) {
require('dotenv').config({ silent: true }) require('dotenv').config({ silent: true })
const app = express() const app = express()
const port = process.env.PORT || 3000 const port = process.env.PORT || 3000

View file

@ -5,13 +5,13 @@ import { ONE_DAY } from './util'
const debug = require('debug')('graphbrainz:loaders') const debug = require('debug')('graphbrainz:loaders')
export default function createLoaders (client) { export default function createLoaders(client) {
// All loaders share a single LRU cache that will remember 8192 responses, // All loaders share a single LRU cache that will remember 8192 responses,
// each cached for 1 day. // each cached for 1 day.
const cache = LRUCache({ const cache = LRUCache({
max: parseInt(process.env.GRAPHBRAINZ_CACHE_SIZE || 8192, 10), max: parseInt(process.env.GRAPHBRAINZ_CACHE_SIZE || 8192, 10),
maxAge: parseInt(process.env.GRAPHBRAINZ_CACHE_TTL || ONE_DAY, 10), maxAge: parseInt(process.env.GRAPHBRAINZ_CACHE_TTL || ONE_DAY, 10),
dispose (key) { dispose(key) {
debug(`Removed from cache. key=${key}`) debug(`Removed from cache. key=${key}`)
} }
}) })
@ -19,56 +19,71 @@ export default function createLoaders (client) {
cache.delete = cache.del cache.delete = cache.del
cache.clear = cache.reset cache.clear = cache.reset
const lookup = new DataLoader(keys => { const lookup = new DataLoader(
return Promise.all(keys.map(key => { keys => {
const [ entityType, id, params = {} ] = key return Promise.all(
return client.lookup(entityType, id, params).then(entity => { keys.map(key => {
if (entity) { const [entityType, id, params = {}] = key
// Store the entity type so we can determine what type of object this return client.lookup(entityType, id, params).then(entity => {
// is elsewhere in the code. if (entity) {
entity._type = entityType // Store the entity type so we can determine what type of object this
} // is elsewhere in the code.
return entity entity._type = entityType
}) }
})) return entity
}, { })
cacheKeyFn: (key) => client.getLookupURL(...key),
cacheMap: cache
})
const browse = new DataLoader(keys => {
return Promise.all(keys.map(key => {
const [ entityType, params = {} ] = key
return client.browse(entityType, params).then(list => {
list[toPlural(entityType)].forEach(entity => {
// Store the entity type so we can determine what type of object this
// is elsewhere in the code.
entity._type = entityType
}) })
return list )
}) },
})) {
}, { cacheKeyFn: key => client.getLookupURL(...key),
cacheKeyFn: (key) => client.getBrowseURL(...key), cacheMap: cache
cacheMap: cache }
}) )
const search = new DataLoader(keys => { const browse = new DataLoader(
return Promise.all(keys.map(key => { keys => {
const [ entityType, query, params = {} ] = key return Promise.all(
return client.search(entityType, query, params).then(list => { keys.map(key => {
list[toPlural(entityType)].forEach(entity => { const [entityType, params = {}] = key
// Store the entity type so we can determine what type of object this return client.browse(entityType, params).then(list => {
// is elsewhere in the code. list[toPlural(entityType)].forEach(entity => {
entity._type = entityType // Store the entity type so we can determine what type of object this
// is elsewhere in the code.
entity._type = entityType
})
return list
})
}) })
return list )
}) },
})) {
}, { cacheKeyFn: key => client.getBrowseURL(...key),
cacheKeyFn: key => client.getSearchURL(...key), cacheMap: cache
cacheMap: cache }
}) )
const search = new DataLoader(
keys => {
return Promise.all(
keys.map(key => {
const [entityType, query, params = {}] = key
return client.search(entityType, query, params).then(list => {
list[toPlural(entityType)].forEach(entity => {
// Store the entity type so we can determine what type of object this
// is elsewhere in the code.
entity._type = entityType
})
return list
})
})
)
},
{
cacheKeyFn: key => client.getSearchURL(...key),
cacheMap: cache
}
)
return { lookup, browse, search } return { lookup, browse, search }
} }

View file

@ -60,7 +60,7 @@ const work = {
description: 'The MBID of a work to which the entity is linked.' description: 'The MBID of a work to which the entity is linked.'
} }
function createBrowseField (connectionType, args) { function createBrowseField(connectionType, args) {
const typeName = toWords(connectionType.name.slice(0, -10)) const typeName = toWords(connectionType.name.slice(0, -10))
return { return {
type: connectionType, type: connectionType,
@ -173,7 +173,8 @@ release, but is not included in the credits for the release itself.`
export const browse = { export const browse = {
type: BrowseQuery, type: BrowseQuery,
description: 'Browse all MusicBrainz entities directly linked to another entity.', description:
'Browse all MusicBrainz entities directly linked to another entity.',
// We only have work to do once we know what entity types are being requested, // We only have work to do once we know what entity types are being requested,
// so this can just resolve to an empty object. // so this can just resolve to an empty object.
resolve: () => ({}) resolve: () => ({})

View file

@ -21,7 +21,7 @@ import {
Work Work
} from '../types' } from '../types'
function createLookupField (entity, args) { function createLookupField(entity, args) {
const typeName = toWords(entity.name) const typeName = toWords(entity.name)
return { return {
type: entity, type: entity,

View file

@ -16,7 +16,7 @@ import {
} from '../types' } from '../types'
import { toWords } from '../types/helpers' import { toWords } from '../types/helpers'
function createSearchField (connectionType) { function createSearchField(connectionType) {
const typeName = toWords(connectionType.name.slice(0, -10)) const typeName = toWords(connectionType.name.slice(0, -10))
return { return {
type: connectionType, type: connectionType,

View file

@ -1,12 +1,14 @@
const debug = require('debug')('graphbrainz:rate-limit') const debug = require('debug')('graphbrainz:rate-limit')
export default class RateLimit { export default class RateLimit {
constructor ({ constructor(
limit = 1, {
period = 1000, limit = 1,
concurrency = limit || 1, period = 1000,
defaultPriority = 1 concurrency = limit || 1,
} = {}) { defaultPriority = 1
} = {}
) {
this.limit = limit this.limit = limit
this.period = period this.period = period
this.defaultPriority = defaultPriority this.defaultPriority = defaultPriority
@ -20,16 +22,16 @@ export default class RateLimit {
this.prevTaskID = null this.prevTaskID = null
} }
nextTaskID (prevTaskID = this.prevTaskID) { nextTaskID(prevTaskID = this.prevTaskID) {
const id = (prevTaskID || 0) + 1 const id = (prevTaskID || 0) + 1
this.prevTaskID = id this.prevTaskID = id
return id return id
} }
enqueue (fn, args, priority = this.defaultPriority) { enqueue(fn, args, priority = this.defaultPriority) {
priority = Math.max(0, priority) priority = Math.max(0, priority)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const queue = this.queues[priority] = this.queues[priority] || [] const queue = (this.queues[priority] = this.queues[priority] || [])
const id = this.nextTaskID() const id = this.nextTaskID()
debug(`Enqueuing task. id=${id} priority=${priority}`) debug(`Enqueuing task. id=${id} priority=${priority}`)
queue.push({ fn, args, resolve, reject, id }) queue.push({ fn, args, resolve, reject, id })
@ -43,7 +45,7 @@ export default class RateLimit {
}) })
} }
dequeue () { dequeue() {
let task let task
for (let i = this.queues.length - 1; i >= 0; i--) { for (let i = this.queues.length - 1; i >= 0; i--) {
const queue = this.queues[i] const queue = this.queues[i]
@ -60,7 +62,7 @@ export default class RateLimit {
return task return task
} }
flush () { flush() {
if (this.numPending < this.concurrency && this.periodCapacity > 0) { if (this.numPending < this.concurrency && this.periodCapacity > 0) {
const task = this.dequeue() const task = this.dequeue()
if (task) { if (task) {
@ -83,12 +85,12 @@ export default class RateLimit {
} }
this.numPending += 1 this.numPending += 1
this.periodCapacity -= 1 this.periodCapacity -= 1
const onResolve = (value) => { const onResolve = value => {
this.numPending -= 1 this.numPending -= 1
resolve(value) resolve(value)
this.flush() this.flush()
} }
const onReject = (err) => { const onReject = err => {
this.numPending -= 1 this.numPending -= 1
reject(err) reject(err)
this.flush() this.flush()

View file

@ -6,7 +6,7 @@ import {
} from 'graphql-relay' } from 'graphql-relay'
import { getFields, extendIncludes } from './util' import { getFields, extendIncludes } from './util'
export function includeRelationships (params, info, fragments = info.fragments) { export function includeRelationships(params, info, fragments = info.fragments) {
let fields = getFields(info, fragments) let fields = getFields(info, fragments)
if (info.fieldName !== 'relationships') { if (info.fieldName !== 'relationships') {
if (fields.relationships) { if (fields.relationships) {
@ -36,7 +36,7 @@ export function includeRelationships (params, info, fragments = info.fragments)
return params return params
} }
export function includeSubqueries (params, info, fragments = info.fragments) { export function includeSubqueries(params, info, fragments = info.fragments) {
const subqueryIncludes = { const subqueryIncludes = {
aliases: ['aliases'], aliases: ['aliases'],
artistCredit: ['artist-credits'], artistCredit: ['artist-credits'],
@ -67,7 +67,7 @@ export function includeSubqueries (params, info, fragments = info.fragments) {
return params return params
} }
export function resolveLookup (root, { mbid, ...params }, { loaders }, info) { export function resolveLookup(root, { mbid, ...params }, { loaders }, info) {
if (!mbid && !params.resource) { if (!mbid && !params.resource) {
throw new Error('Lookups by a field other than MBID must provide: resource') throw new Error('Lookups by a field other than MBID must provide: resource')
} }
@ -77,16 +77,12 @@ export function resolveLookup (root, { mbid, ...params }, { loaders }, info) {
return loaders.lookup.load([entityType, mbid, params]) return loaders.lookup.load([entityType, mbid, params])
} }
export function resolveBrowse (root, { export function resolveBrowse(
first, root,
after, { first, after, type = [], status = [], discID, isrc, iswc, ...args },
type = [], { loaders },
status = [], info
discID, ) {
isrc,
iswc,
...args
}, { loaders }, info) {
const pluralName = toDashed(info.fieldName) const pluralName = toDashed(info.fieldName)
const singularName = toSingular(pluralName) const singularName = toSingular(pluralName)
let params = { let params = {
@ -127,7 +123,11 @@ export function resolveBrowse (root, {
[`${singularName}-count`]: arrayLength = arraySlice.length [`${singularName}-count`]: arrayLength = arraySlice.length
} = list } = list
const meta = { sliceStart, arrayLength } const meta = { sliceStart, arrayLength }
const connection = connectionFromArraySlice(arraySlice, { first, after }, meta) const connection = connectionFromArraySlice(
arraySlice,
{ first, after },
meta
)
return { return {
nodes: connection.edges.map(edge => edge.node), nodes: connection.edges.map(edge => edge.node),
totalCount: arrayLength, totalCount: arrayLength,
@ -136,12 +136,12 @@ export function resolveBrowse (root, {
}) })
} }
export function resolveSearch (root, { export function resolveSearch(
after, root,
first, { after, first, query, ...args },
query, { loaders },
...args info
}, { loaders }, info) { ) {
const pluralName = toDashed(info.fieldName) const pluralName = toDashed(info.fieldName)
const singularName = toSingular(pluralName) const singularName = toSingular(pluralName)
let params = { let params = {
@ -157,10 +157,17 @@ export function resolveSearch (root, {
count: arrayLength count: arrayLength
} = list } = list
const meta = { sliceStart, arrayLength } const meta = { sliceStart, arrayLength }
const connection = connectionFromArraySlice(arraySlice, { first, after }, meta) const connection = connectionFromArraySlice(
arraySlice,
{ first, after },
meta
)
// Move the `score` field up to the edge object and make sure it's a // Move the `score` field up to the edge object and make sure it's a
// number (MusicBrainz returns a string). // number (MusicBrainz returns a string).
const edges = connection.edges.map(edge => ({ ...edge, score: +edge.node.score })) const edges = connection.edges.map(edge => ({
...edge,
score: +edge.node.score
}))
const connectionWithExtras = { const connectionWithExtras = {
nodes: edges.map(edge => edge.node), nodes: edges.map(edge => edge.node),
totalCount: arrayLength, totalCount: arrayLength,
@ -171,7 +178,7 @@ export function resolveSearch (root, {
}) })
} }
export function resolveRelationship (rels, args, context, info) { export function resolveRelationship(rels, args, context, info) {
const targetType = toDashed(toSingular(info.fieldName)).replace('-', '_') const targetType = toDashed(toSingular(info.fieldName)).replace('-', '_')
let matches = rels.filter(rel => rel['target-type'] === targetType) let matches = rels.filter(rel => rel['target-type'] === targetType)
// There's no way to filter these at the API level, so do it here. // There's no way to filter these at the API level, so do it here.
@ -192,7 +199,7 @@ export function resolveRelationship (rels, args, context, info) {
} }
} }
export function resolveLinked (entity, args, context, info) { export function resolveLinked(entity, args, context, info) {
const parentEntity = toDashed(info.parentType.name) const parentEntity = toDashed(info.parentType.name)
args = { ...args, [parentEntity]: entity.id } args = { ...args, [parentEntity]: entity.id }
return resolveBrowse(entity, args, context, info) return resolveBrowse(entity, args, context, info)
@ -203,7 +210,10 @@ export function resolveLinked (entity, args, context, info) {
* for a particular field that's being requested, make another request to grab * for a particular field that's being requested, make another request to grab
* it (after making sure it isn't already available). * it (after making sure it isn't already available).
*/ */
export function createSubqueryResolver ({ inc, key } = {}, handler = value => value) { export function createSubqueryResolver(
{ inc, key } = {},
handler = value => value
) {
return (entity, args, { loaders }, info) => { return (entity, args, { loaders }, info) => {
key = key || toDashed(info.fieldName) key = key || toDashed(info.fieldName)
let promise let promise
@ -218,7 +228,7 @@ export function createSubqueryResolver ({ inc, key } = {}, handler = value => va
} }
} }
export function resolveDiscReleases (disc, args, context, info) { export function resolveDiscReleases(disc, args, context, info) {
const { releases } = disc const { releases } = disc
if (releases != null) { if (releases != null) {
const connection = connectionFromArray(releases, args) const connection = connectionFromArray(releases, args)

View file

@ -5,11 +5,13 @@ import { nodeField } from './types/node'
const debug = require('debug')('graphbrainz:schema') const debug = require('debug')('graphbrainz:schema')
export function applyExtension (extension, schema, options = {}) { export function applyExtension(extension, schema, options = {}) {
let outputSchema = schema let outputSchema = schema
if (extension.extendSchema) { if (extension.extendSchema) {
if (typeof extension.extendSchema === 'object') { if (typeof extension.extendSchema === 'object') {
debug(`Extending schema via an object from the “${extension.name}” extension.`) debug(
`Extending schema via an object from the “${extension.name}” extension.`
)
const { schemas = [], resolvers } = extension.extendSchema const { schemas = [], resolvers } = extension.extendSchema
outputSchema = schemas.reduce((updatedSchema, extensionSchema) => { outputSchema = schemas.reduce((updatedSchema, extensionSchema) => {
if (typeof extensionSchema === 'string') { if (typeof extensionSchema === 'string') {
@ -21,12 +23,16 @@ export function applyExtension (extension, schema, options = {}) {
addResolveFunctionsToSchema(outputSchema, resolvers) addResolveFunctionsToSchema(outputSchema, resolvers)
} }
} else if (typeof extension.extendSchema === 'function') { } else if (typeof extension.extendSchema === 'function') {
debug(`Extending schema via a function from the “${extension.name}” extension.`) debug(
`Extending schema via a function from the “${
extension.name
} extension.`
)
outputSchema = extension.extendSchema(schema, options) outputSchema = extension.extendSchema(schema, options)
} else { } else {
throw new Error( throw new Error(
`The “${extension.name}” extension contains an invalid ` + `The “${extension.name}” extension contains an invalid ` +
`\`extendSchema\` value: ${extension.extendSchema}` `\`extendSchema\` value: ${extension.extendSchema}`
) )
} }
} }
@ -38,7 +44,7 @@ export function applyExtension (extension, schema, options = {}) {
return outputSchema return outputSchema
} }
export function createSchema (schema, options = {}) { export function createSchema(schema, options = {}) {
const extensions = options.extensions || [] const extensions = options.extensions || []
return extensions.reduce((updatedSchema, extension) => { return extensions.reduce((updatedSchema, extension) => {
return applyExtension(extension, updatedSchema, options) return applyExtension(extension, updatedSchema, options)

View file

@ -1,7 +1,4 @@
import { import { GraphQLObjectType, GraphQLBoolean } from 'graphql/type'
GraphQLObjectType,
GraphQLBoolean
} from 'graphql/type'
import { Locale } from './scalars' import { Locale } from './scalars'
import { name, sortName, fieldWithID } from './helpers' import { name, sortName, fieldWithID } from './helpers'

View file

@ -13,7 +13,7 @@ track, etc., and join phrases between them.`,
type: Artist, type: Artist,
description: `The entity representing the artist referenced in the description: `The entity representing the artist referenced in the
credits.`, credits.`,
resolve: (source) => { resolve: source => {
const { artist } = source const { artist } = source
if (artist) { if (artist) {
artist._type = 'artist' artist._type = 'artist'

View file

@ -1,8 +1,4 @@
import { import { GraphQLObjectType, GraphQLNonNull, GraphQLString } from 'graphql/type'
GraphQLObjectType,
GraphQLNonNull,
GraphQLString
} from 'graphql/type'
import Node from './node' import Node from './node'
import Entity from './entity' import Entity from './entity'
import { import {

View file

@ -4,7 +4,7 @@ import { mbid, connectionWithExtras } from './helpers'
const Entity = new GraphQLInterfaceType({ const Entity = new GraphQLInterfaceType({
name: 'Entity', name: 'Entity',
description: 'An entity in the MusicBrainz schema.', description: 'An entity in the MusicBrainz schema.',
resolveType (value) { resolveType(value) {
if (value._type && require.resolve(`./${value._type}`)) { if (value._type && require.resolve(`./${value._type}`)) {
return require(`./${value._type}`).default return require(`./${value._type}`).default
} }

View file

@ -18,7 +18,8 @@ distinctive name.`,
}, },
ORCHESTRA: { ORCHESTRA: {
name: 'Orchestra', name: 'Orchestra',
description: 'This indicates an orchestra (a large instrumental ensemble).', description:
'This indicates an orchestra (a large instrumental ensemble).',
value: 'Orchestra' value: 'Orchestra'
}, },
CHOIR: { CHOIR: {

View file

@ -45,7 +45,8 @@ artists and works. See the [setlist documentation](https://musicbrainz.org/doc/E
for syntax and examples.` for syntax and examples.`
}, },
...fieldWithID('type', { ...fieldWithID('type', {
description: 'What kind of event the event is, e.g. concert, festival, etc.' description:
'What kind of event the event is, e.g. concert, festival, etc.'
}), }),
relationships, relationships,
collections, collections,

View file

@ -44,15 +44,15 @@ import {
export const toPascal = pascalCase export const toPascal = pascalCase
export const toDashed = dashify export const toDashed = dashify
export function toPlural (name) { export function toPlural(name) {
return name.endsWith('s') ? name : name + 's' return name.endsWith('s') ? name : name + 's'
} }
export function toSingular (name) { export function toSingular(name) {
return name.endsWith('s') && !/series/i.test(name) ? name.slice(0, -1) : name return name.endsWith('s') && !/series/i.test(name) ? name.slice(0, -1) : name
} }
export function toWords (name) { export function toWords(name) {
return toPascal(name).replace(/([^A-Z])?([A-Z]+)/g, (match, tail, head) => { return toPascal(name).replace(/([^A-Z])?([A-Z]+)/g, (match, tail, head) => {
tail = tail ? tail + ' ' : '' tail = tail ? tail + ' ' : ''
head = head.length > 1 ? head : head.toLowerCase() head = head.length > 1 ? head : head.toLowerCase()
@ -60,13 +60,13 @@ export function toWords (name) {
}) })
} }
export function resolveHyphenated (obj, args, context, info) { export function resolveHyphenated(obj, args, context, info) {
const name = toDashed(info.fieldName) const name = toDashed(info.fieldName)
return obj[name] return obj[name]
} }
export function resolveWithFallback (keys) { export function resolveWithFallback(keys) {
return (obj) => { return obj => {
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
const key = keys[i] const key = keys[i]
if (key in obj) { if (key in obj) {
@ -76,7 +76,7 @@ export function resolveWithFallback (keys) {
} }
} }
export function fieldWithID (name, config = {}) { export function fieldWithID(name, config = {}) {
config = { config = {
type: GraphQLString, type: GraphQLString,
resolve: resolveHyphenated, resolve: resolveHyphenated,
@ -95,7 +95,8 @@ field.`,
if (fieldName in entity) { if (fieldName in entity) {
return entity[fieldName] return entity[fieldName]
} }
return loaders.lookup.load([entity._type, entity.id]) return loaders.lookup
.load([entity._type, entity.id])
.then(data => data[fieldName]) .then(data => data[fieldName])
} }
} }
@ -105,7 +106,7 @@ field.`,
} }
} }
export function createCollectionField (config) { export function createCollectionField(config) {
const typeName = toPlural(toWords(config.type.name.slice(0, -10))) const typeName = toPlural(toWords(config.type.name.slice(0, -10)))
return { return {
...config, ...config,
@ -145,7 +146,7 @@ meaning depends on the type of entity.`,
resolve: resolveHyphenated resolve: resolveHyphenated
} }
function linkedQuery (connectionType, { args, ...config } = {}) { function linkedQuery(connectionType, { args, ...config } = {}) {
const typeName = toPlural(toWords(connectionType.name.slice(0, -10))) const typeName = toPlural(toWords(connectionType.name.slice(0, -10)))
return { return {
type: connectionType, type: connectionType,
@ -293,7 +294,7 @@ export const score = {
these results were found through a search.` these results were found through a search.`
} }
export function connectionWithExtras (nodeType) { export function connectionWithExtras(nodeType) {
return connectionDefinitions({ return connectionDefinitions({
nodeType, nodeType,
connectionFields: () => ({ connectionFields: () => ({

View file

@ -12,7 +12,10 @@ export { default as Label, LabelConnection } from './label'
export { default as Place, PlaceConnection } from './place' export { default as Place, PlaceConnection } from './place'
export { default as Recording, RecordingConnection } from './recording' export { default as Recording, RecordingConnection } from './recording'
export { default as Release, ReleaseConnection } from './release' export { default as Release, ReleaseConnection } from './release'
export { default as ReleaseGroup, ReleaseGroupConnection } from './release-group' export {
default as ReleaseGroup,
ReleaseGroupConnection
} from './release-group'
export { default as Series, SeriesConnection } from './series' export { default as Series, SeriesConnection } from './series'
export { default as Tag, TagConnection } from './tag' export { default as Tag, TagConnection } from './tag'
export { default as URL, URLConnection } from './url' export { default as URL, URLConnection } from './url'

View file

@ -35,7 +35,8 @@ multi-disc release).`
}, },
discs: { discs: {
type: new GraphQLList(Disc), type: new GraphQLList(Disc),
description: 'A list of physical discs and their disc IDs for this medium.' description:
'A list of physical discs and their disc IDs for this medium.'
} }
}) })
}) })

View file

@ -13,7 +13,7 @@ const { nodeInterface, nodeField } = nodeDefinitions(
const entityType = toDashed(type) const entityType = toDashed(type)
return loaders.lookup.load([entityType, id]) return loaders.lookup.load([entityType, id])
}, },
(obj) => { obj => {
const type = TYPE_MODULES[obj._type] || obj._type const type = TYPE_MODULES[obj._type] || obj._type
try { try {
return require(`./${type}`).default return require(`./${type}`).default

View file

@ -1,8 +1,4 @@
import { import { GraphQLObjectType, GraphQLList, GraphQLBoolean } from 'graphql/type'
GraphQLObjectType,
GraphQLList,
GraphQLBoolean
} from 'graphql/type'
import Node from './node' import Node from './node'
import Entity from './entity' import Entity from './entity'
import { Duration, ISRC } from './scalars' import { Duration, ISRC } from './scalars'

View file

@ -7,11 +7,7 @@ import {
} from 'graphql/type' } from 'graphql/type'
import { DateType } from './scalars' import { DateType } from './scalars'
import Entity from './entity' import Entity from './entity'
import { import { resolveHyphenated, fieldWithID, connectionWithExtras } from './helpers'
resolveHyphenated,
fieldWithID,
connectionWithExtras
} from './helpers'
const Relationship = new GraphQLObjectType({ const Relationship = new GraphQLObjectType({
name: 'Relationship', name: 'Relationship',
@ -35,7 +31,8 @@ other and to URLs outside MusicBrainz.`,
}, },
targetType: { targetType: {
type: new GraphQLNonNull(GraphQLString), type: new GraphQLNonNull(GraphQLString),
description: 'The type of entity on the receiving end of the relationship.', description:
'The type of entity on the receiving end of the relationship.',
resolve: resolveHyphenated resolve: resolveHyphenated
}, },
sourceCredit: { sourceCredit: {
@ -56,7 +53,8 @@ from its main (performance) name.`,
}, },
end: { end: {
type: DateType, type: DateType,
description: 'The date on which the relationship became no longer applicable.' description:
'The date on which the relationship became no longer applicable.'
}, },
ended: { ended: {
type: GraphQLBoolean, type: GraphQLBoolean,

View file

@ -1,8 +1,4 @@
import { import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql/type'
GraphQLObjectType,
GraphQLString,
GraphQLList
} from 'graphql/type'
import Node from './node' import Node from './node'
import Entity from './entity' import Entity from './entity'
import { ASIN, DateType } from './scalars' import { ASIN, DateType } from './scalars'

View file

@ -1,11 +1,11 @@
import { Kind } from 'graphql/language' import { Kind } from 'graphql/language'
import { GraphQLScalarType } from 'graphql/type' import { GraphQLScalarType } from 'graphql/type'
function createScalar (config) { function createScalar(config) {
return new GraphQLScalarType({ return new GraphQLScalarType({
serialize: value => value, serialize: value => value,
parseValue: value => value, parseValue: value => value,
parseLiteral (ast) { parseLiteral(ast) {
if (ast.kind === Kind.STRING) { if (ast.kind === Kind.STRING) {
return ast.value return ast.value
} }
@ -20,28 +20,28 @@ const locale = /^([a-z]{2})(_[A-Z]{2})?(\.[a-zA-Z0-9-]+)?$/
// Be extremely lenient; just prevent major input errors. // Be extremely lenient; just prevent major input errors.
const url = /^\w+:\/\/[\w-]+\.\w+/ const url = /^\w+:\/\/[\w-]+\.\w+/
function validateMBID (value) { function validateMBID(value) {
if (typeof value === 'string' && uuid.test(value)) { if (typeof value === 'string' && uuid.test(value)) {
return value return value
} }
throw new TypeError(`Malformed MBID: ${value}`) throw new TypeError(`Malformed MBID: ${value}`)
} }
function validatePositive (value) { function validatePositive(value) {
if (value >= 0) { if (value >= 0) {
return value return value
} }
throw new TypeError(`Expected positive value: ${value}`) throw new TypeError(`Expected positive value: ${value}`)
} }
function validateLocale (value) { function validateLocale(value) {
if (typeof value === 'string' && locale.test(value)) { if (typeof value === 'string' && locale.test(value)) {
return value return value
} }
throw new TypeError(`Malformed locale: ${value}`) throw new TypeError(`Malformed locale: ${value}`)
} }
function validateURL (value) { function validateURL(value) {
if (typeof value === 'string' && url.test(value)) { if (typeof value === 'string' && url.test(value)) {
return value return value
} }
@ -57,7 +57,8 @@ and its partners for product identification within the Amazon organization.`
export const DateType = createScalar({ export const DateType = createScalar({
name: 'Date', name: 'Date',
description: 'Year, month (optional), and day (optional) in YYYY-MM-DD format.' description:
'Year, month (optional), and day (optional) in YYYY-MM-DD format.'
}) })
export const Degrees = createScalar({ export const Degrees = createScalar({
@ -87,7 +88,7 @@ export const Duration = createScalar({
description: 'A length of time, in milliseconds.', description: 'A length of time, in milliseconds.',
serialize: validatePositive, serialize: validatePositive,
parseValue: validatePositive, parseValue: validatePositive,
parseLiteral (ast) { parseLiteral(ast) {
if (ast.kind === Kind.INT) { if (ast.kind === Kind.INT) {
return validatePositive(parseInt(ast.value, 10)) return validatePositive(parseInt(ast.value, 10))
} }
@ -135,7 +136,7 @@ export const Locale = createScalar({
description: 'Language code, optionally with country and encoding.', description: 'Language code, optionally with country and encoding.',
serialize: validateLocale, serialize: validateLocale,
parseValue: validateLocale, parseValue: validateLocale,
parseLiteral (ast) { parseLiteral(ast) {
if (ast.kind === Kind.STRING) { if (ast.kind === Kind.STRING) {
return validateLocale(ast.value) return validateLocale(ast.value)
} }
@ -149,7 +150,7 @@ export const MBID = createScalar({
36-character UUIDs.`, 36-character UUIDs.`,
serialize: validateMBID, serialize: validateMBID,
parseValue: validateMBID, parseValue: validateMBID,
parseLiteral (ast) { parseLiteral(ast) {
if (ast.kind === Kind.STRING) { if (ast.kind === Kind.STRING) {
return validateMBID(ast.value) return validateMBID(ast.value)
} }
@ -167,7 +168,7 @@ export const URLString = createScalar({
description: 'A web address.', description: 'A web address.',
serialize: validateURL, serialize: validateURL,
parseValue: validateURL, parseValue: validateURL,
parseLiteral (ast) { parseLiteral(ast) {
if (ast.kind === Kind.STRING) { if (ast.kind === Kind.STRING) {
return validateURL(ast.value) return validateURL(ast.value)
} }

View file

@ -2,7 +2,7 @@ import util from 'util'
export const ONE_DAY = 24 * 60 * 60 * 1000 export const ONE_DAY = 24 * 60 * 60 * 1000
export function getFields (info, fragments = info.fragments) { export function getFields(info, fragments = info.fragments) {
if (info.kind !== 'Field') { if (info.kind !== 'Field') {
info = info.fieldNodes[0] info = info.fieldNodes[0]
} }
@ -25,17 +25,18 @@ export function getFields (info, fragments = info.fragments) {
return selections.reduce(reducer, {}) return selections.reduce(reducer, {})
} }
export function prettyPrint (obj, { depth = 5, export function prettyPrint(
colors = true, obj,
breakLength = 120 } = {}) { { depth = 5, colors = true, breakLength = 120 } = {}
) {
console.log(util.inspect(obj, { depth, colors, breakLength })) console.log(util.inspect(obj, { depth, colors, breakLength }))
} }
export function toFilteredArray (obj) { export function toFilteredArray(obj) {
return (Array.isArray(obj) ? obj : [obj]).filter(x => x) return (Array.isArray(obj) ? obj : [obj]).filter(x => x)
} }
export function extendIncludes (includes, moreIncludes) { export function extendIncludes(includes, moreIncludes) {
includes = toFilteredArray(includes) includes = toFilteredArray(includes)
moreIncludes = toFilteredArray(moreIncludes) moreIncludes = toFilteredArray(moreIncludes)
const seen = {} const seen = {}

View file

@ -3,7 +3,10 @@ import Client from '../../src/api/client'
test('parseErrorMessage() returns the body or status code', t => { test('parseErrorMessage() returns the body or status code', t => {
const client = new Client() const client = new Client()
t.is(client.parseErrorMessage({ statusCode: 500 }, 'something went wrong'), 'something went wrong') t.is(
client.parseErrorMessage({ statusCode: 500 }, 'something went wrong'),
'something went wrong'
)
t.is(client.parseErrorMessage({ statusCode: 500 }, ''), '500') t.is(client.parseErrorMessage({ statusCode: 500 }, ''), '500')
t.is(client.parseErrorMessage({ statusCode: 404 }, {}), '404') t.is(client.parseErrorMessage({ statusCode: 404 }, {}), '404')
}) })

View file

@ -3,28 +3,39 @@ import MusicBrainz, { MusicBrainzError } from '../../src/api'
import client from '../helpers/client/musicbrainz' import client from '../helpers/client/musicbrainz'
test('getLookupURL() generates a lookup URL', t => { test('getLookupURL() generates a lookup URL', t => {
t.is(client.getLookupURL('artist', 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8', { t.is(
inc: ['recordings', 'release-groups'] client.getLookupURL('artist', 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8', {
}), 'artist/c8da2e40-bd28-4d4e-813a-bd2f51958ba8?inc=recordings%2Brelease-groups') inc: ['recordings', 'release-groups']
}),
'artist/c8da2e40-bd28-4d4e-813a-bd2f51958ba8?inc=recordings%2Brelease-groups'
)
}) })
test('getBrowseURL() generates a browse URL', t => { test('getBrowseURL() generates a browse URL', t => {
t.is(client.getBrowseURL('recording', { t.is(
artist: 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8', client.getBrowseURL('recording', {
limit: null, artist: 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8',
offset: 0 limit: null,
}), 'recording?artist=c8da2e40-bd28-4d4e-813a-bd2f51958ba8&offset=0') offset: 0
}),
'recording?artist=c8da2e40-bd28-4d4e-813a-bd2f51958ba8&offset=0'
)
}) })
test('getSearchURL() generates a search URL', t => { test('getSearchURL() generates a search URL', t => {
t.is(client.getSearchURL('artist', 'Lures', { inc: null }), 'artist?query=Lures') t.is(
client.getSearchURL('artist', 'Lures', { inc: null }),
'artist?query=Lures'
)
}) })
test('lookup() sends a lookup query', t => { test('lookup() sends a lookup query', t => {
return client.lookup('artist', 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8').then(response => { return client
t.is(response.id, 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8') .lookup('artist', 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8')
t.is(response.type, 'Group') .then(response => {
}) t.is(response.id, 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8')
t.is(response.type, 'Group')
})
}) })
test('rejects the promise when the API returns an error', t => { test('rejects the promise when the API returns an error', t => {
@ -60,7 +71,10 @@ test('shouldRetry() retries only transient local connection issues', t => {
test('rejects non-MusicBrainz errors', t => { test('rejects non-MusicBrainz errors', t => {
const client = new MusicBrainz({ baseURL: '$!@#$' }) const client = new MusicBrainz({ baseURL: '$!@#$' })
return t.throws(client.get('artist/5b11f4ce-a62d-471e-81fc-a69a8278c7da'), Error) return t.throws(
client.get('artist/5b11f4ce-a62d-471e-81fc-a69a8278c7da'),
Error
)
}) })
test('uses the default error impementation if there is no JSON error', t => { test('uses the default error impementation if there is no JSON error', t => {

View file

@ -2,23 +2,35 @@ import test from 'ava'
import client from '../../helpers/client/cover-art-archive' import client from '../../helpers/client/cover-art-archive'
test('can retrieve a front image URL', t => { test('can retrieve a front image URL', t => {
return client.imageURL('release', '76df3287-6cda-33eb-8e9a-044b5e15ffdd', 'front') return client
.imageURL('release', '76df3287-6cda-33eb-8e9a-044b5e15ffdd', 'front')
.then(url => { .then(url => {
t.is(url, 'http://archive.org/download/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd-829521842.jpg') t.is(
url,
'http://archive.org/download/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd-829521842.jpg'
)
}) })
}) })
test('can retrieve a back image URL', t => { test('can retrieve a back image URL', t => {
return client.imageURL('release', '76df3287-6cda-33eb-8e9a-044b5e15ffdd', 'back') return client
.imageURL('release', '76df3287-6cda-33eb-8e9a-044b5e15ffdd', 'back')
.then(url => { .then(url => {
t.is(url, 'http://archive.org/download/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd-5769317885.jpg') t.is(
url,
'http://archive.org/download/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd/mbid-76df3287-6cda-33eb-8e9a-044b5e15ffdd-5769317885.jpg'
)
}) })
}) })
test('can retrieve a list of release images', t => { test('can retrieve a list of release images', t => {
return client.images('release', '76df3287-6cda-33eb-8e9a-044b5e15ffdd') return client
.images('release', '76df3287-6cda-33eb-8e9a-044b5e15ffdd')
.then(data => { .then(data => {
t.is(data.release, 'http://musicbrainz.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd') t.is(
data.release,
'http://musicbrainz.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd'
)
t.true(data.images.length >= 3) t.true(data.images.length >= 3)
data.images.forEach(image => { data.images.forEach(image => {
t.true(image.approved) t.true(image.approved)

View file

@ -7,7 +7,7 @@ import baseContext from '../../helpers/context'
const schema = applyExtension(extension, baseSchema) const schema = applyExtension(extension, baseSchema)
const context = extension.extendContext(baseContext) const context = extension.extendContext(baseContext)
function testData (t, query, handler) { function testData(t, query, handler) {
return graphql(schema, query, null, context).then(result => { return graphql(schema, query, null, context).then(result => {
if (result.errors !== undefined) { if (result.errors !== undefined) {
console.log(result.errors) console.log(result.errors)
@ -17,7 +17,10 @@ function testData (t, query, handler) {
}) })
} }
test('releases have a cover art summary', testData, ` test(
'releases have a cover art summary',
testData,
`
{ {
lookup { lookup {
release(mbid: "b84ee12a-09ef-421b-82de-0441a926375b") { release(mbid: "b84ee12a-09ef-421b-82de-0441a926375b") {
@ -28,13 +31,18 @@ test('releases have a cover art summary', testData, `
} }
} }
} }
`, (t, data) => { `,
const { coverArtArchive } = data.lookup.release (t, data) => {
t.true(coverArtArchive.artwork) const { coverArtArchive } = data.lookup.release
t.true(coverArtArchive.count >= 10) t.true(coverArtArchive.artwork)
}) t.true(coverArtArchive.count >= 10)
}
)
test('releases have a set of cover art images', testData, ` test(
'releases have a set of cover art images',
testData,
`
{ {
lookup { lookup {
release(mbid: "b84ee12a-09ef-421b-82de-0441a926375b") { release(mbid: "b84ee12a-09ef-421b-82de-0441a926375b") {
@ -59,28 +67,55 @@ test('releases have a set of cover art images', testData, `
} }
} }
} }
`, (t, data) => { `,
const { coverArtArchive } = data.lookup.release (t, data) => {
t.is(coverArtArchive.front, 'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/1611507818.jpg') const { coverArtArchive } = data.lookup.release
t.is(coverArtArchive.back, 'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/13536418798.jpg') t.is(
t.true(coverArtArchive.images.length >= 10) coverArtArchive.front,
t.true(coverArtArchive.images.some(image => image.front === true)) 'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/1611507818.jpg'
t.true(coverArtArchive.images.some(image => image.back === true)) )
t.true(coverArtArchive.images.some(image => image.types.indexOf('Front') >= 0)) t.is(
t.true(coverArtArchive.images.some(image => image.types.indexOf('Back') >= 0)) coverArtArchive.back,
t.true(coverArtArchive.images.some(image => image.types.indexOf('Liner') >= 0)) 'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/13536418798.jpg'
t.true(coverArtArchive.images.some(image => image.types.indexOf('Poster') >= 0)) )
t.true(coverArtArchive.images.some(image => image.types.indexOf('Medium') >= 0)) t.true(coverArtArchive.images.length >= 10)
t.true(coverArtArchive.images.some(image => image.edit === 18544122)) t.true(coverArtArchive.images.some(image => image.front === true))
t.true(coverArtArchive.images.some(image => image.comment === '')) t.true(coverArtArchive.images.some(image => image.back === true))
t.true(coverArtArchive.images.some(image => image.fileID === '1611507818')) t.true(
t.true(coverArtArchive.images.some(image => image.image === 'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/13536422691.jpg')) coverArtArchive.images.some(image => image.types.indexOf('Front') >= 0)
t.true(coverArtArchive.images.every(image => image.approved === true)) )
t.true(coverArtArchive.images.every(image => image.thumbnails.small)) t.true(
t.true(coverArtArchive.images.every(image => image.thumbnails.large)) coverArtArchive.images.some(image => image.types.indexOf('Back') >= 0)
}) )
t.true(
coverArtArchive.images.some(image => image.types.indexOf('Liner') >= 0)
)
t.true(
coverArtArchive.images.some(image => image.types.indexOf('Poster') >= 0)
)
t.true(
coverArtArchive.images.some(image => image.types.indexOf('Medium') >= 0)
)
t.true(coverArtArchive.images.some(image => image.edit === 18544122))
t.true(coverArtArchive.images.some(image => image.comment === ''))
t.true(coverArtArchive.images.some(image => image.fileID === '1611507818'))
t.true(
coverArtArchive.images.some(
image =>
image.image ===
'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/13536422691.jpg'
)
)
t.true(coverArtArchive.images.every(image => image.approved === true))
t.true(coverArtArchive.images.every(image => image.thumbnails.small))
t.true(coverArtArchive.images.every(image => image.thumbnails.large))
}
)
test('can request a size for front and back cover art', testData, ` test(
'can request a size for front and back cover art',
testData,
`
{ {
lookup { lookup {
release(mbid: "b84ee12a-09ef-421b-82de-0441a926375b") { release(mbid: "b84ee12a-09ef-421b-82de-0441a926375b") {
@ -92,14 +127,28 @@ test('can request a size for front and back cover art', testData, `
} }
} }
} }
`, (t, data) => { `,
const { coverArtArchive } = data.lookup.release (t, data) => {
t.is(coverArtArchive.front, 'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/1611507818-500.jpg') const { coverArtArchive } = data.lookup.release
t.is(coverArtArchive.back, 'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/13536418798-250.jpg') t.is(
t.is(coverArtArchive.fullFront, 'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/1611507818.jpg') coverArtArchive.front,
}) 'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/1611507818-500.jpg'
)
t.is(
coverArtArchive.back,
'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/13536418798-250.jpg'
)
t.is(
coverArtArchive.fullFront,
'http://coverartarchive.org/release/b84ee12a-09ef-421b-82de-0441a926375b/1611507818.jpg'
)
}
)
test('release groups have a front cover art image', testData, ` test(
'release groups have a front cover art image',
testData,
`
{ {
lookup { lookup {
releaseGroup(mbid: "f5093c06-23e3-404f-aeaa-40f72885ee3a") { releaseGroup(mbid: "f5093c06-23e3-404f-aeaa-40f72885ee3a") {
@ -118,17 +167,25 @@ test('release groups have a front cover art image', testData, `
} }
} }
} }
`, (t, data) => { `,
const { coverArtArchive } = data.lookup.releaseGroup (t, data) => {
t.true(coverArtArchive.artwork) const { coverArtArchive } = data.lookup.releaseGroup
t.is(coverArtArchive.front, 'http://coverartarchive.org/release/25fbfbb4-b1ee-4448-aadf-ae3bc2e2dd27/1675312275.jpg') t.true(coverArtArchive.artwork)
t.is(coverArtArchive.release.mbid, '25fbfbb4-b1ee-4448-aadf-ae3bc2e2dd27') t.is(
t.is(coverArtArchive.release.title, 'The Dark Side of the Moon') coverArtArchive.front,
t.is(coverArtArchive.images.length, 1) 'http://coverartarchive.org/release/25fbfbb4-b1ee-4448-aadf-ae3bc2e2dd27/1675312275.jpg'
t.true(coverArtArchive.images[0].front) )
}) t.is(coverArtArchive.release.mbid, '25fbfbb4-b1ee-4448-aadf-ae3bc2e2dd27')
t.is(coverArtArchive.release.title, 'The Dark Side of the Moon')
t.is(coverArtArchive.images.length, 1)
t.true(coverArtArchive.images[0].front)
}
)
test('release groups have different cover art sizes available', testData, ` test(
'release groups have different cover art sizes available',
testData,
`
{ {
lookup { lookup {
releaseGroup(mbid: "f5093c06-23e3-404f-aeaa-40f72885ee3a") { releaseGroup(mbid: "f5093c06-23e3-404f-aeaa-40f72885ee3a") {
@ -139,13 +196,24 @@ test('release groups have different cover art sizes available', testData, `
} }
} }
} }
`, (t, data) => { `,
const { coverArtArchive } = data.lookup.releaseGroup (t, data) => {
t.is(coverArtArchive.small, 'http://coverartarchive.org/release/25fbfbb4-b1ee-4448-aadf-ae3bc2e2dd27/1675312275-250.jpg') const { coverArtArchive } = data.lookup.releaseGroup
t.is(coverArtArchive.large, 'http://coverartarchive.org/release/25fbfbb4-b1ee-4448-aadf-ae3bc2e2dd27/1675312275-500.jpg') t.is(
}) coverArtArchive.small,
'http://coverartarchive.org/release/25fbfbb4-b1ee-4448-aadf-ae3bc2e2dd27/1675312275-250.jpg'
)
t.is(
coverArtArchive.large,
'http://coverartarchive.org/release/25fbfbb4-b1ee-4448-aadf-ae3bc2e2dd27/1675312275-500.jpg'
)
}
)
test('can retrieve cover art in searches', testData, ` test(
'can retrieve cover art in searches',
testData,
`
{ {
search { search {
releases(query: "You Want It Darker") { releases(query: "You Want It Darker") {
@ -164,11 +232,13 @@ test('can retrieve cover art in searches', testData, `
} }
} }
} }
`, (t, data) => { `,
const releases = data.search.releases.edges.map(edge => edge.node) (t, data) => {
t.is(releases.length, 25) const releases = data.search.releases.edges.map(edge => edge.node)
t.true(releases.some(release => release.coverArtArchive.artwork === true)) t.is(releases.length, 25)
t.true(releases.some(release => release.coverArtArchive.images.length > 0)) t.true(releases.some(release => release.coverArtArchive.artwork === true))
t.true(releases.some(release => release.coverArtArchive.front === null)) t.true(releases.some(release => release.coverArtArchive.images.length > 0))
t.true(releases.some(release => release.coverArtArchive.back === null)) t.true(releases.some(release => release.coverArtArchive.front === null))
}) t.true(releases.some(release => release.coverArtArchive.back === null))
}
)

View file

@ -7,7 +7,7 @@ import baseContext from '../../helpers/context'
const schema = applyExtension(extension, baseSchema) const schema = applyExtension(extension, baseSchema)
const context = extension.extendContext(baseContext) const context = extension.extendContext(baseContext)
function testData (t, query, handler) { function testData(t, query, handler) {
return graphql(schema, query, null, context).then(result => { return graphql(schema, query, null, context).then(result => {
if (result.errors !== undefined) { if (result.errors !== undefined) {
console.log(result.errors) console.log(result.errors)
@ -17,7 +17,10 @@ function testData (t, query, handler) {
}) })
} }
test('artists have a fanArt field and preview images', testData, ` test(
'artists have a fanArt field and preview images',
testData,
`
{ {
lookup { lookup {
artist(mbid: "5b11f4ce-a62d-471e-81fc-a69a8278c7da") { artist(mbid: "5b11f4ce-a62d-471e-81fc-a69a8278c7da") {
@ -56,21 +59,26 @@ test('artists have a fanArt field and preview images', testData, `
} }
} }
} }
`, (t, data) => { `,
t.snapshot(data) (t, data) => {
const { fanArt } = data.lookup.artist t.snapshot(data)
const allImages = [] const { fanArt } = data.lookup.artist
.concat(fanArt.backgrounds) const allImages = []
.concat(fanArt.banners) .concat(fanArt.backgrounds)
.concat(fanArt.logos) .concat(fanArt.banners)
.concat(fanArt.logosHD) .concat(fanArt.logos)
.concat(fanArt.thumbnails) .concat(fanArt.logosHD)
allImages.forEach(image => { .concat(fanArt.thumbnails)
t.not(image.url, image.fullSizeURL) allImages.forEach(image => {
}) t.not(image.url, image.fullSizeURL)
}) })
}
)
test('release groups have a fanArt field and preview images', testData, ` test(
'release groups have a fanArt field and preview images',
testData,
`
{ {
lookup { lookup {
releaseGroup(mbid: "f5093c06-23e3-404f-aeaa-40f72885ee3a") { releaseGroup(mbid: "f5093c06-23e3-404f-aeaa-40f72885ee3a") {
@ -92,18 +100,21 @@ test('release groups have a fanArt field and preview images', testData, `
} }
} }
} }
`, (t, data) => { `,
t.snapshot(data) (t, data) => {
const { fanArt } = data.lookup.releaseGroup t.snapshot(data)
const allImages = [] const { fanArt } = data.lookup.releaseGroup
.concat(fanArt.albumCovers) const allImages = [].concat(fanArt.albumCovers).concat(fanArt.discImages)
.concat(fanArt.discImages) allImages.forEach(image => {
allImages.forEach(image => { t.not(image.url, image.fullSizeURL)
t.not(image.url, image.fullSizeURL) })
}) }
}) )
test('labels have a fanArt field and preview images', testData, ` test(
'labels have a fanArt field and preview images',
testData,
`
{ {
lookup { lookup {
label(mbid: "0cf56645-50ec-4411-aeb6-c9f4ce0f8edb") { label(mbid: "0cf56645-50ec-4411-aeb6-c9f4ce0f8edb") {
@ -119,10 +130,12 @@ test('labels have a fanArt field and preview images', testData, `
} }
} }
} }
`, (t, data) => { `,
t.snapshot(data) (t, data) => {
const { fanArt } = data.lookup.label t.snapshot(data)
fanArt.logos.forEach(image => { const { fanArt } = data.lookup.label
t.not(image.url, image.fullSizeURL) fanArt.logos.forEach(image => {
}) t.not(image.url, image.fullSizeURL)
}) })
}
)

View file

@ -7,7 +7,7 @@ import baseContext from '../../helpers/context'
const schema = applyExtension(extension, baseSchema) const schema = applyExtension(extension, baseSchema)
const context = extension.extendContext(baseContext) const context = extension.extendContext(baseContext)
function testData (t, query, handler) { function testData(t, query, handler) {
return graphql(schema, query, null, context).then(result => { return graphql(schema, query, null, context).then(result => {
if (result.errors !== undefined) { if (result.errors !== undefined) {
console.log(result.errors) console.log(result.errors)
@ -40,7 +40,10 @@ const fragment = `
} }
` `
test('artists have a mediaWikiImages field', testData, ` test(
'artists have a mediaWikiImages field',
testData,
`
{ {
lookup { lookup {
artist(mbid: "5b11f4ce-a62d-471e-81fc-a69a8278c7da") { artist(mbid: "5b11f4ce-a62d-471e-81fc-a69a8278c7da") {
@ -50,11 +53,16 @@ test('artists have a mediaWikiImages field', testData, `
} }
} }
} }
`, (t, data) => { `,
t.snapshot(data) (t, data) => {
}) t.snapshot(data)
}
)
test('instruments have a mediaWikiImages field', testData, ` test(
'instruments have a mediaWikiImages field',
testData,
`
{ {
search { search {
instruments(query: "guitar", first: 20) { instruments(query: "guitar", first: 20) {
@ -66,11 +74,16 @@ test('instruments have a mediaWikiImages field', testData, `
} }
} }
} }
`, (t, data) => { `,
t.snapshot(data) (t, data) => {
}) t.snapshot(data)
}
)
test('labels have a mediaWikiImages field', testData, ` test(
'labels have a mediaWikiImages field',
testData,
`
{ {
search { search {
labels(query: "Sony", first: 50) { labels(query: "Sony", first: 50) {
@ -82,11 +95,16 @@ test('labels have a mediaWikiImages field', testData, `
} }
} }
} }
`, (t, data) => { `,
t.snapshot(data) (t, data) => {
}) t.snapshot(data)
}
)
test('places have a mediaWikiImages field', testData, ` test(
'places have a mediaWikiImages field',
testData,
`
{ {
lookup { lookup {
place(mbid: "b5297256-8482-4cba-968a-25db61563faf") { place(mbid: "b5297256-8482-4cba-968a-25db61563faf") {
@ -96,6 +114,8 @@ test('places have a mediaWikiImages field', testData, `
} }
} }
} }
`, (t, data) => { `,
t.snapshot(data) (t, data) => {
}) t.snapshot(data)
}
)

View file

@ -7,7 +7,7 @@ import baseContext from '../../helpers/context'
const schema = applyExtension(extension, baseSchema) const schema = applyExtension(extension, baseSchema)
const context = extension.extendContext(baseContext) const context = extension.extendContext(baseContext)
function testData (t, query, handler) { function testData(t, query, handler) {
return graphql(schema, query, null, context).then(result => { return graphql(schema, query, null, context).then(result => {
if (result.errors !== undefined) { if (result.errors !== undefined) {
console.log(result.errors) console.log(result.errors)
@ -17,7 +17,10 @@ function testData (t, query, handler) {
}) })
} }
test('artists have a theAudioDB field', testData, ` test(
'artists have a theAudioDB field',
testData,
`
{ {
lookup { lookup {
artist(mbid: "5b11f4ce-a62d-471e-81fc-a69a8278c7da") { artist(mbid: "5b11f4ce-a62d-471e-81fc-a69a8278c7da") {
@ -41,11 +44,16 @@ test('artists have a theAudioDB field', testData, `
} }
} }
} }
`, (t, data) => { `,
t.snapshot(data) (t, data) => {
}) t.snapshot(data)
}
)
test('release groups have a theAudioDB field', testData, ` test(
'release groups have a theAudioDB field',
testData,
`
{ {
lookup { lookup {
releaseGroup(mbid: "aa997ea0-2936-40bd-884d-3af8a0e064dc") { releaseGroup(mbid: "aa997ea0-2936-40bd-884d-3af8a0e064dc") {
@ -75,11 +83,16 @@ test('release groups have a theAudioDB field', testData, `
} }
} }
} }
`, (t, data) => { `,
t.snapshot(data) (t, data) => {
}) t.snapshot(data)
}
)
test('recordings have a theAudioDB field', testData, ` test(
'recordings have a theAudioDB field',
testData,
`
{ {
lookup { lookup {
recording(mbid: "1109d8da-ce4a-4739-9414-242dc3e9b81c") { recording(mbid: "1109d8da-ce4a-4739-9414-242dc3e9b81c") {
@ -113,6 +126,8 @@ test('recordings have a theAudioDB field', testData, `
} }
} }
} }
`, (t, data) => { `,
t.snapshot(data) (t, data) => {
}) t.snapshot(data)
}
)

View file

@ -4,8 +4,7 @@ import CoverArtArchiveClient from '../../../src/extensions/cover-art-archive/cli
sepia.fixtureDir(path.join(__dirname, '..', '..', 'fixtures')) sepia.fixtureDir(path.join(__dirname, '..', '..', 'fixtures'))
const options = process.env.VCR_MODE === 'playback' const options =
? { limit: Infinity, period: 0 } process.env.VCR_MODE === 'playback' ? { limit: Infinity, period: 0 } : {}
: {}
export default new CoverArtArchiveClient(options) export default new CoverArtArchiveClient(options)

View file

@ -4,8 +4,7 @@ import MusicBrainz from '../../../src/api'
sepia.fixtureDir(path.join(__dirname, '..', '..', 'fixtures')) sepia.fixtureDir(path.join(__dirname, '..', '..', 'fixtures'))
const options = process.env.VCR_MODE === 'playback' const options =
? { limit: Infinity, period: 0 } process.env.VCR_MODE === 'playback' ? { limit: Infinity, period: 0 } : {}
: {}
export default new MusicBrainz(options) export default new MusicBrainz(options)

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,12 @@
import test from 'ava' import test from 'ava'
import { Kind } from 'graphql/language' import { Kind } from 'graphql/language'
import { Duration, Locale, MBID, ISWC, URLString } from '../../src/types/scalars' import {
Duration,
Locale,
MBID,
ISWC,
URLString
} from '../../src/types/scalars'
test('Locale scalar allows language code', t => { test('Locale scalar allows language code', t => {
t.is(Locale.parseLiteral({ kind: Kind.STRING, value: 'en' }), 'en') t.is(Locale.parseLiteral({ kind: Kind.STRING, value: 'en' }), 'en')
@ -15,9 +21,18 @@ test('Locale scalar allows language and country code', t => {
}) })
test('Locale scalar allows language, country, and encoding', t => { test('Locale scalar allows language, country, and encoding', t => {
t.is(Locale.parseLiteral({ kind: Kind.STRING, value: 'en_US.UTF-8' }), 'en_US.UTF-8') t.is(
t.is(Locale.parseLiteral({ kind: Kind.STRING, value: 'de_CH.utf8' }), 'de_CH.utf8') Locale.parseLiteral({ kind: Kind.STRING, value: 'en_US.UTF-8' }),
t.is(Locale.parseLiteral({ kind: Kind.STRING, value: 'zh_TW.Big5' }), 'zh_TW.Big5') 'en_US.UTF-8'
)
t.is(
Locale.parseLiteral({ kind: Kind.STRING, value: 'de_CH.utf8' }),
'de_CH.utf8'
)
t.is(
Locale.parseLiteral({ kind: Kind.STRING, value: 'zh_TW.Big5' }),
'zh_TW.Big5'
)
}) })
test('Locale scalar only accepts strings', t => { test('Locale scalar only accepts strings', t => {
@ -26,16 +41,46 @@ test('Locale scalar only accepts strings', t => {
}) })
test('Locale scalar rejects malformed locales', t => { test('Locale scalar rejects malformed locales', t => {
t.throws(() => Locale.parseLiteral({ kind: Kind.STRING, value: 'en_' }), TypeError) t.throws(
t.throws(() => Locale.parseLiteral({ kind: Kind.STRING, value: 'en_USA' }), TypeError) () => Locale.parseLiteral({ kind: Kind.STRING, value: 'en_' }),
t.throws(() => Locale.parseLiteral({ kind: Kind.STRING, value: 'EN' }), TypeError) TypeError
t.throws(() => Locale.parseLiteral({ kind: Kind.STRING, value: 'en_us' }), TypeError) )
t.throws(() => Locale.parseLiteral({ kind: Kind.STRING, value: 'en-US' }), TypeError) t.throws(
t.throws(() => Locale.parseLiteral({ kind: Kind.STRING, value: 'en_US_foo' }), TypeError) () => Locale.parseLiteral({ kind: Kind.STRING, value: 'en_USA' }),
t.throws(() => Locale.parseLiteral({ kind: Kind.STRING, value: 'en_US-utf8' }), TypeError) TypeError
t.throws(() => Locale.parseLiteral({ kind: Kind.STRING, value: '12_US' }), TypeError) )
t.throws(() => Locale.parseLiteral({ kind: Kind.STRING, value: 'en_US.' }), TypeError) t.throws(
t.throws(() => Locale.parseLiteral({ kind: Kind.STRING, value: 'en_US.utf!' }), TypeError) () => Locale.parseLiteral({ kind: Kind.STRING, value: 'EN' }),
TypeError
)
t.throws(
() => Locale.parseLiteral({ kind: Kind.STRING, value: 'en_us' }),
TypeError
)
t.throws(
() => Locale.parseLiteral({ kind: Kind.STRING, value: 'en-US' }),
TypeError
)
t.throws(
() => Locale.parseLiteral({ kind: Kind.STRING, value: 'en_US_foo' }),
TypeError
)
t.throws(
() => Locale.parseLiteral({ kind: Kind.STRING, value: 'en_US-utf8' }),
TypeError
)
t.throws(
() => Locale.parseLiteral({ kind: Kind.STRING, value: '12_US' }),
TypeError
)
t.throws(
() => Locale.parseLiteral({ kind: Kind.STRING, value: 'en_US.' }),
TypeError
)
t.throws(
() => Locale.parseLiteral({ kind: Kind.STRING, value: 'en_US.utf!' }),
TypeError
)
}) })
test('Duration scalar must be a positive integer', t => { test('Duration scalar must be a positive integer', t => {
@ -43,8 +88,14 @@ test('Duration scalar must be a positive integer', t => {
t.is(Duration.parseLiteral({ kind: Kind.INT, value: 1 }), 1) t.is(Duration.parseLiteral({ kind: Kind.INT, value: 1 }), 1)
t.is(Duration.parseLiteral({ kind: Kind.INT, value: 3000 }), 3000) t.is(Duration.parseLiteral({ kind: Kind.INT, value: 3000 }), 3000)
t.is(Duration.parseLiteral({ kind: Kind.STRING, value: '1000' }), null) t.is(Duration.parseLiteral({ kind: Kind.STRING, value: '1000' }), null)
t.throws(() => Duration.parseLiteral({ kind: Kind.INT, value: -1 }), TypeError) t.throws(
t.throws(() => Duration.parseLiteral({ kind: Kind.INT, value: -1000 }), TypeError) () => Duration.parseLiteral({ kind: Kind.INT, value: -1 }),
TypeError
)
t.throws(
() => Duration.parseLiteral({ kind: Kind.INT, value: -1000 }),
TypeError
)
t.is(Duration.parseValue(0), 0) t.is(Duration.parseValue(0), 0)
t.is(Duration.parseValue(1), 1) t.is(Duration.parseValue(1), 1)
t.is(Duration.parseValue(3000), 3000) t.is(Duration.parseValue(3000), 3000)
@ -54,11 +105,29 @@ test('Duration scalar must be a positive integer', t => {
test('URLString scalar must be a valid URL', t => { test('URLString scalar must be a valid URL', t => {
t.is(URLString.parseLiteral({ kind: Kind.INT, value: 1000 }), null) t.is(URLString.parseLiteral({ kind: Kind.INT, value: 1000 }), null)
t.is(URLString.parseLiteral({ kind: Kind.STRING, value: 'http://www.google.com' }), 'http://www.google.com') t.is(
t.throws(() => URLString.parseLiteral({ kind: Kind.STRING, value: 'foo:bar' }), TypeError) URLString.parseLiteral({
t.throws(() => URLString.parseLiteral({ kind: Kind.STRING, value: 'foo:/bar' }), TypeError) kind: Kind.STRING,
t.throws(() => URLString.parseLiteral({ kind: Kind.STRING, value: 'foo://bar' }), TypeError) value: 'http://www.google.com'
t.throws(() => URLString.parseLiteral({ kind: Kind.STRING, value: 'foo://bar.' }), TypeError) }),
'http://www.google.com'
)
t.throws(
() => URLString.parseLiteral({ kind: Kind.STRING, value: 'foo:bar' }),
TypeError
)
t.throws(
() => URLString.parseLiteral({ kind: Kind.STRING, value: 'foo:/bar' }),
TypeError
)
t.throws(
() => URLString.parseLiteral({ kind: Kind.STRING, value: 'foo://bar' }),
TypeError
)
t.throws(
() => URLString.parseLiteral({ kind: Kind.STRING, value: 'foo://bar.' }),
TypeError
)
}) })
test('ISWC scalar only accepts strings', t => { test('ISWC scalar only accepts strings', t => {
@ -79,6 +148,19 @@ test('MBID scalar only accepts strings', t => {
}) })
test('MBID scalar must be a valid UUID', t => { test('MBID scalar must be a valid UUID', t => {
t.is(MBID.parseLiteral({ kind: Kind.STRING, value: 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8' }), 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8') t.is(
t.throws(() => MBID.parseLiteral({ kind: Kind.STRING, value: 'c8da2e40-bd28-4d4e-813a-bd2f51958bag' }), TypeError) MBID.parseLiteral({
kind: Kind.STRING,
value: 'c8da2e40-bd28-4d4e-813a-bd2f51958ba8'
}),
'c8da2e40-bd28-4d4e-813a-bd2f51958ba8'
)
t.throws(
() =>
MBID.parseLiteral({
kind: Kind.STRING,
value: 'c8da2e40-bd28-4d4e-813a-bd2f51958bag'
}),
TypeError
)
}) })

View file

@ -2,8 +2,12 @@ import test from 'ava'
import sinon from 'sinon' import sinon from 'sinon'
import { prettyPrint } from '../src/util' import { prettyPrint } from '../src/util'
test.beforeEach(t => { sinon.stub(console, 'log') }) test.beforeEach(t => {
test.afterEach(t => { console.log.restore() }) sinon.stub(console, 'log')
})
test.afterEach(t => {
console.log.restore()
})
test('prettyPrint writes to stdout', t => { test('prettyPrint writes to stdout', t => {
prettyPrint('foo') prettyPrint('foo')

745
yarn.lock

File diff suppressed because it is too large Load diff