mirror of
https://github.com/BradNut/graphbrainz
synced 2025-09-08 17:40:32 +00:00
Relay-compliant API
This commit is contained in:
parent
116775eaca
commit
1eeaa83eef
38 changed files with 11329 additions and 5527 deletions
47
package.json
47
package.json
|
|
@ -3,6 +3,12 @@
|
|||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "lib/index.js",
|
||||
"files": [
|
||||
"lib",
|
||||
"scripts",
|
||||
"Procfile",
|
||||
"schema.json"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^4.3.0",
|
||||
"npm": "^3.10.5"
|
||||
|
|
@ -17,11 +23,14 @@
|
|||
"lint": "standard --verbose | snazzy",
|
||||
"prepublish": "npm run clean && npm run check && npm run build",
|
||||
"print-schema": "babel-node scripts/print-schema.js",
|
||||
"print-schema:json": "babel-node scripts/print-schema.js --json",
|
||||
"print-schema:json": "npm run print-schema -- --json",
|
||||
"print-schema:md": "printf '```graphql\\n%s\\n```' \"$(npm run -s print-schema)\"",
|
||||
"start": "node lib/index.js",
|
||||
"start:dev": "nodemon --exec babel-node src/index.js",
|
||||
"test": "mocha --compilers js:babel-register",
|
||||
"update-schema": "npm run -s print-schema:json > schema.json"
|
||||
"update-schema": "npm run update-schema:json && npm run update-schema:md",
|
||||
"update-schema:json": "npm run -s print-schema:json > schema.json",
|
||||
"update-schema:md": "printf '# GraphQL Schema\\n\\n%s' \"$(npm run -s print-schema:md)\" > schema.md"
|
||||
},
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/exogen/graphbrainz",
|
||||
|
|
@ -37,29 +46,33 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^1.1.3",
|
||||
"compression": "^1.6.2",
|
||||
"dashify": "^0.2.2",
|
||||
"dataloader": "^1.2.0",
|
||||
"es6-error": "^3.0.1",
|
||||
"debug": "^2.3.3",
|
||||
"dotenv": "^2.0.0",
|
||||
"es6-error": "^4.0.0",
|
||||
"express": "^4.14.0",
|
||||
"express-graphql": "^0.5.3",
|
||||
"graphql": "^0.6.2",
|
||||
"graphql-relay": "^0.4.2",
|
||||
"express-graphql": "^0.6.1",
|
||||
"graphql": "^0.8.2",
|
||||
"graphql-relay": "^0.4.4",
|
||||
"lru-cache": "^4.0.1",
|
||||
"pascalcase": "^0.1.1",
|
||||
"qs": "^6.2.1",
|
||||
"request": "^2.74.0",
|
||||
"qs": "^6.3.0",
|
||||
"request": "^2.79.0",
|
||||
"retry": "^0.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.11.4",
|
||||
"babel-eslint": "^6.1.2",
|
||||
"babel-preset-es2015": "^6.13.2",
|
||||
"babel-preset-stage-2": "^6.13.0",
|
||||
"babel-register": "^6.11.6",
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-preset-es2015": "^6.18.0",
|
||||
"babel-preset-stage-2": "^6.18.0",
|
||||
"babel-register": "^6.18.0",
|
||||
"chai": "^3.5.0",
|
||||
"mocha": "^3.0.1",
|
||||
"nodemon": "^1.10.2",
|
||||
"snazzy": "^4.0.1",
|
||||
"standard": "^8.0.0"
|
||||
"mocha": "^3.2.0",
|
||||
"nodemon": "^1.11.0",
|
||||
"snazzy": "^5.0.0",
|
||||
"standard": "^8.6.0"
|
||||
},
|
||||
"standard": {
|
||||
"parser": "babel-eslint"
|
||||
|
|
|
|||
9571
schema.json
9571
schema.json
File diff suppressed because it is too large
Load diff
75
src/api.js
75
src/api.js
|
|
@ -6,6 +6,8 @@ import ExtendableError from 'es6-error'
|
|||
import RateLimit from './rate-limit'
|
||||
import pkg from '../package.json'
|
||||
|
||||
const debug = require('debug')('graphbrainz:api')
|
||||
|
||||
// If the `request` callback returns an error, it indicates a failure at a lower
|
||||
// level than the HTTP response itself. If it's any of the following error
|
||||
// codes, we should retry.
|
||||
|
|
@ -28,45 +30,39 @@ export class MusicBrainzError extends ExtendableError {
|
|||
}
|
||||
|
||||
export default class MusicBrainz {
|
||||
constructor (options = {}) {
|
||||
options = {
|
||||
baseURL: 'http://musicbrainz.org/ws/2/',
|
||||
userAgent: `${pkg.name}/${pkg.version} ` +
|
||||
`( ${pkg.homepage || pkg.author.url || pkg.author.email} )`,
|
||||
timeout: 60000,
|
||||
// MusicBrainz API requests are limited to an *average* of 1 req/sec.
|
||||
// That means if, for example, we only need to make a few API requests to
|
||||
// fulfill a query, we might as well make them all at once - as long as
|
||||
// we then wait a few seconds before making more. In practice this can
|
||||
// seemingly be set to about 5 requests every 5 seconds before we're
|
||||
// considered to exceed the rate limit.
|
||||
limit: 5,
|
||||
limitPeriod: 5000,
|
||||
concurrency: 10,
|
||||
retries: 10,
|
||||
// It's OK for `retryDelayMin` to be less than one second, even 0, because
|
||||
// `RateLimit` will already make sure we don't exceed the API rate limit.
|
||||
// We're not doing exponential backoff because it will help with being
|
||||
// rate limited, but rather to be chill in case MusicBrainz is returning
|
||||
// some other error or our network is failing.
|
||||
retryDelayMin: 100,
|
||||
retryDelayMax: 60000,
|
||||
randomizeRetry: true,
|
||||
...options
|
||||
}
|
||||
this.baseURL = options.baseURL
|
||||
this.userAgent = options.userAgent
|
||||
this.timeout = options.timeout
|
||||
this.limiter = new RateLimit({
|
||||
limit: options.limit,
|
||||
period: options.limitPeriod,
|
||||
concurrency: options.concurrency
|
||||
})
|
||||
constructor ({
|
||||
baseURL = process.env.MUSICBRAINZ_BASE_URL || 'http://musicbrainz.org/ws/2/',
|
||||
userAgent = `${pkg.name}/${pkg.version} ` +
|
||||
`( ${pkg.homepage || pkg.author.url || pkg.author.email} )`,
|
||||
timeout = 60000,
|
||||
// MusicBrainz API requests are limited to an *average* of 1 req/sec.
|
||||
// That means if, for example, we only need to make a few API requests to
|
||||
// fulfill a query, we might as well make them all at once - as long as
|
||||
// we then wait a few seconds before making more. In practice this can
|
||||
// seemingly be set to about 5 requests every 5 seconds before we're
|
||||
// considered to exceed the rate limit.
|
||||
limit = 5,
|
||||
period = 5000,
|
||||
concurrency = 10,
|
||||
retries = 10,
|
||||
// It's OK for `retryDelayMin` to be less than one second, even 0, because
|
||||
// `RateLimit` will already make sure we don't exceed the API rate limit.
|
||||
// We're not doing exponential backoff because it will help with being
|
||||
// rate limited, but rather to be chill in case MusicBrainz is returning
|
||||
// some other error or our network is failing.
|
||||
retryDelayMin = 100,
|
||||
retryDelayMax = 60000,
|
||||
randomizeRetry = true
|
||||
} = {}) {
|
||||
this.baseURL = baseURL
|
||||
this.userAgent = userAgent
|
||||
this.timeout = timeout
|
||||
this.limiter = new RateLimit({ limit, period, concurrency })
|
||||
this.retryOptions = {
|
||||
retries: options.retries,
|
||||
minTimeout: options.retryDelayMin,
|
||||
maxTimeout: options.retryDelayMax,
|
||||
randomize: options.randomizeRetry
|
||||
retries,
|
||||
minTimeout: retryDelayMin,
|
||||
maxTimeout: retryDelayMax,
|
||||
randomize: randomizeRetry
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,8 +94,7 @@ export default class MusicBrainz {
|
|||
timeout: this.timeout
|
||||
}
|
||||
|
||||
const attempt = `(attempt #${info.currentAttempt})`
|
||||
console.log('GET:', path, info.currentAttempt > 1 ? attempt : '')
|
||||
debug(path, info.currentAttempt > 1 ? `(attempt #${info.currentAttempt})` : '')
|
||||
|
||||
request(options, (err, response, body) => {
|
||||
if (err) {
|
||||
|
|
|
|||
49
src/index.js
49
src/index.js
|
|
@ -1,24 +1,37 @@
|
|||
import express from 'express'
|
||||
import graphqlHTTP from 'express-graphql'
|
||||
import compression from 'compression'
|
||||
import MusicBrainz from './api'
|
||||
import schema from './schema'
|
||||
import { lookupLoader, browseLoader, searchLoader } from './loaders'
|
||||
import createLoaders from './loaders'
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use('/graphql', graphqlHTTP({
|
||||
schema,
|
||||
context: { lookupLoader, browseLoader, searchLoader },
|
||||
pretty: true,
|
||||
graphiql: true,
|
||||
formatError: error => ({
|
||||
message: error.message,
|
||||
locations: error.locations,
|
||||
stack: error.stack
|
||||
})
|
||||
}))
|
||||
|
||||
app.get('/graphiql', (req, res) => {
|
||||
res.redirect(`/graph${req.url.slice(7)}`)
|
||||
const formatError = (err) => ({
|
||||
message: err.message,
|
||||
locations: err.locations,
|
||||
stack: err.stack
|
||||
})
|
||||
|
||||
app.listen(process.env.PORT || 3001)
|
||||
const middleware = ({ client = new MusicBrainz(), ...options } = {}) => {
|
||||
const DEV = process.env.NODE_ENV !== 'production'
|
||||
const loaders = createLoaders(client)
|
||||
return graphqlHTTP({
|
||||
schema,
|
||||
context: { client, loaders },
|
||||
pretty: DEV,
|
||||
graphiql: DEV,
|
||||
formatError: DEV ? formatError : undefined,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
export default middleware
|
||||
|
||||
if (require.main === module) {
|
||||
require('dotenv').config({ silent: true })
|
||||
const app = express()
|
||||
const port = process.env.PORT || 3000
|
||||
const route = process.env.GRAPHBRAINZ_PATH || '/'
|
||||
app.use(compression())
|
||||
app.use(route, middleware())
|
||||
app.listen(port)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,64 @@
|
|||
import DataLoader from 'dataloader'
|
||||
import MusicBrainz from './api'
|
||||
import LRUCache from 'lru-cache'
|
||||
import { toPlural } from './types/helpers'
|
||||
|
||||
const client = new MusicBrainz()
|
||||
const debug = require('debug')('graphbrainz:loaders')
|
||||
|
||||
export const lookupLoader = new DataLoader(keys => {
|
||||
return Promise.all(keys.map(key => {
|
||||
const [ entityType, id, params ] = key
|
||||
return client.lookup(entityType, id, params).then(entity => {
|
||||
if (entity) {
|
||||
entity.entityType = entityType
|
||||
}
|
||||
return entity
|
||||
})
|
||||
}))
|
||||
}, {
|
||||
cacheKeyFn: (key) => client.getLookupURL(...key)
|
||||
})
|
||||
export default function createLoaders (client) {
|
||||
const cache = LRUCache({
|
||||
max: 8192,
|
||||
maxAge: 24 * 60 * 60 * 1000, // 1 day.
|
||||
dispose (key) {
|
||||
debug(`Removed '${key}' from cache.`)
|
||||
}
|
||||
})
|
||||
cache.delete = cache.del
|
||||
cache.clear = cache.reset
|
||||
|
||||
export const browseLoader = new DataLoader(keys => {
|
||||
return Promise.all(keys.map(key => {
|
||||
const [ entityType, params ] = key
|
||||
const pluralName = entityType.endsWith('s') ? entityType : `${entityType}s`
|
||||
return client.browse(entityType, params).then(list => {
|
||||
list[pluralName].forEach(entity => {
|
||||
entity.entityType = entityType
|
||||
const lookup = new DataLoader(keys => {
|
||||
return Promise.all(keys.map(key => {
|
||||
const [ entityType, id, params ] = key
|
||||
return client.lookup(entityType, id, params).then(entity => {
|
||||
if (entity) {
|
||||
entity.entityType = entityType
|
||||
}
|
||||
return entity
|
||||
})
|
||||
return list
|
||||
})
|
||||
}))
|
||||
}, {
|
||||
cacheKeyFn: (key) => client.getBrowseURL(...key)
|
||||
})
|
||||
}))
|
||||
}, {
|
||||
cacheKeyFn: (key) => client.getLookupURL(...key),
|
||||
cacheMap: cache
|
||||
})
|
||||
|
||||
export const searchLoader = new DataLoader(keys => {
|
||||
return Promise.all(keys.map(key => {
|
||||
const [ entityType, query, params ] = key
|
||||
const pluralName = entityType.endsWith('s') ? entityType : `${entityType}s`
|
||||
return client.search(entityType, query, params).then(list => {
|
||||
list[pluralName].forEach(entity => {
|
||||
entity.entityType = entityType
|
||||
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 => {
|
||||
entity.entityType = entityType
|
||||
})
|
||||
return list
|
||||
})
|
||||
return list
|
||||
})
|
||||
}))
|
||||
}, {
|
||||
cacheKeyFn: (key) => client.getSearchURL(...key)
|
||||
})
|
||||
}))
|
||||
}, {
|
||||
cacheKeyFn: (key) => client.getBrowseURL(...key),
|
||||
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 => {
|
||||
entity.entityType = entityType
|
||||
})
|
||||
return list
|
||||
})
|
||||
}))
|
||||
}, {
|
||||
cacheKeyFn: (key) => client.getSearchURL(...key),
|
||||
cacheMap: cache
|
||||
})
|
||||
|
||||
return { lookup, browse, search }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,34 @@ import {
|
|||
URLConnection,
|
||||
WorkConnection
|
||||
} from '../types'
|
||||
import { toWords } from '../types/helpers'
|
||||
|
||||
const area = {
|
||||
type: MBID,
|
||||
description: 'The MBID of an area to which the entity is linked.'
|
||||
}
|
||||
const artist = {
|
||||
type: MBID,
|
||||
description: 'The MBID of an artist to which the entity is linked.'
|
||||
}
|
||||
const recording = {
|
||||
type: MBID,
|
||||
description: 'The MBID of a recording to which the entity is linked.'
|
||||
}
|
||||
const release = {
|
||||
type: MBID,
|
||||
description: 'The MBID of a release to which the entity is linked.'
|
||||
}
|
||||
const releaseGroup = {
|
||||
type: MBID,
|
||||
description: 'The MBID of a release group to which the entity is linked.'
|
||||
}
|
||||
|
||||
function browseQuery (connectionType, args) {
|
||||
const typeName = toWords(connectionType.name.slice(0, -10))
|
||||
return {
|
||||
type: connectionType,
|
||||
description: `Browse ${typeName} entities linked to the given arguments.`,
|
||||
args: {
|
||||
...forwardConnectionArgs,
|
||||
...args
|
||||
|
|
@ -28,51 +52,69 @@ function browseQuery (connectionType, args) {
|
|||
|
||||
export default new GraphQLObjectType({
|
||||
name: 'BrowseQuery',
|
||||
description:
|
||||
'Browse requests are a direct lookup of all the entities directly linked ' +
|
||||
'to another entity.',
|
||||
description: `A query for all MusicBrainz entities directly linked to another
|
||||
entity.`,
|
||||
fields: {
|
||||
artists: browseQuery(ArtistConnection, {
|
||||
area: { type: MBID },
|
||||
recording: { type: MBID },
|
||||
release: { type: MBID },
|
||||
releaseGroup: { type: MBID },
|
||||
work: { type: MBID }
|
||||
area,
|
||||
recording,
|
||||
release,
|
||||
releaseGroup,
|
||||
work: {
|
||||
type: MBID,
|
||||
description: 'The MBID of a work to which the artist is linked.'
|
||||
}
|
||||
}),
|
||||
events: browseQuery(EventConnection, {
|
||||
area: { type: MBID },
|
||||
artist: { type: MBID },
|
||||
place: { type: MBID }
|
||||
area,
|
||||
artist,
|
||||
place: {
|
||||
type: MBID,
|
||||
description: 'The MBID of a place to which the event is linked.'
|
||||
}
|
||||
}),
|
||||
labels: browseQuery(LabelConnection, {
|
||||
area: { type: MBID },
|
||||
release: { type: MBID }
|
||||
area,
|
||||
release
|
||||
}),
|
||||
places: browseQuery(PlaceConnection, {
|
||||
area: { type: MBID }
|
||||
area
|
||||
}),
|
||||
recordings: browseQuery(RecordingConnection, {
|
||||
artist: { type: MBID },
|
||||
release: { type: MBID }
|
||||
artist,
|
||||
release
|
||||
}),
|
||||
releases: browseQuery(ReleaseConnection, {
|
||||
area: { type: MBID },
|
||||
artist: { type: MBID },
|
||||
label: { type: MBID },
|
||||
track: { type: MBID },
|
||||
trackArtist: { type: MBID },
|
||||
recording: { type: MBID },
|
||||
releaseGroup: { type: MBID }
|
||||
area,
|
||||
artist,
|
||||
label: {
|
||||
type: MBID,
|
||||
description: 'The MBID of a label to which the release is linked.'
|
||||
},
|
||||
track: {
|
||||
type: MBID,
|
||||
description: 'The MBID of a track that is included in the release.'
|
||||
},
|
||||
trackArtist: {
|
||||
type: MBID,
|
||||
description: `The MBID of an artist that appears on a track in the
|
||||
release, but is not included in the credits for the release itself.`
|
||||
},
|
||||
recording,
|
||||
releaseGroup
|
||||
}),
|
||||
releaseGroups: browseQuery(ReleaseGroupConnection, {
|
||||
artist: { type: MBID },
|
||||
release: { type: MBID }
|
||||
artist,
|
||||
release
|
||||
}),
|
||||
works: browseQuery(WorkConnection, {
|
||||
artist: { type: MBID }
|
||||
artist
|
||||
}),
|
||||
urls: browseQuery(URLConnection, {
|
||||
resource: { type: URLString }
|
||||
resource: {
|
||||
type: URLString,
|
||||
description: 'The web address for which to browse URL entities.'
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { GraphQLObjectType } from 'graphql'
|
||||
import { lookupResolver } from '../resolvers'
|
||||
import { mbid } from '../types/helpers'
|
||||
import { mbid, toWords } from '../types/helpers'
|
||||
import {
|
||||
Area,
|
||||
Artist,
|
||||
|
|
@ -17,9 +17,10 @@ import {
|
|||
} from '../types'
|
||||
|
||||
function lookupQuery (entity) {
|
||||
const typeName = toWords(entity.name)
|
||||
return {
|
||||
type: entity,
|
||||
description: `Look up a specific ${entity.name} by its MBID.`,
|
||||
description: `Look up a specific ${typeName} by its MBID.`,
|
||||
args: { mbid },
|
||||
resolve: lookupResolver()
|
||||
}
|
||||
|
|
@ -27,9 +28,7 @@ function lookupQuery (entity) {
|
|||
|
||||
export default new GraphQLObjectType({
|
||||
name: 'LookupQuery',
|
||||
description:
|
||||
'You can perform a lookup of an entity when you have the MBID for that ' +
|
||||
'entity.',
|
||||
description: 'A lookup of an individual MusicBrainz entity by its MBID.',
|
||||
fields: {
|
||||
area: lookupQuery(Area),
|
||||
artist: lookupQuery(Artist),
|
||||
|
|
|
|||
|
|
@ -11,10 +11,13 @@ import {
|
|||
ReleaseGroupConnection,
|
||||
WorkConnection
|
||||
} from '../types'
|
||||
import { toWords } from '../types/helpers'
|
||||
|
||||
function searchQuery (connectionType) {
|
||||
const typeName = toWords(connectionType.name.slice(0, -10))
|
||||
return {
|
||||
type: connectionType,
|
||||
description: `Search for ${typeName} entities matching the given query.`,
|
||||
args: {
|
||||
query: { type: new GraphQLNonNull(GraphQLString) },
|
||||
...forwardConnectionArgs
|
||||
|
|
@ -25,9 +28,7 @@ function searchQuery (connectionType) {
|
|||
|
||||
export default new GraphQLObjectType({
|
||||
name: 'SearchQuery',
|
||||
description:
|
||||
'Search queries provide a way to search for MusicBrainz entities using ' +
|
||||
'Lucene query syntax.',
|
||||
description: 'A search for MusicBrainz entities using Lucene query syntax.',
|
||||
fields: {
|
||||
areas: searchQuery(AreaConnection),
|
||||
artists: searchQuery(ArtistConnection),
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
export default class RateLimit {
|
||||
constructor (options = {}) {
|
||||
options = {
|
||||
limit: 1,
|
||||
period: 1000,
|
||||
concurrency: options.limit || 1,
|
||||
defaultPriority: 1,
|
||||
...options
|
||||
}
|
||||
this.limit = options.limit
|
||||
this.period = options.period
|
||||
this.defaultPriority = options.defaultPriority
|
||||
this.concurrency = options.concurrency
|
||||
constructor ({
|
||||
limit = 1,
|
||||
period = 1000,
|
||||
concurrency = limit || 1,
|
||||
defaultPriority = 1
|
||||
} = {}) {
|
||||
this.limit = limit
|
||||
this.period = period
|
||||
this.defaultPriority = defaultPriority
|
||||
this.concurrency = concurrency
|
||||
this.queues = []
|
||||
this.numPending = 0
|
||||
this.periodStart = null
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { toEntityType } from './types/helpers'
|
||||
import { toDashed, toSingular } from './types/helpers'
|
||||
import {
|
||||
getOffsetWithDefault,
|
||||
connectionFromArray,
|
||||
|
|
@ -6,18 +6,20 @@ import {
|
|||
} from 'graphql-relay'
|
||||
import { getFields, extendIncludes } from './util'
|
||||
|
||||
export function includeRelations (params, info) {
|
||||
export function includeRelationships (params, info) {
|
||||
let fields = getFields(info)
|
||||
if (info.fieldName !== 'relations') {
|
||||
if (fields.relations) {
|
||||
fields = getFields(fields.relations)
|
||||
if (info.fieldName !== 'relationships') {
|
||||
if (fields.relationships) {
|
||||
fields = getFields(fields.relationships)
|
||||
} else {
|
||||
return params
|
||||
}
|
||||
}
|
||||
if (fields) {
|
||||
const relations = Object.keys(fields)
|
||||
const includeRels = relations.map(key => `${toEntityType(key)}-rels`)
|
||||
const relationships = Object.keys(fields)
|
||||
const includeRels = relationships.map(field => {
|
||||
return `${toDashed(toSingular(field))}-rels`
|
||||
})
|
||||
if (includeRels.length) {
|
||||
params = {
|
||||
...params,
|
||||
|
|
@ -40,43 +42,30 @@ export function includeSubqueries (params, info) {
|
|||
}
|
||||
|
||||
export function lookupResolver () {
|
||||
return (root, { mbid }, { lookupLoader }, info) => {
|
||||
const entityType = toEntityType(info.fieldName)
|
||||
const params = includeRelations({}, info)
|
||||
return lookupLoader.load([entityType, mbid, params])
|
||||
return (root, { mbid }, { loaders }, info) => {
|
||||
const entityType = toDashed(info.fieldName)
|
||||
const params = includeRelationships({}, info)
|
||||
return loaders.lookup.load([entityType, mbid, params])
|
||||
}
|
||||
}
|
||||
|
||||
export function browseResolver () {
|
||||
return (source, { first = 25, after, ...args }, { browseLoader }, info) => {
|
||||
const pluralName = toEntityType(info.fieldName)
|
||||
let singularName = pluralName
|
||||
if (pluralName.endsWith('s')) {
|
||||
singularName = pluralName.slice(0, -1)
|
||||
}
|
||||
const { type, types, status, statuses, ...moreParams } = args
|
||||
return (source, { first = 25, after, type = [], status = [], ...args }, { loaders }, info) => {
|
||||
const pluralName = toDashed(info.fieldName)
|
||||
const singularName = toSingular(pluralName)
|
||||
let params = {
|
||||
...moreParams,
|
||||
type: [],
|
||||
status: [],
|
||||
...args,
|
||||
type,
|
||||
status,
|
||||
limit: first,
|
||||
offset: getOffsetWithDefault(after, 0)
|
||||
}
|
||||
params = includeSubqueries(params, info)
|
||||
params = includeRelations(params, info)
|
||||
if (type) {
|
||||
params.type.push(type)
|
||||
}
|
||||
if (types) {
|
||||
params.type.push(...types)
|
||||
}
|
||||
if (status) {
|
||||
params.status.push(status)
|
||||
}
|
||||
if (statuses) {
|
||||
params.status.push(...statuses)
|
||||
}
|
||||
return browseLoader.load([singularName, params]).then(list => {
|
||||
params = includeRelationships(params, info)
|
||||
const formatValue = value => value.toLowerCase().replace(/ /g, '')
|
||||
params.type = params.type.map(formatValue)
|
||||
params.status = params.status.map(formatValue)
|
||||
return loaders.browse.load([singularName, params]).then(list => {
|
||||
const {
|
||||
[pluralName]: arraySlice,
|
||||
[`${singularName}-offset`]: sliceStart,
|
||||
|
|
@ -89,16 +78,13 @@ export function browseResolver () {
|
|||
}
|
||||
|
||||
export function searchResolver () {
|
||||
return (source, { first = 25, after, ...args }, { searchLoader }, info) => {
|
||||
const pluralName = toEntityType(info.fieldName)
|
||||
let singularName = pluralName
|
||||
if (pluralName.endsWith('s')) {
|
||||
singularName = pluralName.slice(0, -1)
|
||||
}
|
||||
return (source, { first = 25, after, ...args }, { loaders }, info) => {
|
||||
const pluralName = toDashed(info.fieldName)
|
||||
const singularName = toSingular(pluralName)
|
||||
const { query, ...params } = args
|
||||
params.limit = first
|
||||
params.offset = getOffsetWithDefault(after, 0)
|
||||
return searchLoader.load([singularName, query, params]).then(list => {
|
||||
return loaders.search.load([singularName, query, params]).then(list => {
|
||||
const {
|
||||
[pluralName]: arraySlice,
|
||||
offset: sliceStart,
|
||||
|
|
@ -110,31 +96,31 @@ export function searchResolver () {
|
|||
}
|
||||
}
|
||||
|
||||
export function relationResolver () {
|
||||
export function relationshipResolver () {
|
||||
return (source, args, context, info) => {
|
||||
const targetType = toEntityType(info.fieldName).replace('-', '_')
|
||||
const relations = source.filter(relation => {
|
||||
if (relation['target-type'] !== targetType) {
|
||||
const targetType = toDashed(toSingular(info.fieldName)).replace('-', '_')
|
||||
const relationships = source.filter(rel => {
|
||||
if (rel['target-type'] !== targetType) {
|
||||
return false
|
||||
}
|
||||
if (args.direction != null && relation.direction !== args.direction) {
|
||||
if (args.direction != null && rel.direction !== args.direction) {
|
||||
return false
|
||||
}
|
||||
if (args.type != null && relation.type !== args.type) {
|
||||
if (args.type != null && rel.type !== args.type) {
|
||||
return false
|
||||
}
|
||||
if (args.typeID != null && relation['type-id'] !== args.typeID) {
|
||||
if (args.typeID != null && rel['type-id'] !== args.typeID) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return connectionFromArray(relations, args)
|
||||
return connectionFromArray(relationships, args)
|
||||
}
|
||||
}
|
||||
|
||||
export function linkedResolver () {
|
||||
return (source, args, context, info) => {
|
||||
const parentEntity = toEntityType(info.parentType.name)
|
||||
const parentEntity = toDashed(info.parentType.name)
|
||||
args = { ...args, [parentEntity]: source.id }
|
||||
return browseResolver()(source, args, context, info)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,28 @@ import { nodeField } from './types/node'
|
|||
|
||||
export default new GraphQLSchema({
|
||||
query: new GraphQLObjectType({
|
||||
name: 'RootQuery',
|
||||
name: 'Query',
|
||||
description: `The query root, from which multiple types of MusicBrainz
|
||||
requests can be made.`,
|
||||
fields: () => ({
|
||||
node: nodeField,
|
||||
lookup: { type: LookupQuery, resolve: () => ({}) },
|
||||
browse: { type: BrowseQuery, resolve: () => ({}) },
|
||||
search: { type: SearchQuery, resolve: () => ({}) }
|
||||
lookup: {
|
||||
type: LookupQuery,
|
||||
description: 'Perform a lookup of a MusicBrainz entity by its MBID.',
|
||||
resolve: () => ({})
|
||||
},
|
||||
browse: {
|
||||
type: BrowseQuery,
|
||||
description: `Browse all MusicBrainz entities directly linked to another
|
||||
entity.`,
|
||||
resolve: () => ({})
|
||||
},
|
||||
search: {
|
||||
type: SearchQuery,
|
||||
description: `Search for MusicBrainz entities using Lucene query
|
||||
syntax.`,
|
||||
resolve: () => ({})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,17 +3,33 @@ import {
|
|||
GraphQLString,
|
||||
GraphQLBoolean
|
||||
} from 'graphql/type'
|
||||
import { MBID } from './scalars'
|
||||
import { getHyphenated } from './helpers'
|
||||
import { name, sortName, fieldWithID } from './helpers'
|
||||
|
||||
export default new GraphQLObjectType({
|
||||
name: 'Alias',
|
||||
description: `[Aliases](https://musicbrainz.org/doc/Aliases) are variant names
|
||||
that are mostly used as search help: if a search matches an entity’s alias, the
|
||||
entity will be given as a result – even if the actual name wouldn’t be. They are
|
||||
available for artists, labels, and works.`,
|
||||
fields: () => ({
|
||||
name: { type: GraphQLString },
|
||||
sortName: { type: GraphQLString, resolve: getHyphenated },
|
||||
locale: { type: GraphQLString },
|
||||
primary: { type: GraphQLBoolean },
|
||||
type: { type: GraphQLString },
|
||||
typeID: { type: MBID, resolve: getHyphenated }
|
||||
name: {
|
||||
...name,
|
||||
description: 'The aliased name of the entity.'
|
||||
},
|
||||
sortName,
|
||||
locale: {
|
||||
type: GraphQLString,
|
||||
description: `The locale (language and/or country) in which the alias is
|
||||
used.`
|
||||
},
|
||||
primary: {
|
||||
type: GraphQLBoolean,
|
||||
description: `Whether this is the main alias for the entity in the
|
||||
specified locale (this could mean the most recent or the most common).`
|
||||
},
|
||||
...fieldWithID('type', {
|
||||
description: `The type or purpose of the alias – whether it is a variant,
|
||||
search hint, etc.`
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ import {
|
|||
|
||||
const Area = new GraphQLObjectType({
|
||||
name: 'Area',
|
||||
description: 'A country, region, city or the like.',
|
||||
description: `[Areas](https://musicbrainz.org/doc/Area) are geographic regions
|
||||
or settlements (countries, cities, or the like).`,
|
||||
interfaces: () => [Node, Entity],
|
||||
fields: () => ({
|
||||
id,
|
||||
|
|
@ -27,6 +28,8 @@ const Area = new GraphQLObjectType({
|
|||
disambiguation,
|
||||
isoCodes: {
|
||||
type: new GraphQLList(GraphQLString),
|
||||
description: `[ISO 3166 codes](https://en.wikipedia.org/wiki/ISO_3166) are
|
||||
the codes assigned by ISO to countries and subdivisions.`,
|
||||
resolve: data => data['iso-3166-1-codes']
|
||||
},
|
||||
artists,
|
||||
|
|
|
|||
|
|
@ -1,22 +1,36 @@
|
|||
import { GraphQLObjectType, GraphQLString } from 'graphql/type'
|
||||
import Artist from './artist'
|
||||
import { name } from './helpers'
|
||||
|
||||
export default new GraphQLObjectType({
|
||||
name: 'ArtistCredit',
|
||||
description:
|
||||
'Artist, variation of artist name and piece of text to join the artist ' +
|
||||
'name to the next.',
|
||||
description: `[Artist credits](https://musicbrainz.org/doc/Artist_Credits)
|
||||
indicate who is the main credited artist (or artists) for releases, release
|
||||
groups, tracks and recordings, and how they are credited. They consist of
|
||||
artists, with (optionally) their names as credited in the specific release,
|
||||
track, etc., and join phrases between them.`,
|
||||
fields: () => ({
|
||||
artist: {
|
||||
type: Artist,
|
||||
description: `The entity representing the artist referenced in the
|
||||
credits.`,
|
||||
resolve: (source) => {
|
||||
const { artist } = source
|
||||
artist.entityType = 'artist'
|
||||
if (artist) {
|
||||
artist.entityType = 'artist'
|
||||
}
|
||||
return artist
|
||||
}
|
||||
},
|
||||
name,
|
||||
joinPhrase: { type: GraphQLString, resolve: data => data['joinphrase'] }
|
||||
name: {
|
||||
type: GraphQLString,
|
||||
description: `The name of the artist as credited in the specific release,
|
||||
track, etc.`
|
||||
},
|
||||
joinPhrase: {
|
||||
type: GraphQLString,
|
||||
description: `Join phrases might include words and/or punctuation to
|
||||
separate artist names as they appear on the release, track, etc.`,
|
||||
resolve: data => data['joinphrase']
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -17,14 +17,16 @@ import {
|
|||
releases,
|
||||
releaseGroups,
|
||||
works,
|
||||
relations
|
||||
relationships
|
||||
} from './helpers'
|
||||
|
||||
const Artist = new GraphQLObjectType({
|
||||
name: 'Artist',
|
||||
description:
|
||||
'An artist is generally a musician, a group of musicians, or another ' +
|
||||
'music professional (composer, engineer, illustrator, producer, etc.)',
|
||||
description: `An [artist](https://musicbrainz.org/doc/Artist) is generally a
|
||||
musician, group of musicians, or other music professional (like a producer or
|
||||
engineer). Occasionally, it can also be a non-musical person (like a
|
||||
photographer, an illustrator, or a poet whose writings are set to music), or
|
||||
even a fictional character.`,
|
||||
interfaces: () => [Node, Entity],
|
||||
fields: () => ({
|
||||
id,
|
||||
|
|
@ -34,37 +36,56 @@ const Artist = new GraphQLObjectType({
|
|||
disambiguation,
|
||||
aliases: {
|
||||
type: new GraphQLList(Alias),
|
||||
resolve: (source, args, { lookupLoader }, info) => {
|
||||
description: `[Aliases](https://musicbrainz.org/doc/Aliases) are used to
|
||||
store alternate names or misspellings.`,
|
||||
resolve: (source, args, { loaders }, info) => {
|
||||
const key = 'aliases'
|
||||
if (key in source) {
|
||||
return source[key]
|
||||
} else {
|
||||
const { entityType, id } = source
|
||||
const params = { inc: ['aliases'] }
|
||||
return lookupLoader.load([entityType, id, params]).then(entity => {
|
||||
return loaders.lookup.load([entityType, id, params]).then(entity => {
|
||||
return entity[key]
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
country: { type: GraphQLString },
|
||||
area: { type: Area },
|
||||
country: {
|
||||
type: GraphQLString,
|
||||
description: `The country with which an artist is primarily identified. It
|
||||
is often, but not always, its birth/formation country.`
|
||||
},
|
||||
area: {
|
||||
type: Area,
|
||||
description: `The area with which an artist is primarily identified. It
|
||||
is often, but not always, its birth/formation country.`
|
||||
},
|
||||
beginArea: {
|
||||
type: Area,
|
||||
description: `The area in which an artist began their career (or where
|
||||
were born, if the artist is a person).`,
|
||||
resolve: getFallback(['begin-area', 'begin_area'])
|
||||
},
|
||||
endArea: {
|
||||
type: Area,
|
||||
description: `The area in which an artist ended their career (or where
|
||||
they died, if the artist is a person).`,
|
||||
resolve: getFallback(['end-area', 'end_area'])
|
||||
},
|
||||
lifeSpan,
|
||||
...fieldWithID('gender'),
|
||||
...fieldWithID('type'),
|
||||
...fieldWithID('gender', {
|
||||
description: `Whether a person or character identifies as male, female, or
|
||||
neither. Groups do not have genders.`
|
||||
}),
|
||||
...fieldWithID('type', {
|
||||
description: 'Whether an artist is a person, a group, or something else.'
|
||||
}),
|
||||
recordings,
|
||||
releases,
|
||||
releaseGroups,
|
||||
works,
|
||||
relations
|
||||
relationships
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,199 +1,186 @@
|
|||
import { GraphQLEnumType } from 'graphql/type'
|
||||
|
||||
/*
|
||||
ReleaseStatus {
|
||||
OFFICIAL
|
||||
PROMOTION
|
||||
BOOTLEG
|
||||
PSEUDORELEASE
|
||||
}
|
||||
*/
|
||||
export const ArtistType = new GraphQLEnumType({
|
||||
name: 'ArtistType',
|
||||
values: {
|
||||
PERSON: {
|
||||
name: 'Person',
|
||||
description: 'This indicates an individual person.',
|
||||
value: 'Person'
|
||||
},
|
||||
GROUP: {
|
||||
name: 'Group',
|
||||
description: `This indicates a group of people that may or may not have a
|
||||
distinctive name.`,
|
||||
value: 'Group'
|
||||
},
|
||||
ORCHESTRA: {
|
||||
name: 'Orchestra',
|
||||
description: 'This indicates an orchestra (a large instrumental ensemble).',
|
||||
value: 'Orchestra'
|
||||
},
|
||||
CHOIR: {
|
||||
name: 'Choir',
|
||||
description: 'This indicates a choir/chorus (a large vocal ensemble).',
|
||||
value: 'Choir'
|
||||
},
|
||||
CHARACTER: {
|
||||
name: 'Character',
|
||||
description: 'This indicates an individual fictional character.',
|
||||
value: 'Character'
|
||||
},
|
||||
OTHER: {
|
||||
name: 'Other',
|
||||
description: 'An artist which does not fit into the other categories.',
|
||||
value: 'Other'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const ReleaseStatus = new GraphQLEnumType({
|
||||
name: 'ReleaseStatus',
|
||||
values: {
|
||||
OFFICIAL: {
|
||||
name: 'Official',
|
||||
description:
|
||||
'Any release officially sanctioned by the artist and/or their record ' +
|
||||
'company. (Most releases will fit into this category.)',
|
||||
value: 'official'
|
||||
description: `Any release officially sanctioned by the artist and/or their
|
||||
record company. (Most releases will fit into this category.)`,
|
||||
value: 'Official'
|
||||
},
|
||||
PROMOTION: {
|
||||
name: 'Promotion',
|
||||
description:
|
||||
'A giveaway release or a release intended to promote an upcoming ' +
|
||||
'official release. (e.g. prerelease albums or releases included ' +
|
||||
'with a magazine)',
|
||||
value: 'promotion'
|
||||
description: `A giveaway release or a release intended to promote an
|
||||
upcoming official release. (e.g. prerelease albums or releases included with a
|
||||
magazine)`,
|
||||
value: 'Promotion'
|
||||
},
|
||||
BOOTLEG: {
|
||||
name: 'Bootleg',
|
||||
description:
|
||||
'An unofficial/underground release that was not sanctioned by the ' +
|
||||
'artist and/or the record company.',
|
||||
value: 'bootleg'
|
||||
description: `An unofficial/underground release that was not sanctioned by
|
||||
the artist and/or the record company.`,
|
||||
value: 'Bootleg'
|
||||
},
|
||||
PSEUDORELEASE: {
|
||||
name: 'Pseudo-Release',
|
||||
description:
|
||||
'A pseudo-release is a duplicate release for translation/' +
|
||||
'transliteration purposes.',
|
||||
value: 'pseudo-release'
|
||||
description: `A pseudo-release is a duplicate release for
|
||||
translation/transliteration purposes.`,
|
||||
value: 'Pseudo-Release'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
enum ReleaseGroupType {
|
||||
# Primary types
|
||||
ALBUM
|
||||
SINGLE
|
||||
EP
|
||||
OTHER
|
||||
BROADCAST
|
||||
|
||||
# Secondary types
|
||||
COMPILATION
|
||||
SOUNDTRACK
|
||||
SPOKEN_WORD
|
||||
INTERVIEW
|
||||
AUDIOBOOK
|
||||
LIVE
|
||||
REMIX
|
||||
DJMIX
|
||||
MIXTAPE
|
||||
DEMO
|
||||
NAT
|
||||
}
|
||||
*/
|
||||
export const ReleaseGroupType = new GraphQLEnumType({
|
||||
name: 'ReleaseGroupType',
|
||||
values: {
|
||||
ALBUM: {
|
||||
name: 'Album',
|
||||
description:
|
||||
'An album, perhaps better defined as a “Long Play” (LP) release, ' +
|
||||
'generally consists of previously unreleased material (unless this ' +
|
||||
'type is combined with secondary types which change that, such as ' +
|
||||
'“Compilation”). This includes album re-issues, with or without ' +
|
||||
'bonus tracks.',
|
||||
value: 'album'
|
||||
description: `An album, perhaps better defined as a “Long Play” (LP)
|
||||
release, generally consists of previously unreleased material (unless this type
|
||||
is combined with secondary types which change that, such as “Compilation”). This
|
||||
includes album re-issues, with or without bonus tracks.`,
|
||||
value: 'Album'
|
||||
},
|
||||
SINGLE: {
|
||||
name: 'Single',
|
||||
description:
|
||||
'A single typically has one main song and possibly a handful of ' +
|
||||
'additional tracks or remixes of the main track. A single is usually ' +
|
||||
'named after its main song.',
|
||||
value: 'single'
|
||||
description: `A single typically has one main song and possibly a handful
|
||||
of additional tracks or remixes of the main track. A single is usually named
|
||||
after its main song.`,
|
||||
value: 'Single'
|
||||
},
|
||||
EP: {
|
||||
name: 'EP',
|
||||
description:
|
||||
'An EP is a so-called “Extended Play” release and often contains the ' +
|
||||
'letters EP in the title. Generally an EP will be shorter than a ' +
|
||||
'full length release (an LP or “Long Play”) and the tracks are ' +
|
||||
'usually exclusive to the EP, in other words the tracks don’t come ' +
|
||||
'from a previously issued release. EP is fairly difficult to define; ' +
|
||||
'usually it should only be assumed that a release is an EP if the ' +
|
||||
'artist defines it as such.',
|
||||
value: 'ep'
|
||||
description: `An EP is a so-called “Extended Play” release and often
|
||||
contains the letters EP in the title. Generally an EP will be shorter than a
|
||||
full length release (an LP or “Long Play”) and the tracks are usually exclusive
|
||||
to the EP, in other words the tracks don’t come from a previously issued
|
||||
release. EP is fairly difficult to define; usually it should only be assumed
|
||||
that a release is an EP if the artist defines it as such.`,
|
||||
value: 'EP'
|
||||
},
|
||||
OTHER: {
|
||||
name: 'Other',
|
||||
description:
|
||||
'Any release that does not fit any of the other categories.',
|
||||
value: 'other'
|
||||
description: 'Any release that does not fit any of the other categories.',
|
||||
value: 'Other'
|
||||
},
|
||||
BROADCAST: {
|
||||
name: 'Broadcast',
|
||||
description:
|
||||
'An episodic release that was originally broadcast via radio, ' +
|
||||
'television, or the Internet, including podcasts.',
|
||||
value: 'broadcast'
|
||||
description: `An episodic release that was originally broadcast via radio,
|
||||
television, or the Internet, including podcasts.`,
|
||||
value: 'Broadcast'
|
||||
},
|
||||
COMPILATION: {
|
||||
name: 'Compilation',
|
||||
description:
|
||||
'A compilation is a collection of previously released tracks by one ' +
|
||||
'or more artists.',
|
||||
value: 'compilation'
|
||||
description: `A compilation is a collection of previously released tracks
|
||||
by one or more artists.`,
|
||||
value: 'Compilation'
|
||||
},
|
||||
SOUNDTRACK: {
|
||||
name: 'Soundtrack',
|
||||
description:
|
||||
'A soundtrack is the musical score to a movie, TV series, stage ' +
|
||||
'show, computer game etc.',
|
||||
value: 'soundtrack'
|
||||
description: `A soundtrack is the musical score to a movie, TV series,
|
||||
stage show, computer game etc.`,
|
||||
value: 'Soundtrack'
|
||||
},
|
||||
SPOKENWORD: {
|
||||
name: 'Spoken Word',
|
||||
description: 'A non-music spoken word release.',
|
||||
value: 'spokenword'
|
||||
value: 'Spoken Word'
|
||||
},
|
||||
INTERVIEW: {
|
||||
name: 'Interview',
|
||||
description:
|
||||
'An interview release contains an interview, generally with an artist.',
|
||||
value: 'interview'
|
||||
description: `An interview release contains an interview, generally with
|
||||
an artist.`,
|
||||
value: 'Interview'
|
||||
},
|
||||
AUDIOBOOK: {
|
||||
name: 'Audiobook',
|
||||
description: 'An audiobook is a book read by a narrator without music.',
|
||||
value: 'audiobook'
|
||||
value: 'Audiobook'
|
||||
},
|
||||
LIVE: {
|
||||
name: 'Live',
|
||||
description: 'A release that was recorded live.',
|
||||
value: 'live'
|
||||
value: 'Live'
|
||||
},
|
||||
REMIX: {
|
||||
name: 'Remix',
|
||||
description:
|
||||
'A release that was (re)mixed from previously released material.',
|
||||
value: 'remix'
|
||||
description: `A release that was (re)mixed from previously released
|
||||
material.`,
|
||||
value: 'Remix'
|
||||
},
|
||||
DJMIX: {
|
||||
name: 'DJ-mix',
|
||||
description:
|
||||
'A DJ-mix is a sequence of several recordings played one after the ' +
|
||||
'other, each one modified so that they blend together into a ' +
|
||||
'continuous flow of music. A DJ mix release requires that the ' +
|
||||
'recordings be modified in some manner, and the DJ who does this ' +
|
||||
'modification is usually (although not always) credited in a fairly ' +
|
||||
'prominent way.',
|
||||
value: 'dj-mix'
|
||||
description: `A DJ-mix is a sequence of several recordings played one
|
||||
after the other, each one modified so that they blend together into a continuous
|
||||
flow of music. A DJ mix release requires that the recordings be modified in some
|
||||
manner, and the DJ who does this modification is usually (although not always)
|
||||
credited in a fairly prominent way.`,
|
||||
value: 'DJ-mix'
|
||||
},
|
||||
MIXTAPE: {
|
||||
name: 'Mixtape/Street',
|
||||
description:
|
||||
'Promotional in nature (but not necessarily free), mixtapes and ' +
|
||||
'street albums are often released by artists to promote new artists, ' +
|
||||
'or upcoming studio albums by prominent artists. They are also ' +
|
||||
'sometimes used to keep fans’ attention between studio releases and ' +
|
||||
'are most common in rap & hip hop genres. They are often not ' +
|
||||
'sanctioned by the artist’s label, may lack proper sample or song ' +
|
||||
'clearances and vary widely in production and recording quality. ' +
|
||||
'While mixtapes are generally DJ-mixed, they are distinct from ' +
|
||||
'commercial DJ mixes (which are usually deemed compilations) and are ' +
|
||||
'defined by having a significant proportion of new material, ' +
|
||||
'including original production or original vocals over top of other ' +
|
||||
'artists’ instrumentals. They are distinct from demos in that they ' +
|
||||
'are designed for release directly to the public and fans; not ' +
|
||||
'to labels.',
|
||||
value: 'mixtape/street'
|
||||
description: `Promotional in nature (but not necessarily free), mixtapes
|
||||
and street albums are often released by artists to promote new artists, or
|
||||
upcoming studio albums by prominent artists. They are also sometimes used to
|
||||
keep fans’ attention between studio releases and are most common in rap & hip
|
||||
hop genres. They are often not sanctioned by the artist’s label, may lack proper
|
||||
sample or song clearances and vary widely in production and recording quality.
|
||||
While mixtapes are generally DJ-mixed, they are distinct from commercial DJ
|
||||
mixes (which are usually deemed compilations) and are defined by having a
|
||||
significant proportion of new material, including original production or
|
||||
original vocals over top of other artists’ instrumentals. They are distinct from
|
||||
demos in that they are designed for release directly to the public and fans; not
|
||||
to labels.`,
|
||||
value: 'Mixtape/Street'
|
||||
},
|
||||
DEMO: {
|
||||
name: 'Demo',
|
||||
description:
|
||||
'A release that was recorded for limited circulation or reference ' +
|
||||
'use rather than for general public release.',
|
||||
value: 'demo'
|
||||
description: `A release that was recorded for limited circulation or
|
||||
reference use rather than for general public release.`,
|
||||
value: 'Demo'
|
||||
},
|
||||
NAT: {
|
||||
name: 'Non-Album Track',
|
||||
description: 'A non-album track (special case).',
|
||||
value: 'nat'
|
||||
value: 'NAT'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ import {
|
|||
|
||||
const Event = new GraphQLObjectType({
|
||||
name: 'Event',
|
||||
description:
|
||||
'An organized event which people can attend, usually live performances ' +
|
||||
'like concerts and festivals.',
|
||||
description: `An [event](https://musicbrainz.org/doc/Event) refers to an
|
||||
organised event which people can attend, and is relevant to MusicBrainz.
|
||||
Generally this means live performances, like concerts and festivals.`,
|
||||
interfaces: () => [Node, Entity],
|
||||
fields: () => ({
|
||||
id,
|
||||
|
|
@ -24,10 +24,23 @@ const Event = new GraphQLObjectType({
|
|||
name,
|
||||
disambiguation,
|
||||
lifeSpan,
|
||||
time: { type: Time },
|
||||
cancelled: { type: GraphQLBoolean },
|
||||
setlist: { type: GraphQLString },
|
||||
...fieldWithID('type')
|
||||
time: {
|
||||
type: Time,
|
||||
description: 'The start time of the event.'
|
||||
},
|
||||
cancelled: {
|
||||
type: GraphQLBoolean,
|
||||
description: 'Whether or not the event took place.'
|
||||
},
|
||||
setlist: {
|
||||
type: GraphQLString,
|
||||
description: `A list of songs performed, optionally including links to
|
||||
artists and works. See the [setlist documentation](https://musicbrainz.org/doc/Event/Setlist)
|
||||
for syntax and examples.`
|
||||
},
|
||||
...fieldWithID('type', {
|
||||
description: 'What kind of event the event is, e.g. concert, festival, etc.'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -20,18 +20,34 @@ import { LabelConnection } from './label'
|
|||
import LifeSpan from './life-span'
|
||||
import { PlaceConnection } from './place'
|
||||
import { RecordingConnection } from './recording'
|
||||
import Relation from './relation'
|
||||
import { RelationshipConnection } from './relationship'
|
||||
import { ReleaseConnection } from './release'
|
||||
import { ReleaseGroupConnection } from './release-group'
|
||||
import { WorkConnection } from './work'
|
||||
import {
|
||||
linkedResolver,
|
||||
relationResolver,
|
||||
includeRelations
|
||||
relationshipResolver,
|
||||
includeRelationships
|
||||
} from '../resolvers'
|
||||
|
||||
export const toNodeType = pascalCase
|
||||
export const toEntityType = dashify
|
||||
export const toPascal = pascalCase
|
||||
export const toDashed = dashify
|
||||
|
||||
export function toPlural (name) {
|
||||
return name.endsWith('s') ? name : name + 's'
|
||||
}
|
||||
|
||||
export function toSingular (name) {
|
||||
return name.endsWith('s') && !/series/i.test(name) ? name.slice(0, -1) : name
|
||||
}
|
||||
|
||||
export function toWords (name) {
|
||||
return toPascal(name).replace(/([^A-Z])?([A-Z]+)/g, (match, tail, head) => {
|
||||
tail = tail ? tail + ' ' : ''
|
||||
head = head.length > 1 ? head : head.toLowerCase()
|
||||
return `${tail}${head}`
|
||||
})
|
||||
}
|
||||
|
||||
export function fieldWithID (name, config = {}) {
|
||||
config = {
|
||||
|
|
@ -40,10 +56,12 @@ export function fieldWithID (name, config = {}) {
|
|||
...config
|
||||
}
|
||||
const isPlural = config.type instanceof GraphQLList
|
||||
const singularName = isPlural && name.endsWith('s') ? name.slice(0, -1) : name
|
||||
const singularName = isPlural ? toSingular(name) : name
|
||||
const idName = isPlural ? `${singularName}IDs` : `${name}ID`
|
||||
const idConfig = {
|
||||
type: isPlural ? new GraphQLList(MBID) : MBID,
|
||||
description: `The MBID${isPlural ? 's' : ''} associated with the
|
||||
value${isPlural ? 's' : ''} of the \`${name}\` field.`,
|
||||
resolve: getHyphenated
|
||||
}
|
||||
return {
|
||||
|
|
@ -74,15 +92,37 @@ export const mbid = {
|
|||
description: 'The MBID of the entity.',
|
||||
resolve: source => source.id
|
||||
}
|
||||
export const name = { type: GraphQLString }
|
||||
export const sortName = { type: GraphQLString, resolve: getHyphenated }
|
||||
export const title = { type: GraphQLString }
|
||||
export const disambiguation = { type: GraphQLString }
|
||||
export const lifeSpan = { type: LifeSpan, resolve: getHyphenated }
|
||||
export const name = {
|
||||
type: GraphQLString,
|
||||
description: 'The official name of the entity.'
|
||||
}
|
||||
export const sortName = {
|
||||
type: GraphQLString,
|
||||
description: `The string to use for the purpose of ordering by name (for
|
||||
example, by moving articles like ‘the’ to the end or a person’s last name to
|
||||
the front).`,
|
||||
resolve: getHyphenated
|
||||
}
|
||||
export const title = {
|
||||
type: GraphQLString,
|
||||
description: 'The official title of the entity.'
|
||||
}
|
||||
export const disambiguation = {
|
||||
type: GraphQLString,
|
||||
description: 'A comment used to help distinguish identically named entitites.'
|
||||
}
|
||||
export const lifeSpan = {
|
||||
type: LifeSpan,
|
||||
description: `The begin and end dates of the entity’s existence. Its exact
|
||||
meaning depends on the type of entity.`,
|
||||
resolve: getHyphenated
|
||||
}
|
||||
|
||||
function linkedQuery (connectionType, args) {
|
||||
const typeName = toWords(connectionType.name.slice(0, -10))
|
||||
return {
|
||||
type: connectionType,
|
||||
description: `A list of ${typeName} entities linked to this entity.`,
|
||||
args: {
|
||||
...forwardConnectionArgs,
|
||||
...args
|
||||
|
|
@ -91,43 +131,50 @@ function linkedQuery (connectionType, args) {
|
|||
}
|
||||
}
|
||||
|
||||
export const relation = {
|
||||
type: new GraphQLList(Relation),
|
||||
export const relationship = {
|
||||
type: RelationshipConnection,
|
||||
description: 'A list of relationships between these two entity types.',
|
||||
args: {
|
||||
...connectionArgs,
|
||||
direction: { type: GraphQLString },
|
||||
type: { type: GraphQLString },
|
||||
typeID: { type: MBID }
|
||||
direction: {
|
||||
type: GraphQLString,
|
||||
description: 'Filter by the relationship direction.'
|
||||
},
|
||||
...fieldWithID('type', {
|
||||
description: 'Filter by the relationship type.'
|
||||
})
|
||||
},
|
||||
resolve: relationResolver()
|
||||
resolve: relationshipResolver()
|
||||
}
|
||||
|
||||
export const relations = {
|
||||
export const relationships = {
|
||||
type: new GraphQLObjectType({
|
||||
name: 'Relations',
|
||||
name: 'Relationships',
|
||||
description: 'Lists of entity relationships for each entity type.',
|
||||
fields: () => ({
|
||||
area: relation,
|
||||
artist: relation,
|
||||
event: relation,
|
||||
instrument: relation,
|
||||
label: relation,
|
||||
place: relation,
|
||||
recording: relation,
|
||||
release: relation,
|
||||
releaseGroup: relation,
|
||||
series: relation,
|
||||
url: relation,
|
||||
work: relation
|
||||
areas: relationship,
|
||||
artists: relationship,
|
||||
events: relationship,
|
||||
instruments: relationship,
|
||||
labels: relationship,
|
||||
places: relationship,
|
||||
recordings: relationship,
|
||||
releases: relationship,
|
||||
releaseGroups: relationship,
|
||||
series: relationship,
|
||||
urls: relationship,
|
||||
works: relationship
|
||||
})
|
||||
}),
|
||||
resolve: (source, args, { lookupLoader }, info) => {
|
||||
description: 'Relationships between this entity and other entitites.',
|
||||
resolve: (source, args, { loaders }, info) => {
|
||||
if (source.relations != null) {
|
||||
return source.relations
|
||||
}
|
||||
const entityType = toEntityType(info.parentType.name)
|
||||
const entityType = toDashed(info.parentType.name)
|
||||
const id = source.id
|
||||
const params = includeRelations({}, info)
|
||||
return lookupLoader.load([entityType, id, params]).then(entity => {
|
||||
const params = includeRelationships({}, info)
|
||||
return loaders.lookup.load([entityType, id, params]).then(entity => {
|
||||
return entity.relations
|
||||
})
|
||||
}
|
||||
|
|
@ -135,14 +182,15 @@ export const relations = {
|
|||
|
||||
export const artistCredit = {
|
||||
type: new GraphQLList(ArtistCredit),
|
||||
resolve: (source, args, { lookupLoader }, info) => {
|
||||
description: 'The main credited artist(s).',
|
||||
resolve: (source, args, { loaders }, info) => {
|
||||
const key = 'artist-credit'
|
||||
if (key in source) {
|
||||
return source[key]
|
||||
} else {
|
||||
const { entityType, id } = source
|
||||
const params = { inc: ['artists'] }
|
||||
return lookupLoader.load([entityType, id, params]).then(entity => {
|
||||
return loaders.lookup.load([entityType, id, params]).then(entity => {
|
||||
return entity[key]
|
||||
})
|
||||
}
|
||||
|
|
@ -154,17 +202,11 @@ export const events = linkedQuery(EventConnection)
|
|||
export const labels = linkedQuery(LabelConnection)
|
||||
export const places = linkedQuery(PlaceConnection)
|
||||
export const recordings = linkedQuery(RecordingConnection)
|
||||
|
||||
export const releases = linkedQuery(ReleaseConnection, {
|
||||
type: { type: ReleaseGroupType },
|
||||
types: { type: new GraphQLList(ReleaseGroupType) },
|
||||
status: { type: ReleaseStatus },
|
||||
statuses: { type: new GraphQLList(ReleaseStatus) }
|
||||
type: { type: new GraphQLList(ReleaseGroupType) },
|
||||
status: { type: new GraphQLList(ReleaseStatus) }
|
||||
})
|
||||
|
||||
export const releaseGroups = linkedQuery(ReleaseGroupConnection, {
|
||||
type: { type: ReleaseGroupType },
|
||||
types: { type: new GraphQLList(ReleaseGroupType) }
|
||||
type: { type: new GraphQLList(ReleaseGroupType) }
|
||||
})
|
||||
|
||||
export const works = linkedQuery(WorkConnection)
|
||||
|
|
|
|||
|
|
@ -11,16 +11,25 @@ import {
|
|||
|
||||
const Instrument = new GraphQLObjectType({
|
||||
name: 'Instrument',
|
||||
description:
|
||||
'Instruments are devices created or adapted to make musical sounds.',
|
||||
description: `[Instruments](https://musicbrainz.org/doc/Instrument) are
|
||||
devices created or adapted to make musical sounds. Instruments are primarily
|
||||
used in relationships between two other entities.`,
|
||||
interfaces: () => [Node, Entity],
|
||||
fields: () => ({
|
||||
id,
|
||||
mbid,
|
||||
name,
|
||||
disambiguation,
|
||||
description: { type: GraphQLString },
|
||||
...fieldWithID('type')
|
||||
description: {
|
||||
type: GraphQLString,
|
||||
description: `A brief description of the main characteristics of the
|
||||
instrument.`
|
||||
},
|
||||
...fieldWithID('type', {
|
||||
description: `The type categorises the instrument by the way the sound is
|
||||
created, similar to the [Hornbostel-Sachs](https://en.wikipedia.org/wiki/Hornbostel%E2%80%93Sachs)
|
||||
classification.`
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ import {
|
|||
|
||||
const Label = new GraphQLObjectType({
|
||||
name: 'Label',
|
||||
description: 'Labels represent mostly (but not only) imprints.',
|
||||
description: `[Labels](https://musicbrainz.org/doc/Label) represent mostly
|
||||
(but not only) imprints. To a lesser extent, a label entity may be created to
|
||||
represent a record company.`,
|
||||
interfaces: () => [Node, Entity],
|
||||
fields: () => ({
|
||||
id,
|
||||
|
|
@ -30,12 +32,29 @@ const Label = new GraphQLObjectType({
|
|||
name,
|
||||
sortName,
|
||||
disambiguation,
|
||||
country: { type: GraphQLString },
|
||||
area: { type: Area },
|
||||
country: {
|
||||
type: GraphQLString,
|
||||
description: 'The country of origin for the label.'
|
||||
},
|
||||
area: {
|
||||
type: Area,
|
||||
description: 'The area in which the label is based.'
|
||||
},
|
||||
lifeSpan,
|
||||
labelCode: { type: GraphQLInt },
|
||||
ipis: { type: new GraphQLList(IPI) },
|
||||
...fieldWithID('type'),
|
||||
labelCode: {
|
||||
type: GraphQLInt,
|
||||
description: `The [“LC” code](https://musicbrainz.org/doc/Label/Label_Code)
|
||||
of the label.`
|
||||
},
|
||||
ipis: {
|
||||
type: new GraphQLList(IPI),
|
||||
description: `List of IPI (interested party information) codes for the
|
||||
label.`
|
||||
},
|
||||
...fieldWithID('type', {
|
||||
description: `A type describing the main activity of the label, e.g.
|
||||
imprint, production, distributor, rights society, etc.`
|
||||
}),
|
||||
releases
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { DateType } from './scalars'
|
|||
|
||||
export default new GraphQLObjectType({
|
||||
name: 'LifeSpan',
|
||||
description:
|
||||
'Begin and end date of an entity that may have a finite lifetime.',
|
||||
description: `Fields indicating the begin and end date of an entity’s
|
||||
lifetime, including whether it has ended (even if the date is unknown).`,
|
||||
fields: () => ({
|
||||
begin: { type: DateType },
|
||||
end: { type: DateType },
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { nodeDefinitions, fromGlobalId } from 'graphql-relay'
|
||||
import { lookupLoader } from '../loaders'
|
||||
import { toEntityType } from './helpers'
|
||||
import { toDashed } from './helpers'
|
||||
|
||||
const { nodeInterface, nodeField } = nodeDefinitions(
|
||||
(globalID) => {
|
||||
(globalID, { loaders }) => {
|
||||
const { type, id } = fromGlobalId(globalID)
|
||||
const entityType = toEntityType(type)
|
||||
return lookupLoader.load([entityType, id])
|
||||
const entityType = toDashed(type)
|
||||
return loaders.lookup.load([entityType, id])
|
||||
},
|
||||
(obj) => {
|
||||
console.log(obj.entityType)
|
||||
try {
|
||||
return require(`./${obj.entityType}`).default
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -16,29 +16,48 @@ import {
|
|||
|
||||
export const Coordinates = new GraphQLObjectType({
|
||||
name: 'Coordinates',
|
||||
description: 'Geographic coordinates with latitude and longitude.',
|
||||
description: 'Geographic coordinates described with latitude and longitude.',
|
||||
fields: () => ({
|
||||
latitude: { type: Degrees },
|
||||
longitude: { type: Degrees }
|
||||
latitude: {
|
||||
type: Degrees,
|
||||
description: 'The north–south position of a point on the Earth’s surface.'
|
||||
},
|
||||
longitude: {
|
||||
type: Degrees,
|
||||
description: 'The east–west position of a point on the Earth’s surface.'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const Place = new GraphQLObjectType({
|
||||
name: 'Place',
|
||||
description:
|
||||
'A venue, studio or other place where music is performed, recorded, ' +
|
||||
'engineered, etc.',
|
||||
description: `A [place](https://musicbrainz.org/doc/Place) is a venue, studio
|
||||
or other place where music is performed, recorded, engineered, etc.`,
|
||||
interfaces: () => [Node, Entity],
|
||||
fields: () => ({
|
||||
id,
|
||||
mbid,
|
||||
name,
|
||||
disambiguation,
|
||||
address: { type: GraphQLString },
|
||||
area: { type: Area },
|
||||
coordinates: { type: Coordinates },
|
||||
address: {
|
||||
type: GraphQLString,
|
||||
description: `The address describes the location of the place using the
|
||||
standard addressing format for the country it is located in.`
|
||||
},
|
||||
area: {
|
||||
type: Area,
|
||||
description: `The area entity representing the area, such as the city, in
|
||||
which the place is located.`
|
||||
},
|
||||
coordinates: {
|
||||
type: Coordinates,
|
||||
description: 'The geographic coordinates of the place.'
|
||||
},
|
||||
lifeSpan,
|
||||
...fieldWithID('type'),
|
||||
...fieldWithID('type', {
|
||||
description: `The type categorises the place based on its primary
|
||||
function.`
|
||||
}),
|
||||
events
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,14 +10,23 @@ import {
|
|||
artistCredit,
|
||||
artists,
|
||||
releases,
|
||||
relations
|
||||
relationships
|
||||
} from './helpers'
|
||||
|
||||
const Recording = new GraphQLObjectType({
|
||||
name: 'Recording',
|
||||
description:
|
||||
'Represents a unique mix or edit. Has title, artist credit, duration, ' +
|
||||
'list of PUIDs and ISRCs.',
|
||||
description: `A [recording](https://musicbrainz.org/doc/Recording) is an
|
||||
entity in MusicBrainz which can be linked to tracks on releases. Each track must
|
||||
always be associated with a single recording, but a recording can be linked to
|
||||
any number of tracks.
|
||||
|
||||
A recording represents distinct audio that has been used to produce at least one
|
||||
released track through copying or mastering. A recording itself is never
|
||||
produced solely through copying or mastering.
|
||||
|
||||
Generally, the audio represented by a recording corresponds to the audio at a
|
||||
stage in the production process before any final mastering but after any editing
|
||||
or mixing.`,
|
||||
interfaces: () => [Node, Entity],
|
||||
fields: () => ({
|
||||
id,
|
||||
|
|
@ -25,11 +34,18 @@ const Recording = new GraphQLObjectType({
|
|||
title,
|
||||
disambiguation,
|
||||
artistCredit,
|
||||
length: { type: GraphQLInt },
|
||||
video: { type: GraphQLBoolean },
|
||||
length: {
|
||||
type: GraphQLInt,
|
||||
description: `An approximation to the length of the recording, calculated
|
||||
from the lengths of the tracks using it.`
|
||||
},
|
||||
video: {
|
||||
type: GraphQLBoolean,
|
||||
description: 'Whether this is a video recording.'
|
||||
},
|
||||
artists,
|
||||
releases,
|
||||
relations
|
||||
relationships
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
import {
|
||||
GraphQLObjectType,
|
||||
GraphQLNonNull,
|
||||
GraphQLString,
|
||||
GraphQLList,
|
||||
GraphQLBoolean
|
||||
} from 'graphql/type'
|
||||
import { DateType } from './scalars'
|
||||
import Entity from './entity'
|
||||
import {
|
||||
getHyphenated,
|
||||
fieldWithID
|
||||
} from './helpers'
|
||||
|
||||
const Relation = new GraphQLObjectType({
|
||||
name: 'Relation',
|
||||
fields: () => ({
|
||||
target: {
|
||||
type: new GraphQLNonNull(Entity),
|
||||
resolve: source => {
|
||||
const targetType = source['target-type']
|
||||
const target = source[targetType]
|
||||
target.entityType = targetType.replace('_', '-')
|
||||
return target
|
||||
}
|
||||
},
|
||||
direction: { type: new GraphQLNonNull(GraphQLString) },
|
||||
targetType: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
resolve: getHyphenated
|
||||
},
|
||||
sourceCredit: { type: GraphQLString, resolve: getHyphenated },
|
||||
targetCredit: { type: GraphQLString, resolve: getHyphenated },
|
||||
begin: { type: DateType },
|
||||
end: { type: DateType },
|
||||
ended: { type: GraphQLBoolean },
|
||||
attributes: { type: new GraphQLList(GraphQLString) },
|
||||
// attributeValues: {},
|
||||
...fieldWithID('type')
|
||||
})
|
||||
})
|
||||
|
||||
export default Relation
|
||||
83
src/types/relationship.js
Normal file
83
src/types/relationship.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import {
|
||||
GraphQLObjectType,
|
||||
GraphQLNonNull,
|
||||
GraphQLString,
|
||||
GraphQLList,
|
||||
GraphQLBoolean
|
||||
} from 'graphql/type'
|
||||
import { connectionDefinitions } from 'graphql-relay'
|
||||
import { DateType } from './scalars'
|
||||
import Entity from './entity'
|
||||
import {
|
||||
getHyphenated,
|
||||
fieldWithID
|
||||
} from './helpers'
|
||||
|
||||
const Relationship = new GraphQLObjectType({
|
||||
name: 'Relationship',
|
||||
description: `[Relationships](https://musicbrainz.org/doc/Relationships) are a
|
||||
way to represent all the different ways in which entities are connected to each
|
||||
other and to URLs outside MusicBrainz.`,
|
||||
fields: () => ({
|
||||
target: {
|
||||
type: new GraphQLNonNull(Entity),
|
||||
description: 'The target entity.',
|
||||
resolve: source => {
|
||||
const targetType = source['target-type']
|
||||
const target = source[targetType]
|
||||
target.entityType = targetType.replace('_', '-')
|
||||
return target
|
||||
}
|
||||
},
|
||||
direction: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
description: 'The direction of the relationship.'
|
||||
},
|
||||
targetType: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
description: 'The type of entity on the receiving end of the relationship.',
|
||||
resolve: getHyphenated
|
||||
},
|
||||
sourceCredit: {
|
||||
type: GraphQLString,
|
||||
description: `How the source entity was actually credited, if different
|
||||
from its main (performance) name.`,
|
||||
resolve: getHyphenated
|
||||
},
|
||||
targetCredit: {
|
||||
type: GraphQLString,
|
||||
description: `How the target entity was actually credited, if different
|
||||
from its main (performance) name.`,
|
||||
resolve: getHyphenated
|
||||
},
|
||||
begin: {
|
||||
type: DateType,
|
||||
description: 'The date on which the relationship became applicable.'
|
||||
},
|
||||
end: {
|
||||
type: DateType,
|
||||
description: 'The date on which the relationship became no longer applicable.'
|
||||
},
|
||||
ended: {
|
||||
type: GraphQLBoolean,
|
||||
description: 'Whether the relationship still applies.'
|
||||
},
|
||||
attributes: {
|
||||
type: new GraphQLList(GraphQLString),
|
||||
description: `Attributes which modify the relationship. There is a [list
|
||||
of all attributes](https://musicbrainz.org/relationship-attributes), but the
|
||||
attributes which are available, and how they should be used, depends on the
|
||||
relationship type.`
|
||||
},
|
||||
// There doesn't seem to be any documentation for the `attribute-values`
|
||||
// field.
|
||||
// attributeValues: {},
|
||||
...fieldWithID('type', {
|
||||
description: 'The type of relationship.'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const { connectionType: RelationshipConnection } = connectionDefinitions({ nodeType: Relationship })
|
||||
export { RelationshipConnection }
|
||||
export default Relationship
|
||||
|
|
@ -4,10 +4,8 @@ import Area from './area'
|
|||
|
||||
export default new GraphQLObjectType({
|
||||
name: 'ReleaseEvent',
|
||||
description:
|
||||
'Date on which a release was released in a country/region with a ' +
|
||||
'particular label, catalog number, barcode, and what release format ' +
|
||||
'was used.',
|
||||
description: `Date on which a release was released in a country/region with a
|
||||
particular label, catalog number, barcode, and what release format was used.`,
|
||||
fields: () => ({
|
||||
area: { type: Area },
|
||||
date: { type: DateType }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql/type'
|
||||
import { GraphQLObjectType, GraphQLList } from 'graphql/type'
|
||||
import { connectionDefinitions } from 'graphql-relay'
|
||||
import Node from './node'
|
||||
import Entity from './entity'
|
||||
import { DateType } from './scalars'
|
||||
import { ReleaseGroupType } from './enums'
|
||||
import {
|
||||
id,
|
||||
mbid,
|
||||
|
|
@ -11,16 +12,21 @@ import {
|
|||
artistCredit,
|
||||
artists,
|
||||
releases,
|
||||
relations,
|
||||
relationships,
|
||||
getHyphenated,
|
||||
fieldWithID
|
||||
} from './helpers'
|
||||
|
||||
const ReleaseGroup = new GraphQLObjectType({
|
||||
name: 'ReleaseGroup',
|
||||
description:
|
||||
'Represents an abstract "album" (or "single", or "EP") entity. ' +
|
||||
'Technically it’s a group of releases, with a specified type.',
|
||||
description: `A [release group](https://musicbrainz.org/doc/Release_Group) is
|
||||
used to group several different releases into a single logical entity. Every
|
||||
release belongs to one, and only one release group.
|
||||
|
||||
Both release groups and releases are “albums” in a general sense, but with an
|
||||
important difference: a release is something you can buy as media such as a CD
|
||||
or a vinyl record, while a release group embraces the overall concept of an
|
||||
album – it doesn’t matter how many CDs or editions/versions it had.`,
|
||||
interfaces: () => [Node, Entity],
|
||||
fields: () => ({
|
||||
id,
|
||||
|
|
@ -28,12 +34,26 @@ const ReleaseGroup = new GraphQLObjectType({
|
|||
title,
|
||||
disambiguation,
|
||||
artistCredit,
|
||||
firstReleaseDate: { type: DateType, resolve: getHyphenated },
|
||||
...fieldWithID('primaryType'),
|
||||
...fieldWithID('secondaryTypes', { type: new GraphQLList(GraphQLString) }),
|
||||
firstReleaseDate: {
|
||||
type: DateType,
|
||||
description: 'The date of the earliest release in the group.',
|
||||
resolve: getHyphenated
|
||||
},
|
||||
...fieldWithID('primaryType', {
|
||||
type: ReleaseGroupType,
|
||||
description: `The [type](https://musicbrainz.org/doc/Release_Group/Type)
|
||||
of a release group describes what kind of releases the release group represents,
|
||||
e.g. album, single, soundtrack, compilation, etc. A release group can have a
|
||||
“main” type and an unspecified number of additional types.`
|
||||
}),
|
||||
...fieldWithID('secondaryTypes', {
|
||||
type: new GraphQLList(ReleaseGroupType),
|
||||
description: `Additional [types](https://musicbrainz.org/doc/Release_Group/Type)
|
||||
that apply to this release group.`
|
||||
}),
|
||||
artists,
|
||||
releases,
|
||||
relations
|
||||
relationships
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { connectionDefinitions } from 'graphql-relay'
|
|||
import Node from './node'
|
||||
import Entity from './entity'
|
||||
import { DateType } from './scalars'
|
||||
import { ReleaseStatus } from './enums'
|
||||
import ReleaseEvent from './release-event'
|
||||
import {
|
||||
id,
|
||||
|
|
@ -14,17 +15,18 @@ import {
|
|||
labels,
|
||||
recordings,
|
||||
releaseGroups,
|
||||
relations,
|
||||
relationships,
|
||||
getHyphenated,
|
||||
fieldWithID
|
||||
} from './helpers'
|
||||
|
||||
const Release = new GraphQLObjectType({
|
||||
name: 'Release',
|
||||
description:
|
||||
'Real-world release object you can buy in your music store. It has ' +
|
||||
'release date and country, list of catalog number and label pairs, ' +
|
||||
'packaging type and release status.',
|
||||
description: `A [release](https://musicbrainz.org/doc/Release) represents the
|
||||
unique release (i.e. issuing) of a product on a specific date with specific
|
||||
release information such as the country, label, barcode, packaging, etc. If you
|
||||
walk into a store and purchase an album or single, they’re each represented in
|
||||
MusicBrainz as one release.`,
|
||||
interfaces: () => [Node, Entity],
|
||||
fields: () => ({
|
||||
id,
|
||||
|
|
@ -34,19 +36,46 @@ const Release = new GraphQLObjectType({
|
|||
artistCredit,
|
||||
releaseEvents: {
|
||||
type: new GraphQLList(ReleaseEvent),
|
||||
description: 'The release events for this release.',
|
||||
resolve: getHyphenated
|
||||
},
|
||||
date: { type: DateType },
|
||||
country: { type: GraphQLString },
|
||||
barcode: { type: GraphQLString },
|
||||
...fieldWithID('status'),
|
||||
...fieldWithID('packaging'),
|
||||
quality: { type: GraphQLString },
|
||||
date: {
|
||||
type: DateType,
|
||||
description: `The [release date](https://musicbrainz.org/doc/Release/Date)
|
||||
is the date in which a release was made available through some sort of
|
||||
distribution mechanism.`
|
||||
},
|
||||
country: {
|
||||
type: GraphQLString,
|
||||
description: 'The country in which the release was issued.'
|
||||
},
|
||||
barcode: {
|
||||
type: GraphQLString,
|
||||
description: `The [barcode](https://en.wikipedia.org/wiki/Barcode), if the
|
||||
release has one. The most common types found on releases are 12-digit
|
||||
[UPCs](https://en.wikipedia.org/wiki/Universal_Product_Code) and 13-digit
|
||||
[EANs](https://en.wikipedia.org/wiki/International_Article_Number).`
|
||||
},
|
||||
...fieldWithID('status', {
|
||||
type: ReleaseStatus,
|
||||
description: 'The status describes how “official” a release is.'
|
||||
}),
|
||||
...fieldWithID('packaging', {
|
||||
description: `The physical packaging that accompanies the release. See
|
||||
the [list of packaging](https://musicbrainz.org/doc/Release/Packaging) for more
|
||||
information.`
|
||||
}),
|
||||
quality: {
|
||||
type: GraphQLString,
|
||||
description: `Data quality indicates how good the data for a release is.
|
||||
It is not a mark of how good or bad the music itself is – for that, use
|
||||
[ratings](https://musicbrainz.org/doc/Rating_System).`
|
||||
},
|
||||
artists,
|
||||
labels,
|
||||
recordings,
|
||||
releaseGroups,
|
||||
relations
|
||||
relationships
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -17,9 +17,6 @@ function validatePositive (value) {
|
|||
throw new TypeError(`Expected positive value: ${value}`)
|
||||
}
|
||||
|
||||
/*
|
||||
scalar Date
|
||||
*/
|
||||
export const DateType = new GraphQLScalarType({
|
||||
name: 'Date',
|
||||
description:
|
||||
|
|
@ -34,9 +31,6 @@ export const DateType = new GraphQLScalarType({
|
|||
}
|
||||
})
|
||||
|
||||
/*
|
||||
scalar Degrees
|
||||
*/
|
||||
export const Degrees = new GraphQLScalarType({
|
||||
name: 'Degrees',
|
||||
description: 'Decimal degrees, used for latitude and longitude.',
|
||||
|
|
@ -50,9 +44,6 @@ export const Degrees = new GraphQLScalarType({
|
|||
}
|
||||
})
|
||||
|
||||
/*
|
||||
scalar Duration
|
||||
*/
|
||||
export const Duration = new GraphQLScalarType({
|
||||
name: 'Duration',
|
||||
description: 'A length of time, in milliseconds.',
|
||||
|
|
@ -66,14 +57,11 @@ export const Duration = new GraphQLScalarType({
|
|||
}
|
||||
})
|
||||
|
||||
/*
|
||||
scalar IPI
|
||||
*/
|
||||
export const IPI = new GraphQLScalarType({
|
||||
name: 'IPI',
|
||||
description:
|
||||
'An IPI (interested party information) code is an identifying number ' +
|
||||
'assigned by the CISAC database for musical rights management.',
|
||||
description: `An [IPI](https://musicbrainz.org/doc/IPI) (interested party
|
||||
information) code is an identifying number assigned by the CISAC database for
|
||||
musical rights management.`,
|
||||
serialize: value => value,
|
||||
parseValue: value => value,
|
||||
parseLiteral (ast) {
|
||||
|
|
@ -84,15 +72,11 @@ export const IPI = new GraphQLScalarType({
|
|||
}
|
||||
})
|
||||
|
||||
/*
|
||||
scalar ISNI
|
||||
*/
|
||||
export const ISNI = new GraphQLScalarType({
|
||||
name: 'ISNI',
|
||||
description:
|
||||
'The International Standard Name Identifier (ISNI) is an ISO standard ' +
|
||||
'for uniquely identifying the public identities of contributors to ' +
|
||||
'media content.',
|
||||
description: `The [International Standard Name Identifier](https://musicbrainz.org/doc/ISNI)
|
||||
(ISNI) is an ISO standard for uniquely identifying the public identities of
|
||||
contributors to media content.`,
|
||||
serialize: value => value,
|
||||
parseValue: value => value,
|
||||
parseLiteral (ast) {
|
||||
|
|
@ -103,14 +87,11 @@ export const ISNI = new GraphQLScalarType({
|
|||
}
|
||||
})
|
||||
|
||||
/*
|
||||
scalar ISWC
|
||||
*/
|
||||
export const ISWC = new GraphQLScalarType({
|
||||
name: 'ISWC',
|
||||
description:
|
||||
'The International Standard Musical Work Code (ISWC) is an ISO standard ' +
|
||||
'similar to ISBNs for identifying musical works / compositions.',
|
||||
description: `The [International Standard Musical Work Code](https://musicbrainz.org/doc/ISWC)
|
||||
(ISWC) is an ISO standard similar to ISBNs for identifying musical works /
|
||||
compositions.`,
|
||||
serialize: value => value,
|
||||
parseValue: value => value,
|
||||
parseLiteral (ast) {
|
||||
|
|
@ -121,9 +102,6 @@ export const ISWC = new GraphQLScalarType({
|
|||
}
|
||||
})
|
||||
|
||||
/*
|
||||
scalar Locale
|
||||
*/
|
||||
export const Locale = new GraphQLScalarType({
|
||||
name: 'Locale',
|
||||
description: 'Language code, optionally with country and encoding.',
|
||||
|
|
@ -137,9 +115,6 @@ export const Locale = new GraphQLScalarType({
|
|||
}
|
||||
})
|
||||
|
||||
/*
|
||||
scalar Time
|
||||
*/
|
||||
export const Time = new GraphQLScalarType({
|
||||
name: 'Time',
|
||||
description: 'A time of day, in 24-hour hh:mm notation.',
|
||||
|
|
@ -153,12 +128,9 @@ export const Time = new GraphQLScalarType({
|
|||
}
|
||||
})
|
||||
|
||||
/*
|
||||
scalar URLString
|
||||
*/
|
||||
export const URLString = new GraphQLScalarType({
|
||||
name: 'URLString',
|
||||
description: 'Description',
|
||||
description: 'A web address.',
|
||||
serialize: value => value,
|
||||
parseValue: value => value,
|
||||
parseLiteral (ast) {
|
||||
|
|
@ -169,14 +141,10 @@ export const URLString = new GraphQLScalarType({
|
|||
}
|
||||
})
|
||||
|
||||
/*
|
||||
scalar MBID
|
||||
*/
|
||||
export const MBID = new GraphQLScalarType({
|
||||
name: 'MBID',
|
||||
description:
|
||||
'The `MBID` scalar represents MusicBrainz identifiers, which are ' +
|
||||
'36-character UUIDs.',
|
||||
description: `The MBID scalar represents MusicBrainz identifiers, which are
|
||||
36-character UUIDs.`,
|
||||
serialize: validateMBID,
|
||||
parseValue: validateMBID,
|
||||
parseLiteral (ast) {
|
||||
|
|
|
|||
|
|
@ -2,26 +2,23 @@ import { GraphQLObjectType } from 'graphql/type'
|
|||
import { connectionDefinitions } from 'graphql-relay'
|
||||
import Node from './node'
|
||||
import Entity from './entity'
|
||||
import {
|
||||
id,
|
||||
mbid,
|
||||
name,
|
||||
disambiguation,
|
||||
fieldWithID
|
||||
} from './helpers'
|
||||
import { id, mbid, name, disambiguation, fieldWithID } from './helpers'
|
||||
|
||||
const Series = new GraphQLObjectType({
|
||||
name: 'Series',
|
||||
description:
|
||||
'A series is a sequence of separate release groups, releases, ' +
|
||||
'recordings, works or events with a common theme.',
|
||||
description: `A [series](https://musicbrainz.org/doc/Series) is a sequence of
|
||||
separate release groups, releases, recordings, works or events with a common
|
||||
theme.`,
|
||||
interfaces: () => [Node, Entity],
|
||||
fields: () => ({
|
||||
id,
|
||||
mbid,
|
||||
name,
|
||||
disambiguation,
|
||||
...fieldWithID('type')
|
||||
...fieldWithID('type', {
|
||||
description: `The type primarily describes what type of entity the series
|
||||
contains.`
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -3,20 +3,22 @@ import { connectionDefinitions } from 'graphql-relay'
|
|||
import Node from './node'
|
||||
import Entity from './entity'
|
||||
import { URLString } from './scalars'
|
||||
import { id, mbid, relations } from './helpers'
|
||||
import { id, mbid, relationships } from './helpers'
|
||||
|
||||
const URL = new GraphQLObjectType({
|
||||
name: 'URL',
|
||||
description:
|
||||
'A URL pointing to a resource external to MusicBrainz, i.e. an official ' +
|
||||
'homepage, a site where music can be acquired, an entry in another ' +
|
||||
'database, etc.',
|
||||
description: `A [URL](https://musicbrainz.org/doc/URL) pointing to a resource
|
||||
external to MusicBrainz, i.e. an official homepage, a site where music can be
|
||||
acquired, an entry in another database, etc.`,
|
||||
interfaces: () => [Node, Entity],
|
||||
fields: () => ({
|
||||
id,
|
||||
mbid,
|
||||
resource: { type: new GraphQLNonNull(URLString) },
|
||||
relations
|
||||
resource: {
|
||||
type: new GraphQLNonNull(URLString),
|
||||
description: 'The actual URL string.'
|
||||
},
|
||||
relationships
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -8,26 +8,35 @@ import {
|
|||
title,
|
||||
disambiguation,
|
||||
artists,
|
||||
relations,
|
||||
relationships,
|
||||
fieldWithID
|
||||
} from './helpers'
|
||||
|
||||
const Work = new GraphQLObjectType({
|
||||
name: 'Work',
|
||||
description:
|
||||
'A distinct intellectual or artistic creation, which can be expressed in ' +
|
||||
'the form of one or more audio recordings',
|
||||
description: `A [work](https://musicbrainz.org/doc/Work) is a distinct
|
||||
intellectual or artistic creation, which can be expressed in the form of one or
|
||||
more audio recordings.`,
|
||||
interfaces: () => [Node, Entity],
|
||||
fields: () => ({
|
||||
id,
|
||||
mbid,
|
||||
title,
|
||||
disambiguation,
|
||||
iswcs: { type: new GraphQLList(GraphQLString) },
|
||||
language: { type: GraphQLString },
|
||||
...fieldWithID('type'),
|
||||
iswcs: {
|
||||
type: new GraphQLList(GraphQLString),
|
||||
description: `A list of [ISWCs](https://musicbrainz.org/doc/ISWC) assigned
|
||||
to the work by copyright collecting agencies.`
|
||||
},
|
||||
language: {
|
||||
type: GraphQLString,
|
||||
description: 'The language in which the work was originally written.'
|
||||
},
|
||||
...fieldWithID('type', {
|
||||
description: 'The type of work.'
|
||||
}),
|
||||
artists,
|
||||
relations
|
||||
relationships
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import util from 'util'
|
|||
|
||||
export function getFields (info) {
|
||||
if (info.kind !== 'Field') {
|
||||
info = info.fieldASTs[0]
|
||||
info = info.fieldNodes[0]
|
||||
}
|
||||
const selections = info.selectionSet.selections
|
||||
return selections.reduce((fields, selection) => {
|
||||
|
|
|
|||
|
|
@ -8,4 +8,11 @@ describe('RateLimit', () => {
|
|||
expect(limiter.limit).to.equal(1)
|
||||
expect(limiter.period).to.equal(1000)
|
||||
})
|
||||
|
||||
it('concurrency defaults to limit', () => {
|
||||
let limiter = new RateLimit()
|
||||
expect(limiter.concurrency).to.equal(1)
|
||||
limiter = new RateLimit({ limit: 5 })
|
||||
expect(limiter.concurrency).to.equal(5)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue