mirror of
https://github.com/BradNut/graphbrainz
synced 2025-09-08 17:40:32 +00:00
Add browse and search queries
This commit is contained in:
parent
92801da402
commit
9adf218ebe
40 changed files with 2064 additions and 351 deletions
1
Procfile
Normal file
1
Procfile
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
web: npm start
|
||||||
488
README.md
488
README.md
|
|
@ -1,2 +1,490 @@
|
||||||
# graphbrainz
|
# graphbrainz
|
||||||
|
|
||||||
GraphQL server for the MusicBrainz API
|
GraphQL server for the MusicBrainz API
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
```graphql
|
||||||
|
|
||||||
|
schema {
|
||||||
|
query: RootQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
type Alias {
|
||||||
|
name: String
|
||||||
|
sortName: String
|
||||||
|
locale: String
|
||||||
|
primary: Boolean
|
||||||
|
type: String
|
||||||
|
typeID: MBID
|
||||||
|
}
|
||||||
|
|
||||||
|
type Area implements Entity {
|
||||||
|
id: MBID!
|
||||||
|
name: String
|
||||||
|
sortName: String
|
||||||
|
disambiguation: String
|
||||||
|
isoCodes: [String]
|
||||||
|
artists(limit: Int, offset: Int): [Artist]
|
||||||
|
events(limit: Int, offset: Int): [Event]
|
||||||
|
labels(limit: Int, offset: Int): [Label]
|
||||||
|
places(limit: Int, offset: Int): [Place]
|
||||||
|
releases(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
type: ReleaseGroupType,
|
||||||
|
types: [ReleaseGroupType],
|
||||||
|
status: ReleaseStatus,
|
||||||
|
statuses: [ReleaseStatus]): [Release]
|
||||||
|
}
|
||||||
|
|
||||||
|
type AreaPage {
|
||||||
|
count: Int!
|
||||||
|
offset: Int!
|
||||||
|
created: Date
|
||||||
|
results: [Area]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Artist implements Entity {
|
||||||
|
id: MBID!
|
||||||
|
name: String
|
||||||
|
sortName: String
|
||||||
|
disambiguation: String
|
||||||
|
aliases: [Alias]
|
||||||
|
country: String
|
||||||
|
area: Area
|
||||||
|
beginArea: Area
|
||||||
|
endArea: Area
|
||||||
|
lifeSpan: LifeSpan
|
||||||
|
gender: String
|
||||||
|
genderID: MBID
|
||||||
|
type: String
|
||||||
|
typeID: MBID
|
||||||
|
recordings(limit: Int, offset: Int): [Recording]
|
||||||
|
releases(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
type: ReleaseGroupType,
|
||||||
|
types: [ReleaseGroupType],
|
||||||
|
status: ReleaseStatus,
|
||||||
|
statuses: [ReleaseStatus]): [Release]
|
||||||
|
releaseGroups(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
type: ReleaseGroupType,
|
||||||
|
types: [ReleaseGroupType]): [ReleaseGroup]
|
||||||
|
works(limit: Int, offset: Int): [Work]
|
||||||
|
relations: Relations
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArtistCredit {
|
||||||
|
artist: Artist
|
||||||
|
name: String
|
||||||
|
joinPhrase: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArtistPage {
|
||||||
|
count: Int!
|
||||||
|
offset: Int!
|
||||||
|
created: Date
|
||||||
|
results: [Artist]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type BrowseQuery {
|
||||||
|
artists(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
area: MBID,
|
||||||
|
recording: MBID,
|
||||||
|
release: MBID,
|
||||||
|
releaseGroup: MBID,
|
||||||
|
work: MBID): ArtistPage
|
||||||
|
events(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
area: MBID,
|
||||||
|
artist: MBID,
|
||||||
|
place: MBID): EventPage
|
||||||
|
labels(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
area: MBID,
|
||||||
|
release: MBID): LabelPage
|
||||||
|
places(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
area: MBID): PlacePage
|
||||||
|
recordings(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
artist: MBID,
|
||||||
|
release: MBID): RecordingPage
|
||||||
|
releases(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
area: MBID,
|
||||||
|
artist: MBID,
|
||||||
|
label: MBID,
|
||||||
|
track: MBID,
|
||||||
|
trackArtist: MBID,
|
||||||
|
recording: MBID,
|
||||||
|
releaseGroup: MBID): ReleasePage
|
||||||
|
releaseGroups(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
artist: MBID,
|
||||||
|
release: MBID): ReleaseGroupPage
|
||||||
|
works(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
artist: MBID): WorkPage
|
||||||
|
urls(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
resource: URLString): URLPage
|
||||||
|
}
|
||||||
|
|
||||||
|
type Coordinates {
|
||||||
|
latitude: Degrees
|
||||||
|
longitude: Degrees
|
||||||
|
}
|
||||||
|
|
||||||
|
scalar Date
|
||||||
|
|
||||||
|
scalar Degrees
|
||||||
|
|
||||||
|
interface Entity {
|
||||||
|
id: MBID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Event implements Entity {
|
||||||
|
id: MBID!
|
||||||
|
name: String
|
||||||
|
disambiguation: String
|
||||||
|
lifeSpan: LifeSpan
|
||||||
|
time: Time
|
||||||
|
cancelled: Boolean
|
||||||
|
setlist: String
|
||||||
|
type: String
|
||||||
|
typeID: MBID
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventPage {
|
||||||
|
count: Int!
|
||||||
|
offset: Int!
|
||||||
|
created: Date
|
||||||
|
results: [Event]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Instrument implements Entity {
|
||||||
|
id: MBID!
|
||||||
|
name: String
|
||||||
|
disambiguation: String
|
||||||
|
description: String
|
||||||
|
type: String
|
||||||
|
typeID: MBID
|
||||||
|
}
|
||||||
|
|
||||||
|
scalar IPI
|
||||||
|
|
||||||
|
type Label implements Entity {
|
||||||
|
id: MBID!
|
||||||
|
name: String
|
||||||
|
sortName: String
|
||||||
|
disambiguation: String
|
||||||
|
country: String
|
||||||
|
area: Area
|
||||||
|
lifeSpan: LifeSpan
|
||||||
|
labelCode: Int
|
||||||
|
ipis: [IPI]
|
||||||
|
type: String
|
||||||
|
typeID: MBID
|
||||||
|
releases(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
type: ReleaseGroupType,
|
||||||
|
types: [ReleaseGroupType],
|
||||||
|
status: ReleaseStatus,
|
||||||
|
statuses: [ReleaseStatus]): [Release]
|
||||||
|
}
|
||||||
|
|
||||||
|
type LabelPage {
|
||||||
|
count: Int!
|
||||||
|
offset: Int!
|
||||||
|
created: Date
|
||||||
|
results: [Label]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type LifeSpan {
|
||||||
|
begin: Date
|
||||||
|
end: Date
|
||||||
|
ended: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type LookupQuery {
|
||||||
|
area(id: MBID!): Area
|
||||||
|
artist(id: MBID!): Artist
|
||||||
|
event(id: MBID!): Event
|
||||||
|
instrument(id: MBID!): Instrument
|
||||||
|
label(id: MBID!): Label
|
||||||
|
place(id: MBID!): Place
|
||||||
|
recording(id: MBID!): Recording
|
||||||
|
release(id: MBID!): Release
|
||||||
|
releaseGroup(id: MBID!): ReleaseGroup
|
||||||
|
url(id: MBID!): URL
|
||||||
|
work(id: MBID!): Work
|
||||||
|
}
|
||||||
|
|
||||||
|
scalar MBID
|
||||||
|
|
||||||
|
type Place implements Entity {
|
||||||
|
id: MBID!
|
||||||
|
name: String
|
||||||
|
disambiguation: String
|
||||||
|
address: String
|
||||||
|
area: Area
|
||||||
|
coordinates: Coordinates
|
||||||
|
lifeSpan: LifeSpan
|
||||||
|
type: String
|
||||||
|
typeID: MBID
|
||||||
|
events(limit: Int, offset: Int): [Event]
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlacePage {
|
||||||
|
count: Int!
|
||||||
|
offset: Int!
|
||||||
|
created: Date
|
||||||
|
results: [Place]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Recording implements Entity {
|
||||||
|
id: MBID!
|
||||||
|
title: String
|
||||||
|
disambiguation: String
|
||||||
|
artistCredit: [ArtistCredit]
|
||||||
|
length: Int
|
||||||
|
video: Boolean
|
||||||
|
artists(limit: Int, offset: Int): [Artist]
|
||||||
|
releases(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
type: ReleaseGroupType,
|
||||||
|
types: [ReleaseGroupType],
|
||||||
|
status: ReleaseStatus,
|
||||||
|
statuses: [ReleaseStatus]): [Release]
|
||||||
|
relations: Relations
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordingPage {
|
||||||
|
count: Int!
|
||||||
|
offset: Int!
|
||||||
|
created: Date
|
||||||
|
results: [Recording]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Relation {
|
||||||
|
target: Entity!
|
||||||
|
direction: String!
|
||||||
|
targetType: String!
|
||||||
|
sourceCredit: String
|
||||||
|
targetCredit: String
|
||||||
|
begin: Date
|
||||||
|
end: Date
|
||||||
|
ended: Boolean
|
||||||
|
attributes: [String]
|
||||||
|
type: String
|
||||||
|
typeID: MBID
|
||||||
|
}
|
||||||
|
|
||||||
|
type Relations {
|
||||||
|
area(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
direction: String,
|
||||||
|
type: String,
|
||||||
|
typeID: MBID): [Relation]
|
||||||
|
artist(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
direction: String,
|
||||||
|
type: String,
|
||||||
|
typeID: MBID): [Relation]
|
||||||
|
event(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
direction: String,
|
||||||
|
type: String,
|
||||||
|
typeID: MBID): [Relation]
|
||||||
|
instrument(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
direction: String,
|
||||||
|
type: String,
|
||||||
|
typeID: MBID): [Relation]
|
||||||
|
label(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
direction: String,
|
||||||
|
type: String,
|
||||||
|
typeID: MBID): [Relation]
|
||||||
|
place(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
direction: String,
|
||||||
|
type: String,
|
||||||
|
typeID: MBID): [Relation]
|
||||||
|
recording(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
direction: String,
|
||||||
|
type: String,
|
||||||
|
typeID: MBID): [Relation]
|
||||||
|
release(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
direction: String,
|
||||||
|
type: String,
|
||||||
|
typeID: MBID): [Relation]
|
||||||
|
releaseGroup(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
direction: String,
|
||||||
|
type: String,
|
||||||
|
typeID: MBID): [Relation]
|
||||||
|
series(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
direction: String,
|
||||||
|
type: String,
|
||||||
|
typeID: MBID): [Relation]
|
||||||
|
url(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
direction: String,
|
||||||
|
type: String,
|
||||||
|
typeID: MBID): [Relation]
|
||||||
|
work(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
direction: String,
|
||||||
|
type: String,
|
||||||
|
typeID: MBID): [Relation]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Release implements Entity {
|
||||||
|
id: MBID!
|
||||||
|
title: String
|
||||||
|
disambiguation: String
|
||||||
|
artistCredit: [ArtistCredit]
|
||||||
|
releaseEvents: [ReleaseEvent]
|
||||||
|
date: Date
|
||||||
|
country: String
|
||||||
|
barcode: String
|
||||||
|
status: String
|
||||||
|
statusID: MBID
|
||||||
|
packaging: String
|
||||||
|
packagingID: MBID
|
||||||
|
quality: String
|
||||||
|
artists(limit: Int, offset: Int): [Artist]
|
||||||
|
labels(limit: Int, offset: Int): [Label]
|
||||||
|
recordings(limit: Int, offset: Int): [Recording]
|
||||||
|
releaseGroups(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
type: ReleaseGroupType,
|
||||||
|
types: [ReleaseGroupType]): [ReleaseGroup]
|
||||||
|
relations: Relations
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReleaseEvent {
|
||||||
|
area: Area
|
||||||
|
date: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReleaseGroup implements Entity {
|
||||||
|
id: MBID!
|
||||||
|
title: String
|
||||||
|
disambiguation: String
|
||||||
|
artistCredit: [ArtistCredit]
|
||||||
|
firstReleaseDate: Date
|
||||||
|
primaryType: String
|
||||||
|
primaryTypeID: MBID
|
||||||
|
secondaryTypes: [String]
|
||||||
|
secondaryTypeIDs: [MBID]
|
||||||
|
artists(limit: Int, offset: Int): [Artist]
|
||||||
|
releases(limit: Int,
|
||||||
|
offset: Int,
|
||||||
|
type: ReleaseGroupType,
|
||||||
|
types: [ReleaseGroupType],
|
||||||
|
status: ReleaseStatus,
|
||||||
|
statuses: [ReleaseStatus]): [Release]
|
||||||
|
relations: Relations
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReleaseGroupPage {
|
||||||
|
count: Int!
|
||||||
|
offset: Int!
|
||||||
|
created: Date
|
||||||
|
results: [ReleaseGroup]!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ReleaseGroupType {
|
||||||
|
ALBUM
|
||||||
|
SINGLE
|
||||||
|
EP
|
||||||
|
OTHER
|
||||||
|
BROADCAST
|
||||||
|
COMPILATION
|
||||||
|
SOUNDTRACK
|
||||||
|
SPOKENWORD
|
||||||
|
INTERVIEW
|
||||||
|
AUDIOBOOK
|
||||||
|
LIVE
|
||||||
|
REMIX
|
||||||
|
DJMIX
|
||||||
|
MIXTAPE
|
||||||
|
DEMO
|
||||||
|
NAT
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReleasePage {
|
||||||
|
count: Int!
|
||||||
|
offset: Int!
|
||||||
|
created: Date
|
||||||
|
results: [Release]!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ReleaseStatus {
|
||||||
|
OFFICIAL
|
||||||
|
PROMOTION
|
||||||
|
BOOTLEG
|
||||||
|
PSEUDORELEASE
|
||||||
|
}
|
||||||
|
|
||||||
|
type RootQuery {
|
||||||
|
lookup: LookupQuery
|
||||||
|
browse: BrowseQuery
|
||||||
|
search: SearchQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchQuery {
|
||||||
|
areas(query: String!, limit: Int, offset: Int): AreaPage
|
||||||
|
artists(query: String!, limit: Int, offset: Int): ArtistPage
|
||||||
|
labels(query: String!, limit: Int, offset: Int): LabelPage
|
||||||
|
places(query: String!, limit: Int, offset: Int): PlacePage
|
||||||
|
recordings(query: String!, limit: Int, offset: Int): RecordingPage
|
||||||
|
releases(query: String!, limit: Int, offset: Int): ReleasePage
|
||||||
|
releaseGroups(query: String!, limit: Int, offset: Int): ReleaseGroupPage
|
||||||
|
works(query: String!, limit: Int, offset: Int): WorkPage
|
||||||
|
}
|
||||||
|
|
||||||
|
scalar Time
|
||||||
|
|
||||||
|
type URL implements Entity {
|
||||||
|
id: MBID!
|
||||||
|
resource: URLString!
|
||||||
|
relations: Relations
|
||||||
|
}
|
||||||
|
|
||||||
|
type URLPage {
|
||||||
|
count: Int!
|
||||||
|
offset: Int!
|
||||||
|
created: Date
|
||||||
|
results: [URL]!
|
||||||
|
}
|
||||||
|
|
||||||
|
scalar URLString
|
||||||
|
|
||||||
|
type Work implements Entity {
|
||||||
|
id: MBID!
|
||||||
|
title: String
|
||||||
|
disambiguation: String
|
||||||
|
iswcs: [String]
|
||||||
|
language: String
|
||||||
|
type: String
|
||||||
|
typeID: MBID
|
||||||
|
artists(limit: Int, offset: Int): [Artist]
|
||||||
|
relations: Relations
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkPage {
|
||||||
|
count: Int!
|
||||||
|
offset: Int!
|
||||||
|
created: Date
|
||||||
|
results: [Work]!
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
|
||||||
12
package.json
12
package.json
|
|
@ -2,15 +2,21 @@
|
||||||
"name": "graphbrainz",
|
"name": "graphbrainz",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "lib/index.js",
|
||||||
|
"engines": {
|
||||||
|
"node": "^4.3.0",
|
||||||
|
"npm": "^3.10.5"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build:lib",
|
"build": "npm run build:lib",
|
||||||
"build:lib": "babel --out-dir lib src",
|
"build:lib": "babel --out-dir lib src",
|
||||||
"clean": "npm run clean:lib",
|
"clean": "npm run clean:lib",
|
||||||
"clean:lib": "rm -rf lib",
|
"clean:lib": "rm -rf lib",
|
||||||
"check": "npm run lint && npm run test",
|
"check": "npm run lint && npm run test",
|
||||||
|
"deploy": "./scripts/deploy.sh",
|
||||||
"lint": "standard --verbose | snazzy",
|
"lint": "standard --verbose | snazzy",
|
||||||
"prepublish": "npm run clean && npm run check && npm run build",
|
"prepublish": "npm run clean && npm run check && npm run build",
|
||||||
|
"print-schema": "babel-node scripts/print-schema.js",
|
||||||
"start": "node lib/index.js",
|
"start": "node lib/index.js",
|
||||||
"start:dev": "nodemon --exec babel-node src/index.js",
|
"start:dev": "nodemon --exec babel-node src/index.js",
|
||||||
"test": "mocha --compilers js:babel-register"
|
"test": "mocha --compilers js:babel-register"
|
||||||
|
|
@ -28,7 +34,6 @@
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bottleneck": "^1.12.0",
|
|
||||||
"chalk": "^1.1.3",
|
"chalk": "^1.1.3",
|
||||||
"dashify": "^0.2.2",
|
"dashify": "^0.2.2",
|
||||||
"dataloader": "^1.2.0",
|
"dataloader": "^1.2.0",
|
||||||
|
|
@ -37,6 +42,7 @@
|
||||||
"express-graphql": "^0.5.3",
|
"express-graphql": "^0.5.3",
|
||||||
"graphql": "^0.6.2",
|
"graphql": "^0.6.2",
|
||||||
"qs": "^6.2.1",
|
"qs": "^6.2.1",
|
||||||
|
"request": "^2.74.0",
|
||||||
"retry": "^0.9.0"
|
"retry": "^0.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -47,7 +53,7 @@
|
||||||
"babel-register": "^6.11.6",
|
"babel-register": "^6.11.6",
|
||||||
"chai": "^3.5.0",
|
"chai": "^3.5.0",
|
||||||
"mocha": "^3.0.1",
|
"mocha": "^3.0.1",
|
||||||
"nodemon": "^1.10.0",
|
"nodemon": "^1.10.2",
|
||||||
"snazzy": "^4.0.1",
|
"snazzy": "^4.0.1",
|
||||||
"standard": "^7.1.2"
|
"standard": "^7.1.2"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
22
scripts/deploy.sh
Executable file
22
scripts/deploy.sh
Executable file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RESET='\033[0m'
|
||||||
|
|
||||||
|
# Fail if the `heroku` remote isn't there.
|
||||||
|
git remote show heroku
|
||||||
|
|
||||||
|
git stash # Stash uncommitted changes.
|
||||||
|
git checkout -B deploy # Force branch creation/reset.
|
||||||
|
npm run build
|
||||||
|
git add -f lib # Force add ignored files.
|
||||||
|
# Use the same commit message, but add a little note.
|
||||||
|
git commit -m "$(git log -1 --pretty=%B) [deploy branch: do NOT push to GitHub]"
|
||||||
|
git push -f heroku deploy:master
|
||||||
|
git rm -r --cached lib # Otherwise switching branches will remove them.
|
||||||
|
git checkout - # Switch back to whatever branch we came from.
|
||||||
|
git branch -D deploy # Just to prevent someone accidentally pushing to GitHub.
|
||||||
|
git stash pop --index || true # Restore uncommitted changes, OK if none.
|
||||||
|
|
||||||
|
echo -e "\n${GREEN}✔︎ Successfully deployed.${RESET}"
|
||||||
4
scripts/print-schema.js
Normal file
4
scripts/print-schema.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { printSchema } from 'graphql'
|
||||||
|
import schema from '../src/schema'
|
||||||
|
|
||||||
|
console.log(printSchema(schema))
|
||||||
|
|
@ -6,6 +6,9 @@ import ExtendableError from 'es6-error'
|
||||||
import RateLimit from './rate-limit'
|
import RateLimit from './rate-limit'
|
||||||
import pkg from '../package.json'
|
import pkg from '../package.json'
|
||||||
|
|
||||||
|
// 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.
|
||||||
const RETRY_CODES = {
|
const RETRY_CODES = {
|
||||||
ECONNRESET: true,
|
ECONNRESET: true,
|
||||||
ENOTFOUND: true,
|
ENOTFOUND: true,
|
||||||
|
|
@ -31,12 +34,23 @@ export default class MusicBrainz {
|
||||||
userAgent: `${pkg.name}/${pkg.version} ` +
|
userAgent: `${pkg.name}/${pkg.version} ` +
|
||||||
`( ${pkg.homepage || pkg.author.url || pkg.author.email} )`,
|
`( ${pkg.homepage || pkg.author.url || pkg.author.email} )`,
|
||||||
timeout: 60000,
|
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: 3,
|
limit: 3,
|
||||||
limitPeriod: 3000,
|
limitPeriod: 3000,
|
||||||
maxConcurrency: 10,
|
concurrency: 10,
|
||||||
retries: 10,
|
retries: 10,
|
||||||
minRetryDelay: 100,
|
// It's OK for `retryDelayMin` to be less than one second, even 0, because
|
||||||
maxRetryDelay: 60000,
|
// `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,
|
randomizeRetry: true,
|
||||||
...options
|
...options
|
||||||
}
|
}
|
||||||
|
|
@ -46,20 +60,21 @@ export default class MusicBrainz {
|
||||||
this.limiter = new RateLimit({
|
this.limiter = new RateLimit({
|
||||||
limit: options.limit,
|
limit: options.limit,
|
||||||
period: options.limitPeriod,
|
period: options.limitPeriod,
|
||||||
maxConcurrency: options.maxConcurrency
|
concurrency: options.concurrency
|
||||||
})
|
})
|
||||||
// Even though `minTimeout` is lower than one second, the `Limiter` is
|
|
||||||
// making sure we don't exceed the API rate limit anyway. So we're not doing
|
|
||||||
// exponential backoff to wait for the rate limit to subside, but rather
|
|
||||||
// to be kind to MusicBrainz in case some other error occurred.
|
|
||||||
this.retryOptions = {
|
this.retryOptions = {
|
||||||
retries: options.retries,
|
retries: options.retries,
|
||||||
minTimeout: options.minRetryTimeout,
|
minTimeout: options.retryDelayMin,
|
||||||
maxTimeout: options.maxRetryDelay,
|
maxTimeout: options.retryDelayMax,
|
||||||
randomize: options.randomizeRetry
|
randomize: options.randomizeRetry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if we should retry the request based on the given error.
|
||||||
|
* Retry any 5XX response from MusicBrainz, as well as any error in
|
||||||
|
* `RETRY_CODES`.
|
||||||
|
*/
|
||||||
shouldRetry (err) {
|
shouldRetry (err) {
|
||||||
if (err instanceof MusicBrainzError) {
|
if (err instanceof MusicBrainzError) {
|
||||||
return err.statusCode >= 500 && err.statusCode < 600
|
return err.statusCode >= 500 && err.statusCode < 600
|
||||||
|
|
@ -67,19 +82,24 @@ export default class MusicBrainz {
|
||||||
return RETRY_CODES[err.code] || false
|
return RETRY_CODES[err.code] || false
|
||||||
}
|
}
|
||||||
|
|
||||||
_get (path, params) {
|
/**
|
||||||
|
* Send a request without any retrying or rate limiting.
|
||||||
|
* Use `get` instead.
|
||||||
|
*/
|
||||||
|
_get (path, params, info = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const options = {
|
const options = {
|
||||||
baseUrl: this.baseURL,
|
baseUrl: this.baseURL,
|
||||||
url: path,
|
url: path,
|
||||||
qs: params,
|
qs: { ...params, fmt: 'json' },
|
||||||
json: true,
|
json: true,
|
||||||
gzip: true,
|
gzip: true,
|
||||||
headers: { 'User-Agent': this.userAgent },
|
headers: { 'User-Agent': this.userAgent },
|
||||||
timeout: this.timeout
|
timeout: this.timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('GET:', path, params)
|
const attempt = `(attempt #${info.currentAttempt})`
|
||||||
|
console.log('GET:', path, info.currentAttempt > 1 ? attempt : '')
|
||||||
|
|
||||||
request(options, (err, response, body) => {
|
request(options, (err, response, body) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
@ -94,13 +114,18 @@ export default class MusicBrainz {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a request with retrying and rate limiting.
|
||||||
|
*/
|
||||||
get (path, params) {
|
get (path, params) {
|
||||||
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)
|
||||||
operation.attempt(currentAttempt => {
|
operation.attempt(currentAttempt => {
|
||||||
|
// This will increase the priority in our `RateLimit` queue for each
|
||||||
|
// retry, so that newer requests don't delay this one further.
|
||||||
const priority = currentAttempt
|
const priority = currentAttempt
|
||||||
this.limiter.enqueue(fn, [path, params], priority)
|
this.limiter.enqueue(fn, [path, params, { currentAttempt }], priority)
|
||||||
.then(resolve)
|
.then(resolve)
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (!this.shouldRetry(err) || !operation.retry(err)) {
|
if (!this.shouldRetry(err) || !operation.retry(err)) {
|
||||||
|
|
@ -111,28 +136,62 @@ export default class MusicBrainz {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getLookupURL (entity, id, params) {
|
stringifyParams (params) {
|
||||||
let url = `${entity}/${id}`
|
|
||||||
if (typeof params.inc === 'object') {
|
if (typeof params.inc === 'object') {
|
||||||
params = {
|
params = {
|
||||||
...params,
|
...params,
|
||||||
inc: params.inc.join('+')
|
inc: params.inc.join('+')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const query = qs.stringify(params, {
|
if (typeof params.type === 'object') {
|
||||||
|
params = {
|
||||||
|
...params,
|
||||||
|
type: params.type.join('|')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof params.status === 'object') {
|
||||||
|
params = {
|
||||||
|
...params,
|
||||||
|
status: params.status.join('|')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return qs.stringify(params, {
|
||||||
skipNulls: true,
|
skipNulls: true,
|
||||||
filter: (key, value) => value === '' ? undefined : value
|
filter: (key, value) => value === '' ? undefined : value
|
||||||
})
|
})
|
||||||
if (query) {
|
|
||||||
url += `?${query}`
|
|
||||||
}
|
}
|
||||||
return url
|
|
||||||
|
getURL (path, params) {
|
||||||
|
const query = params ? this.stringifyParams(params) : ''
|
||||||
|
return query ? `${path}?${query}` : path
|
||||||
|
}
|
||||||
|
|
||||||
|
getLookupURL (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)
|
return this.get(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBrowseURL (entity, params) {
|
||||||
|
return this.getURL(entity, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
browse (entity, params = {}) {
|
||||||
|
const url = this.getBrowseURL(entity, params)
|
||||||
|
return this.get(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearchURL (entity, query, params) {
|
||||||
|
return this.getURL(entity, { ...params, query })
|
||||||
|
}
|
||||||
|
|
||||||
|
search (entity, query, params = {}) {
|
||||||
|
const url = this.getSearchURL(entity, query, params)
|
||||||
|
return this.get(url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
10
src/index.js
10
src/index.js
|
|
@ -1,18 +1,24 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import graphqlHTTP from 'express-graphql'
|
import graphqlHTTP from 'express-graphql'
|
||||||
import schema from './schema'
|
import schema from './schema'
|
||||||
|
import { lookupLoader, browseLoader, searchLoader } from './loaders'
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
app.use('/graphql', graphqlHTTP({
|
app.use('/graphql', graphqlHTTP({
|
||||||
schema,
|
schema,
|
||||||
|
context: { lookupLoader, browseLoader, searchLoader },
|
||||||
|
pretty: true,
|
||||||
graphiql: true,
|
graphiql: true,
|
||||||
formatError: error => ({
|
formatError: error => ({
|
||||||
message: error.message,
|
message: error.message,
|
||||||
statusCode: error.statusCode,
|
|
||||||
locations: error.locations,
|
locations: error.locations,
|
||||||
stack: error.stack
|
stack: error.stack
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
app.listen(3001)
|
app.get('/graphiql', (req, res) => {
|
||||||
|
res.redirect(`/graph${req.url.slice(7)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.listen(process.env.PORT || 3001)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,48 @@
|
||||||
import DataLoader from 'dataloader'
|
import DataLoader from 'dataloader'
|
||||||
import MusicBrainz from './client'
|
import MusicBrainz from './api'
|
||||||
|
|
||||||
const CLIENT = new MusicBrainz()
|
const client = new MusicBrainz()
|
||||||
|
|
||||||
export const entityLoader = new DataLoader(keys => {
|
export const lookupLoader = new DataLoader(keys => {
|
||||||
return Promise.all(keys.map(key => {
|
return Promise.all(keys.map(key => {
|
||||||
const [ entity, id, params ] = key
|
const [ entityType, id, params ] = key
|
||||||
return CLIENT.lookup(entity, id, params)
|
return client.lookup(entityType, id, params).then(entity => {
|
||||||
|
if (entity) {
|
||||||
|
entity.entityType = entityType
|
||||||
|
}
|
||||||
|
return entity
|
||||||
|
})
|
||||||
}))
|
}))
|
||||||
}, {
|
}, {
|
||||||
cacheKeyFn: (key) => CLIENT.getLookupURL(...key)
|
cacheKeyFn: (key) => client.getLookupURL(...key)
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}, {
|
||||||
|
cacheKeyFn: (key) => client.getBrowseURL(...key)
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}, {
|
||||||
|
cacheKeyFn: (key) => client.getSearchURL(...key)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
120
src/queries/browse.js
Normal file
120
src/queries/browse.js
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { GraphQLObjectType, GraphQLInt } from 'graphql'
|
||||||
|
import {
|
||||||
|
MBID,
|
||||||
|
URLString,
|
||||||
|
ArtistPage,
|
||||||
|
EventPage,
|
||||||
|
LabelPage,
|
||||||
|
PlacePage,
|
||||||
|
RecordingPage,
|
||||||
|
ReleasePage,
|
||||||
|
ReleaseGroupPage,
|
||||||
|
URLPage,
|
||||||
|
WorkPage
|
||||||
|
} from '../types'
|
||||||
|
import { browseResolver } from '../resolvers'
|
||||||
|
|
||||||
|
export default new GraphQLObjectType({
|
||||||
|
name: 'BrowseQuery',
|
||||||
|
description:
|
||||||
|
'Browse requests are a direct lookup of all the entities directly linked ' +
|
||||||
|
'to another entity.',
|
||||||
|
fields: {
|
||||||
|
artists: {
|
||||||
|
type: ArtistPage,
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt },
|
||||||
|
area: { type: MBID },
|
||||||
|
recording: { type: MBID },
|
||||||
|
release: { type: MBID },
|
||||||
|
releaseGroup: { type: MBID },
|
||||||
|
work: { type: MBID }
|
||||||
|
},
|
||||||
|
resolve: browseResolver()
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
type: EventPage,
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt },
|
||||||
|
area: { type: MBID },
|
||||||
|
artist: { type: MBID },
|
||||||
|
place: { type: MBID }
|
||||||
|
},
|
||||||
|
resolve: browseResolver()
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
type: LabelPage,
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt },
|
||||||
|
area: { type: MBID },
|
||||||
|
release: { type: MBID }
|
||||||
|
},
|
||||||
|
resolve: browseResolver()
|
||||||
|
},
|
||||||
|
places: {
|
||||||
|
type: PlacePage,
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt },
|
||||||
|
area: { type: MBID }
|
||||||
|
},
|
||||||
|
resolve: browseResolver()
|
||||||
|
},
|
||||||
|
recordings: {
|
||||||
|
type: RecordingPage,
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt },
|
||||||
|
artist: { type: MBID },
|
||||||
|
release: { type: MBID }
|
||||||
|
},
|
||||||
|
resolve: browseResolver()
|
||||||
|
},
|
||||||
|
releases: {
|
||||||
|
type: ReleasePage,
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt },
|
||||||
|
area: { type: MBID },
|
||||||
|
artist: { type: MBID },
|
||||||
|
label: { type: MBID },
|
||||||
|
track: { type: MBID },
|
||||||
|
trackArtist: { type: MBID },
|
||||||
|
recording: { type: MBID },
|
||||||
|
releaseGroup: { type: MBID }
|
||||||
|
},
|
||||||
|
resolve: browseResolver()
|
||||||
|
},
|
||||||
|
releaseGroups: {
|
||||||
|
type: ReleaseGroupPage,
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt },
|
||||||
|
artist: { type: MBID },
|
||||||
|
release: { type: MBID }
|
||||||
|
},
|
||||||
|
resolve: browseResolver()
|
||||||
|
},
|
||||||
|
works: {
|
||||||
|
type: WorkPage,
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt },
|
||||||
|
artist: { type: MBID }
|
||||||
|
},
|
||||||
|
resolve: browseResolver()
|
||||||
|
},
|
||||||
|
urls: {
|
||||||
|
type: URLPage,
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt },
|
||||||
|
resource: { type: URLString }
|
||||||
|
},
|
||||||
|
resolve: browseResolver()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
3
src/queries/index.js
Normal file
3
src/queries/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as LookupQuery } from './lookup'
|
||||||
|
export { default as BrowseQuery } from './browse'
|
||||||
|
export { default as SearchQuery } from './search'
|
||||||
35
src/queries/lookup.js
Normal file
35
src/queries/lookup.js
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { GraphQLObjectType } from 'graphql'
|
||||||
|
import {
|
||||||
|
Area,
|
||||||
|
Artist,
|
||||||
|
Event,
|
||||||
|
Instrument,
|
||||||
|
Label,
|
||||||
|
Place,
|
||||||
|
Recording,
|
||||||
|
Release,
|
||||||
|
ReleaseGroup,
|
||||||
|
URL,
|
||||||
|
Work
|
||||||
|
} from '../types'
|
||||||
|
import { lookupQuery } from '../types/helpers'
|
||||||
|
|
||||||
|
export default new GraphQLObjectType({
|
||||||
|
name: 'LookupQuery',
|
||||||
|
description:
|
||||||
|
'You can perform a lookup of an entity when you have the MBID for that ' +
|
||||||
|
'entity.',
|
||||||
|
fields: {
|
||||||
|
area: lookupQuery(Area),
|
||||||
|
artist: lookupQuery(Artist),
|
||||||
|
event: lookupQuery(Event),
|
||||||
|
instrument: lookupQuery(Instrument),
|
||||||
|
label: lookupQuery(Label),
|
||||||
|
place: lookupQuery(Place),
|
||||||
|
recording: lookupQuery(Recording),
|
||||||
|
release: lookupQuery(Release),
|
||||||
|
releaseGroup: lookupQuery(ReleaseGroup),
|
||||||
|
url: lookupQuery(URL),
|
||||||
|
work: lookupQuery(Work)
|
||||||
|
}
|
||||||
|
})
|
||||||
29
src/queries/search.js
Normal file
29
src/queries/search.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { GraphQLObjectType } from 'graphql'
|
||||||
|
import {
|
||||||
|
AreaPage,
|
||||||
|
ArtistPage,
|
||||||
|
LabelPage,
|
||||||
|
PlacePage,
|
||||||
|
RecordingPage,
|
||||||
|
ReleasePage,
|
||||||
|
ReleaseGroupPage,
|
||||||
|
WorkPage
|
||||||
|
} from '../types'
|
||||||
|
import { searchQuery } from '../types/helpers'
|
||||||
|
|
||||||
|
export default new GraphQLObjectType({
|
||||||
|
name: 'SearchQuery',
|
||||||
|
description:
|
||||||
|
'Search queries provide a way to search for MusicBrainz entities using ' +
|
||||||
|
'Lucene query syntax.',
|
||||||
|
fields: {
|
||||||
|
areas: searchQuery(AreaPage),
|
||||||
|
artists: searchQuery(ArtistPage),
|
||||||
|
labels: searchQuery(LabelPage),
|
||||||
|
places: searchQuery(PlacePage),
|
||||||
|
recordings: searchQuery(RecordingPage),
|
||||||
|
releases: searchQuery(ReleasePage),
|
||||||
|
releaseGroups: searchQuery(ReleaseGroupPage),
|
||||||
|
works: searchQuery(WorkPage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -3,14 +3,14 @@ export default class RateLimit {
|
||||||
options = {
|
options = {
|
||||||
limit: 1,
|
limit: 1,
|
||||||
period: 1000,
|
period: 1000,
|
||||||
maxConcurrency: options.limit || 1,
|
concurrency: options.limit || 1,
|
||||||
defaultPriority: 1,
|
defaultPriority: 1,
|
||||||
...options
|
...options
|
||||||
}
|
}
|
||||||
this.limit = options.limit
|
this.limit = options.limit
|
||||||
this.period = options.period
|
this.period = options.period
|
||||||
this.defaultPriority = options.defaultPriority
|
this.defaultPriority = options.defaultPriority
|
||||||
this.maxConcurrency = options.maxConcurrency
|
this.concurrency = options.concurrency
|
||||||
this.queues = []
|
this.queues = []
|
||||||
this.numPending = 0
|
this.numPending = 0
|
||||||
this.periodStart = null
|
this.periodStart = null
|
||||||
|
|
@ -69,7 +69,7 @@ export default class RateLimit {
|
||||||
if (this.paused) {
|
if (this.paused) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.numPending < this.maxConcurrency && this.periodCapacity > 0) {
|
if (this.numPending < this.concurrency && this.periodCapacity > 0) {
|
||||||
const task = this.dequeue()
|
const task = this.dequeue()
|
||||||
if (task) {
|
if (task) {
|
||||||
const { resolve, reject, fn, args } = task
|
const { resolve, reject, fn, args } = task
|
||||||
|
|
@ -118,7 +118,7 @@ if (require.main === module) {
|
||||||
const limiter = new RateLimit({
|
const limiter = new RateLimit({
|
||||||
limit: 3,
|
limit: 3,
|
||||||
period: 3000,
|
period: 3000,
|
||||||
maxConcurrency: 5
|
concurrency: 5
|
||||||
})
|
})
|
||||||
|
|
||||||
const fn = (i) => {
|
const fn = (i) => {
|
||||||
|
|
|
||||||
127
src/resolvers.js
Normal file
127
src/resolvers.js
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
import dashify from 'dashify'
|
||||||
|
import { getFields, extendIncludes } from './util'
|
||||||
|
|
||||||
|
export function includeRelations (params, info) {
|
||||||
|
let fields = getFields(info)
|
||||||
|
if (info.fieldName !== 'relations') {
|
||||||
|
if (fields.relations) {
|
||||||
|
fields = getFields(fields.relations)
|
||||||
|
} else {
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fields) {
|
||||||
|
const relations = Object.keys(fields)
|
||||||
|
const includeRels = relations.map(key => `${dashify(key)}-rels`)
|
||||||
|
if (includeRels.length) {
|
||||||
|
params = {
|
||||||
|
...params,
|
||||||
|
inc: extendIncludes(params.inc, includeRels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
export function includeSubqueries (params, info) {
|
||||||
|
const fields = getFields(info)
|
||||||
|
if (fields.artistCredit) {
|
||||||
|
params = {
|
||||||
|
...params,
|
||||||
|
inc: extendIncludes(params.inc, ['artist-credits'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lookupResolver (entityType, extraParams = {}) {
|
||||||
|
return (root, { id }, { lookupLoader }, info) => {
|
||||||
|
const params = includeRelations(extraParams, info)
|
||||||
|
entityType = entityType || dashify(info.fieldName)
|
||||||
|
return lookupLoader.load([entityType, id, params])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function browseResolver () {
|
||||||
|
return (source, args, { browseLoader }, info) => {
|
||||||
|
const pluralName = dashify(info.fieldName)
|
||||||
|
let singularName = pluralName
|
||||||
|
if (pluralName.endsWith('s')) {
|
||||||
|
singularName = pluralName.slice(0, -1)
|
||||||
|
}
|
||||||
|
const params = args
|
||||||
|
return browseLoader.load([singularName, params])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function searchResolver () {
|
||||||
|
return (source, args, { searchLoader }, info) => {
|
||||||
|
const pluralName = dashify(info.fieldName)
|
||||||
|
let singularName = pluralName
|
||||||
|
if (pluralName.endsWith('s')) {
|
||||||
|
singularName = pluralName.slice(0, -1)
|
||||||
|
}
|
||||||
|
const { query, ...params } = args
|
||||||
|
return searchLoader.load([singularName, query, params])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function relationResolver () {
|
||||||
|
return (source, { offset = 0,
|
||||||
|
limit,
|
||||||
|
direction,
|
||||||
|
type,
|
||||||
|
typeID }, { lookupLoader }, info) => {
|
||||||
|
const targetType = dashify(info.fieldName).replace('-', '_')
|
||||||
|
return source.filter(relation => {
|
||||||
|
if (relation['target-type'] !== targetType) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (direction != null && relation.direction !== direction) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (type != null && relation.type !== type) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (typeID != null && relation['type-id'] !== typeID) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}).slice(offset, limit == null ? undefined : offset + limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function linkedResolver () {
|
||||||
|
return (source, args, { browseLoader }, info) => {
|
||||||
|
const pluralName = dashify(info.fieldName)
|
||||||
|
let singularName = pluralName
|
||||||
|
if (pluralName.endsWith('s')) {
|
||||||
|
singularName = pluralName.slice(0, -1)
|
||||||
|
}
|
||||||
|
const parentEntity = dashify(info.parentType.name)
|
||||||
|
let params = {
|
||||||
|
[parentEntity]: source.id,
|
||||||
|
type: [],
|
||||||
|
status: [],
|
||||||
|
limit: args.limit,
|
||||||
|
offset: args.offset
|
||||||
|
}
|
||||||
|
params = includeSubqueries(params, info)
|
||||||
|
params = includeRelations(params, info)
|
||||||
|
if (args.type) {
|
||||||
|
params.type.push(args.type)
|
||||||
|
}
|
||||||
|
if (args.types) {
|
||||||
|
params.type.push(...args.types)
|
||||||
|
}
|
||||||
|
if (args.status) {
|
||||||
|
params.status.push(args.status)
|
||||||
|
}
|
||||||
|
if (args.statuses) {
|
||||||
|
params.status.push(...args.statuses)
|
||||||
|
}
|
||||||
|
return browseLoader.load([singularName, params]).then(list => {
|
||||||
|
return list[pluralName]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/schema.js
117
src/schema.js
|
|
@ -1,118 +1,13 @@
|
||||||
import { GraphQLSchema, GraphQLObjectType, GraphQLNonNull } from 'graphql'
|
import { GraphQLSchema, GraphQLObjectType } from 'graphql'
|
||||||
import MBID from './types/mbid'
|
import { LookupQuery, BrowseQuery, SearchQuery } from './queries'
|
||||||
import ArtistType from './types/artist'
|
|
||||||
import WorkType from './types/work'
|
|
||||||
import RecordingType from './types/recording'
|
|
||||||
import ReleaseGroupType from './types/release-group'
|
|
||||||
import ReleaseType from './types/release'
|
|
||||||
import PlaceType from './types/place'
|
|
||||||
import { getFields } from './util'
|
|
||||||
import { entityLoader } from './loaders'
|
|
||||||
|
|
||||||
export default new GraphQLSchema({
|
export default new GraphQLSchema({
|
||||||
query: new GraphQLObjectType({
|
query: new GraphQLObjectType({
|
||||||
name: 'RootQueryType',
|
name: 'RootQuery',
|
||||||
fields: () => ({
|
fields: () => ({
|
||||||
artist: {
|
lookup: { type: LookupQuery, resolve: () => ({}) },
|
||||||
type: ArtistType,
|
browse: { type: BrowseQuery, resolve: () => ({}) },
|
||||||
args: { id: { type: new GraphQLNonNull(MBID) } },
|
search: { type: SearchQuery, resolve: () => ({}) }
|
||||||
resolve (root, { id }, context, info) {
|
|
||||||
const include = []
|
|
||||||
const params = { inc: include }
|
|
||||||
let releaseType
|
|
||||||
let releaseGroupType
|
|
||||||
const fields = getFields(info)
|
|
||||||
if (fields.aliases) {
|
|
||||||
include.push('aliases')
|
|
||||||
}
|
|
||||||
if (fields.works) {
|
|
||||||
include.push('works')
|
|
||||||
}
|
|
||||||
if (fields.recordings) {
|
|
||||||
include.push('recordings')
|
|
||||||
}
|
|
||||||
if (fields.releases) {
|
|
||||||
include.push('releases')
|
|
||||||
fields.releases.arguments.forEach(arg => {
|
|
||||||
if (arg.name.value === 'status' || arg.name.value === 'type') {
|
|
||||||
params[arg.name.value] = arg.value.value
|
|
||||||
releaseType = params.type
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (fields.releaseGroups) {
|
|
||||||
include.push('release-groups')
|
|
||||||
fields.releaseGroups.arguments.forEach(arg => {
|
|
||||||
if (arg.name.value === 'type') {
|
|
||||||
params[arg.name.value] = arg.value.value
|
|
||||||
releaseGroupType = params.type
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (releaseType !== releaseGroupType) {
|
|
||||||
throw new Error(
|
|
||||||
"You tried to fetch both 'releases' and 'releaseGroups', but " +
|
|
||||||
"specified a different 'type' value on each; they must be the " +
|
|
||||||
'same')
|
|
||||||
}
|
|
||||||
return entityLoader.load(['artist', id, params])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
work: {
|
|
||||||
type: WorkType,
|
|
||||||
args: { id: { type: MBID } },
|
|
||||||
resolve (root, { id }, context, info) {
|
|
||||||
const include = []
|
|
||||||
return entityLoader.load(['work', id, { inc: include }])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
recording: {
|
|
||||||
type: RecordingType,
|
|
||||||
args: { id: { type: MBID } },
|
|
||||||
resolve (root, { id }, context, info) {
|
|
||||||
const include = []
|
|
||||||
const fields = getFields(info)
|
|
||||||
if (fields.artists || fields.artistByline) {
|
|
||||||
include.push('artists')
|
|
||||||
}
|
|
||||||
return entityLoader.load(['recording', id, { inc: include }])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
release: {
|
|
||||||
type: ReleaseType,
|
|
||||||
args: { id: { type: MBID } },
|
|
||||||
resolve (root, { id }, context, info) {
|
|
||||||
const include = []
|
|
||||||
const fields = getFields(info)
|
|
||||||
if (fields.artists || fields.artistByline) {
|
|
||||||
include.push('artists')
|
|
||||||
}
|
|
||||||
return entityLoader.load(['release', id, { inc: include }])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
releaseGroup: {
|
|
||||||
type: ReleaseGroupType,
|
|
||||||
args: { id: { type: MBID } },
|
|
||||||
resolve (root, { id }, context, info) {
|
|
||||||
const include = []
|
|
||||||
const fields = getFields(info)
|
|
||||||
if (fields.artists || fields.artistByline) {
|
|
||||||
include.push('artists')
|
|
||||||
}
|
|
||||||
if (fields.releases) {
|
|
||||||
include.push('releases')
|
|
||||||
}
|
|
||||||
return entityLoader.load(['release-group', id, { inc: include }])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
place: {
|
|
||||||
type: PlaceType,
|
|
||||||
args: { id: { type: MBID } },
|
|
||||||
resolve (root, { id }, context, info) {
|
|
||||||
const include = []
|
|
||||||
return entityLoader.load(['place', id, { inc: include }])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import {
|
||||||
GraphQLString,
|
GraphQLString,
|
||||||
GraphQLBoolean
|
GraphQLBoolean
|
||||||
} from 'graphql/type'
|
} from 'graphql/type'
|
||||||
import MBID from './mbid'
|
import { MBID } from './scalars'
|
||||||
import { getHyphenated } from './helpers'
|
import { getHyphenated } from './helpers'
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
export default new GraphQLObjectType({
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,38 @@
|
||||||
|
import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql/type'
|
||||||
|
import Entity from './entity'
|
||||||
import {
|
import {
|
||||||
GraphQLObjectType,
|
id,
|
||||||
GraphQLNonNull,
|
name,
|
||||||
GraphQLString,
|
sortName,
|
||||||
GraphQLList
|
disambiguation,
|
||||||
} from 'graphql/type'
|
artists,
|
||||||
import MBID from './mbid'
|
events,
|
||||||
import { getHyphenated } from './helpers'
|
labels,
|
||||||
|
places,
|
||||||
|
releases,
|
||||||
|
createPageType
|
||||||
|
} from './helpers'
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
const Area = new GraphQLObjectType({
|
||||||
name: 'Area',
|
name: 'Area',
|
||||||
description: 'A country, region, city or the like.',
|
description: 'A country, region, city or the like.',
|
||||||
|
interfaces: () => [Entity],
|
||||||
fields: () => ({
|
fields: () => ({
|
||||||
id: { type: new GraphQLNonNull(MBID) },
|
id,
|
||||||
disambiguation: { type: GraphQLString },
|
name,
|
||||||
name: { type: GraphQLString },
|
sortName,
|
||||||
sortName: { type: GraphQLString, resolve: getHyphenated },
|
disambiguation,
|
||||||
isoCodes: { type: new GraphQLList(GraphQLString), resolve: data => data['iso-3166-1-codes'] }
|
isoCodes: {
|
||||||
|
type: new GraphQLList(GraphQLString),
|
||||||
|
resolve: data => data['iso-3166-1-codes']
|
||||||
|
},
|
||||||
|
artists,
|
||||||
|
events,
|
||||||
|
labels,
|
||||||
|
places,
|
||||||
|
releases
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const AreaPage = createPageType(Area)
|
||||||
|
export default Area
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { GraphQLObjectType, GraphQLString } from 'graphql/type'
|
import { GraphQLObjectType, GraphQLString } from 'graphql/type'
|
||||||
import ArtistType from './artist'
|
import Artist from './artist'
|
||||||
|
import { name } from './helpers'
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
export default new GraphQLObjectType({
|
||||||
name: 'ArtistCredit',
|
name: 'ArtistCredit',
|
||||||
|
|
@ -7,8 +8,15 @@ export default new GraphQLObjectType({
|
||||||
'Artist, variation of artist name and piece of text to join the artist ' +
|
'Artist, variation of artist name and piece of text to join the artist ' +
|
||||||
'name to the next.',
|
'name to the next.',
|
||||||
fields: () => ({
|
fields: () => ({
|
||||||
artist: { type: ArtistType },
|
artist: {
|
||||||
name: { type: GraphQLString },
|
type: Artist,
|
||||||
|
resolve: (source) => {
|
||||||
|
const { artist } = source
|
||||||
|
artist.entityType = 'artist'
|
||||||
|
return artist
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name,
|
||||||
joinPhrase: { type: GraphQLString, resolve: data => data['joinphrase'] }
|
joinPhrase: { type: GraphQLString, resolve: data => data['joinphrase'] }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,69 @@
|
||||||
|
import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql/type'
|
||||||
|
import Entity from './entity'
|
||||||
|
import Alias from './alias'
|
||||||
|
import Area from './area'
|
||||||
import {
|
import {
|
||||||
GraphQLObjectType,
|
getFallback,
|
||||||
GraphQLNonNull,
|
fieldWithID,
|
||||||
GraphQLString,
|
id,
|
||||||
GraphQLList
|
name,
|
||||||
} from 'graphql/type'
|
sortName,
|
||||||
import MBID from './mbid'
|
disambiguation,
|
||||||
import AliasType from './alias'
|
lifeSpan,
|
||||||
import AreaType from './area'
|
recordings,
|
||||||
import LifeSpanType from './life-span'
|
releases,
|
||||||
import WorkType from './work'
|
releaseGroups,
|
||||||
import RecordingType from './recording'
|
works,
|
||||||
import ReleaseType from './release'
|
relations,
|
||||||
import ReleaseGroupType from './release-group'
|
createPageType
|
||||||
import { getHyphenated, getUnderscored, fieldWithID } from './helpers'
|
} from './helpers'
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
const Artist = new GraphQLObjectType({
|
||||||
name: 'Artist',
|
name: 'Artist',
|
||||||
description:
|
description:
|
||||||
'An artist is generally a musician, a group of musicians, or another ' +
|
'An artist is generally a musician, a group of musicians, or another ' +
|
||||||
'music professional (composer, engineer, illustrator, producer, etc.)',
|
'music professional (composer, engineer, illustrator, producer, etc.)',
|
||||||
|
interfaces: () => [Entity],
|
||||||
fields: () => ({
|
fields: () => ({
|
||||||
id: { type: new GraphQLNonNull(MBID) },
|
id,
|
||||||
name: { type: GraphQLString },
|
name,
|
||||||
sortName: { type: GraphQLString, resolve: getHyphenated },
|
sortName,
|
||||||
aliases: { type: new GraphQLList(AliasType) },
|
disambiguation,
|
||||||
disambiguation: { type: GraphQLString },
|
aliases: {
|
||||||
|
type: new GraphQLList(Alias),
|
||||||
|
resolve: (source, args, { lookupLoader }, 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 entity[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
country: { type: GraphQLString },
|
country: { type: GraphQLString },
|
||||||
area: { type: AreaType },
|
area: { type: Area },
|
||||||
beginArea: { type: AreaType, resolve: getUnderscored },
|
beginArea: {
|
||||||
endArea: { type: AreaType, resolve: getUnderscored },
|
type: Area,
|
||||||
|
resolve: getFallback(['begin-area', 'begin_area'])
|
||||||
|
},
|
||||||
|
endArea: {
|
||||||
|
type: Area,
|
||||||
|
resolve: getFallback(['end-area', 'end_area'])
|
||||||
|
},
|
||||||
|
lifeSpan,
|
||||||
...fieldWithID('gender'),
|
...fieldWithID('gender'),
|
||||||
...fieldWithID('type'),
|
...fieldWithID('type'),
|
||||||
lifeSpan: { type: LifeSpanType, resolve: getHyphenated },
|
recordings,
|
||||||
works: { type: new GraphQLList(WorkType) },
|
releases,
|
||||||
recordings: { type: new GraphQLList(RecordingType) },
|
releaseGroups,
|
||||||
releases: {
|
works,
|
||||||
type: new GraphQLList(ReleaseType),
|
relations
|
||||||
args: { type: { type: GraphQLString }, status: { type: GraphQLString } }
|
|
||||||
},
|
|
||||||
releaseGroups: {
|
|
||||||
type: new GraphQLList(ReleaseGroupType),
|
|
||||||
args: { type: { type: GraphQLString } },
|
|
||||||
resolve: getHyphenated
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const ArtistPage = createPageType(Artist)
|
||||||
|
export default Artist
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import { GraphQLString } from 'graphql/type'
|
|
||||||
|
|
||||||
export default GraphQLString
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
import { GraphQLInterfaceType, GraphQLNonNull } from 'graphql/type'
|
import { GraphQLInterfaceType } from 'graphql/type'
|
||||||
import MBID from './mbid'
|
import { id } from './helpers'
|
||||||
|
|
||||||
export default new GraphQLInterfaceType({
|
export default new GraphQLInterfaceType({
|
||||||
name: 'Entity',
|
name: 'Entity',
|
||||||
|
description: 'An entity in the MusicBrainz schema.',
|
||||||
|
resolveType (value) {
|
||||||
|
if (value.entityType && require.resolve(`./${value.entityType}`)) {
|
||||||
|
return require(`./${value.entityType}`).default
|
||||||
|
}
|
||||||
|
},
|
||||||
fields: () => ({
|
fields: () => ({
|
||||||
id: { type: new GraphQLNonNull(MBID) }
|
id
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
199
src/types/enums.js
Normal file
199
src/types/enums.js
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
import { GraphQLEnumType } from 'graphql/type'
|
||||||
|
|
||||||
|
/*
|
||||||
|
ReleaseStatus {
|
||||||
|
OFFICIAL
|
||||||
|
PROMOTION
|
||||||
|
BOOTLEG
|
||||||
|
PSEUDORELEASE
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
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'
|
||||||
|
},
|
||||||
|
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'
|
||||||
|
},
|
||||||
|
BOOTLEG: {
|
||||||
|
name: '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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
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'
|
||||||
|
},
|
||||||
|
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'
|
||||||
|
},
|
||||||
|
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'
|
||||||
|
},
|
||||||
|
OTHER: {
|
||||||
|
name: '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'
|
||||||
|
},
|
||||||
|
COMPILATION: {
|
||||||
|
name: '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'
|
||||||
|
},
|
||||||
|
SPOKENWORD: {
|
||||||
|
name: 'Spoken Word',
|
||||||
|
description: 'A non-music spoken word release.',
|
||||||
|
value: 'spokenword'
|
||||||
|
},
|
||||||
|
INTERVIEW: {
|
||||||
|
name: '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'
|
||||||
|
},
|
||||||
|
LIVE: {
|
||||||
|
name: 'Live',
|
||||||
|
description: 'A release that was recorded live.',
|
||||||
|
value: 'live'
|
||||||
|
},
|
||||||
|
REMIX: {
|
||||||
|
name: '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'
|
||||||
|
},
|
||||||
|
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'
|
||||||
|
},
|
||||||
|
DEMO: {
|
||||||
|
name: '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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
32
src/types/event.js
Normal file
32
src/types/event.js
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { GraphQLObjectType, GraphQLString, GraphQLBoolean } from 'graphql/type'
|
||||||
|
import Entity from './entity'
|
||||||
|
import { Time } from './scalars'
|
||||||
|
import {
|
||||||
|
fieldWithID,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
disambiguation,
|
||||||
|
lifeSpan,
|
||||||
|
createPageType
|
||||||
|
} from './helpers'
|
||||||
|
|
||||||
|
const Event = new GraphQLObjectType({
|
||||||
|
name: 'Event',
|
||||||
|
description:
|
||||||
|
'An organized event which people can attend, usually live performances ' +
|
||||||
|
'like concerts and festivals.',
|
||||||
|
interfaces: () => [Entity],
|
||||||
|
fields: () => ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
disambiguation,
|
||||||
|
lifeSpan,
|
||||||
|
time: { type: Time },
|
||||||
|
cancelled: { type: GraphQLBoolean },
|
||||||
|
setlist: { type: GraphQLString },
|
||||||
|
...fieldWithID('type')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export const EventPage = createPageType(Event)
|
||||||
|
export default Event
|
||||||
|
|
@ -1,6 +1,40 @@
|
||||||
import dashify from 'dashify'
|
import dashify from 'dashify'
|
||||||
import { GraphQLString, GraphQLList } from 'graphql/type'
|
import {
|
||||||
import MBID from './mbid'
|
GraphQLObjectType,
|
||||||
|
GraphQLString,
|
||||||
|
GraphQLInt,
|
||||||
|
GraphQLList,
|
||||||
|
GraphQLNonNull
|
||||||
|
} from 'graphql/type'
|
||||||
|
import { MBID, DateType } from './scalars'
|
||||||
|
import { ReleaseGroupType, ReleaseStatus } from './enums'
|
||||||
|
import ArtistCredit from './artist-credit'
|
||||||
|
import Artist from './artist'
|
||||||
|
import Event from './event'
|
||||||
|
import Label from './label'
|
||||||
|
import LifeSpan from './life-span'
|
||||||
|
import Place from './place'
|
||||||
|
import Recording from './recording'
|
||||||
|
import Relation from './relation'
|
||||||
|
import Release from './release'
|
||||||
|
import ReleaseGroup from './release-group'
|
||||||
|
import Work from './work'
|
||||||
|
import {
|
||||||
|
lookupResolver,
|
||||||
|
linkedResolver,
|
||||||
|
relationResolver,
|
||||||
|
searchResolver,
|
||||||
|
includeRelations
|
||||||
|
} from '../resolvers'
|
||||||
|
|
||||||
|
export function getByline (data) {
|
||||||
|
const credit = data['artist-credit']
|
||||||
|
if (credit && credit.length) {
|
||||||
|
return credit.reduce((byline, credit) => {
|
||||||
|
return byline + credit.name + credit.joinphrase
|
||||||
|
}, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function fieldWithID (name, config = {}) {
|
export function fieldWithID (name, config = {}) {
|
||||||
config = {
|
config = {
|
||||||
|
|
@ -26,16 +60,214 @@ export function getHyphenated (source, args, context, info) {
|
||||||
return source[name]
|
return source[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUnderscored (source, args, context, info) {
|
export function getFallback (keys) {
|
||||||
const name = dashify(info.fieldName).replace('-', '_')
|
return (source) => {
|
||||||
return source[name]
|
for (let i = 0; i < keys.length; i++) {
|
||||||
}
|
const key = keys[i]
|
||||||
|
if (key in source) {
|
||||||
export function getByline (data) {
|
return source[key]
|
||||||
const credit = data['artist-credit']
|
}
|
||||||
if (credit && credit.length) {
|
}
|
||||||
return credit.reduce((byline, credit) => {
|
|
||||||
return byline + credit.name + credit.joinphrase
|
|
||||||
}, '')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function lookupQuery (entity, params) {
|
||||||
|
return {
|
||||||
|
type: entity,
|
||||||
|
description: `Look up a specific ${entity.name} by its MBID.`,
|
||||||
|
args: { id },
|
||||||
|
resolve: lookupResolver(dashify(entity.name), params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function searchQuery (entityPage) {
|
||||||
|
const entity = entityPage.getFields().results.type.ofType.ofType
|
||||||
|
return {
|
||||||
|
type: entityPage,
|
||||||
|
description: `Search for ${entity.name} entities.`,
|
||||||
|
args: {
|
||||||
|
query: { type: new GraphQLNonNull(GraphQLString) },
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt }
|
||||||
|
},
|
||||||
|
resolve: searchResolver()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPageType (type) {
|
||||||
|
const singularName = dashify(type.name)
|
||||||
|
const pluralName = singularName + (singularName.endsWith('s') ? '' : 's')
|
||||||
|
return new GraphQLObjectType({
|
||||||
|
name: `${type.name}Page`,
|
||||||
|
description: `A page of ${type.name} results from browsing or searching.`,
|
||||||
|
fields: {
|
||||||
|
count: {
|
||||||
|
type: new GraphQLNonNull(GraphQLInt),
|
||||||
|
resolve: list => {
|
||||||
|
if (list.count != null) {
|
||||||
|
return list.count
|
||||||
|
}
|
||||||
|
return list[`${singularName}-count`]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
offset: {
|
||||||
|
type: new GraphQLNonNull(GraphQLInt),
|
||||||
|
resolve: list => {
|
||||||
|
if (list.offset != null) {
|
||||||
|
return list.offset
|
||||||
|
}
|
||||||
|
return list[`${singularName}-offset`]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: { type: DateType },
|
||||||
|
results: {
|
||||||
|
type: new GraphQLNonNull(new GraphQLList(type)),
|
||||||
|
resolve: list => list[pluralName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const id = { type: new GraphQLNonNull(MBID) }
|
||||||
|
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 relation = {
|
||||||
|
type: new GraphQLList(Relation),
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt },
|
||||||
|
direction: { type: GraphQLString },
|
||||||
|
type: { type: GraphQLString },
|
||||||
|
typeID: { type: MBID }
|
||||||
|
},
|
||||||
|
resolve: relationResolver()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const relations = {
|
||||||
|
type: new GraphQLObjectType({
|
||||||
|
name: 'Relations',
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
resolve: (source, args, { lookupLoader }, info) => {
|
||||||
|
if (source.relations != null) {
|
||||||
|
return source.relations
|
||||||
|
}
|
||||||
|
const entityType = dashify(info.parentType.name)
|
||||||
|
const id = source.id
|
||||||
|
const params = includeRelations({}, info)
|
||||||
|
return lookupLoader.load([entityType, id, params]).then(entity => {
|
||||||
|
return entity.relations
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const artistCredit = {
|
||||||
|
type: new GraphQLList(ArtistCredit),
|
||||||
|
resolve: (source, args, { lookupLoader }, 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 entity[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const artists = {
|
||||||
|
type: new GraphQLList(Artist),
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt }
|
||||||
|
},
|
||||||
|
resolve: linkedResolver()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const events = {
|
||||||
|
type: new GraphQLList(Event),
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt }
|
||||||
|
},
|
||||||
|
resolve: linkedResolver()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const labels = {
|
||||||
|
type: new GraphQLList(Label),
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt }
|
||||||
|
},
|
||||||
|
resolve: linkedResolver()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const places = {
|
||||||
|
type: new GraphQLList(Place),
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt }
|
||||||
|
},
|
||||||
|
resolve: linkedResolver()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const recordings = {
|
||||||
|
type: new GraphQLList(Recording),
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt }
|
||||||
|
},
|
||||||
|
resolve: linkedResolver()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const releases = {
|
||||||
|
type: new GraphQLList(Release),
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt },
|
||||||
|
type: { type: ReleaseGroupType },
|
||||||
|
types: { type: new GraphQLList(ReleaseGroupType) },
|
||||||
|
status: { type: ReleaseStatus },
|
||||||
|
statuses: { type: new GraphQLList(ReleaseStatus) }
|
||||||
|
},
|
||||||
|
resolve: linkedResolver()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const releaseGroups = {
|
||||||
|
type: new GraphQLList(ReleaseGroup),
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt },
|
||||||
|
type: { type: ReleaseGroupType },
|
||||||
|
types: { type: new GraphQLList(ReleaseGroupType) }
|
||||||
|
},
|
||||||
|
resolve: linkedResolver()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const works = {
|
||||||
|
type: new GraphQLList(Work),
|
||||||
|
args: {
|
||||||
|
limit: { type: GraphQLInt },
|
||||||
|
offset: { type: GraphQLInt }
|
||||||
|
},
|
||||||
|
resolve: linkedResolver()
|
||||||
|
}
|
||||||
|
|
|
||||||
14
src/types/index.js
Normal file
14
src/types/index.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
export { MBID, DateType, IPI, URLString } from './scalars'
|
||||||
|
export { ReleaseGroupType, ReleaseStatus } from './enums'
|
||||||
|
export { default as Entity } from './entity'
|
||||||
|
export { default as Area, AreaPage } from './area'
|
||||||
|
export { default as Artist, ArtistPage } from './artist'
|
||||||
|
export { default as Event, EventPage } from './event'
|
||||||
|
export { default as Instrument } from './instrument'
|
||||||
|
export { default as Label, LabelPage } from './label'
|
||||||
|
export { default as Place, PlacePage } from './place'
|
||||||
|
export { default as Recording, RecordingPage } from './recording'
|
||||||
|
export { default as Release, ReleasePage } from './release'
|
||||||
|
export { default as ReleaseGroup, ReleaseGroupPage } from './release-group'
|
||||||
|
export { default as URL, URLPage } from './url'
|
||||||
|
export { default as Work, WorkPage } from './work'
|
||||||
24
src/types/instrument.js
Normal file
24
src/types/instrument.js
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { GraphQLObjectType, GraphQLString } from 'graphql/type'
|
||||||
|
import Entity from './entity'
|
||||||
|
import {
|
||||||
|
fieldWithID,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
disambiguation
|
||||||
|
} from './helpers'
|
||||||
|
|
||||||
|
const Instrument = new GraphQLObjectType({
|
||||||
|
name: 'Instrument',
|
||||||
|
description:
|
||||||
|
'Instruments are devices created or adapted to make musical sounds.',
|
||||||
|
interfaces: () => [Entity],
|
||||||
|
fields: () => ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
disambiguation,
|
||||||
|
description: { type: GraphQLString },
|
||||||
|
...fieldWithID('type')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Instrument
|
||||||
41
src/types/label.js
Normal file
41
src/types/label.js
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import {
|
||||||
|
GraphQLObjectType,
|
||||||
|
GraphQLList,
|
||||||
|
GraphQLString,
|
||||||
|
GraphQLInt
|
||||||
|
} from 'graphql/type'
|
||||||
|
import Entity from './entity'
|
||||||
|
import { IPI } from './scalars'
|
||||||
|
import Area from './area'
|
||||||
|
import {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
sortName,
|
||||||
|
disambiguation,
|
||||||
|
lifeSpan,
|
||||||
|
releases,
|
||||||
|
fieldWithID,
|
||||||
|
createPageType
|
||||||
|
} from './helpers'
|
||||||
|
|
||||||
|
const Label = new GraphQLObjectType({
|
||||||
|
name: 'Label',
|
||||||
|
description: 'Labels represent mostly (but not only) imprints.',
|
||||||
|
interfaces: () => [Entity],
|
||||||
|
fields: () => ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
sortName,
|
||||||
|
disambiguation,
|
||||||
|
country: { type: GraphQLString },
|
||||||
|
area: { type: Area },
|
||||||
|
lifeSpan,
|
||||||
|
labelCode: { type: GraphQLInt },
|
||||||
|
ipis: { type: new GraphQLList(IPI) },
|
||||||
|
...fieldWithID('type'),
|
||||||
|
releases
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export const LabelPage = createPageType(Label)
|
||||||
|
export default Label
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { GraphQLObjectType, GraphQLBoolean } from 'graphql/type'
|
import { GraphQLObjectType, GraphQLBoolean } from 'graphql/type'
|
||||||
import DateType from './date'
|
import { DateType } from './scalars'
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
export default new GraphQLObjectType({
|
||||||
name: 'LifeSpan',
|
name: 'LifeSpan',
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import { Kind } from 'graphql'
|
|
||||||
import { GraphQLScalarType } from 'graphql/type'
|
|
||||||
|
|
||||||
// e.g. 24fdb962-65ef-41ca-9ba3-7251a23a84fc
|
|
||||||
const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
|
||||||
|
|
||||||
function uuid (value) {
|
|
||||||
if (typeof value === 'string' && value.length === 36 && regex.test(value)) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
throw new TypeError(`Malformed UUID: ${value}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new GraphQLScalarType({
|
|
||||||
name: 'MBID',
|
|
||||||
description:
|
|
||||||
'The `MBID` scalar represents MusicBrainz identifiers, which are ' +
|
|
||||||
'36-character UUIDs.',
|
|
||||||
serialize: uuid,
|
|
||||||
parseValue: uuid,
|
|
||||||
parseLiteral (ast) {
|
|
||||||
if (ast.kind === Kind.STRING) {
|
|
||||||
return uuid(ast.value)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
@ -1,30 +1,44 @@
|
||||||
import { GraphQLObjectType, GraphQLNonNull, GraphQLString } from 'graphql/type'
|
import { GraphQLObjectType, GraphQLString } from 'graphql/type'
|
||||||
import MBID from './mbid'
|
import Entity from './entity'
|
||||||
import AreaType from './area'
|
import { Degrees } from './scalars'
|
||||||
import LifeSpanType from './life-span'
|
import Area from './area'
|
||||||
import { getHyphenated, fieldWithID } from './helpers'
|
import {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
disambiguation,
|
||||||
|
lifeSpan,
|
||||||
|
events,
|
||||||
|
fieldWithID,
|
||||||
|
createPageType
|
||||||
|
} from './helpers'
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
export const Coordinates = new GraphQLObjectType({
|
||||||
|
name: 'Coordinates',
|
||||||
|
description: 'Geographic coordinates with latitude and longitude.',
|
||||||
|
fields: () => ({
|
||||||
|
latitude: { type: Degrees },
|
||||||
|
longitude: { type: Degrees }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const Place = new GraphQLObjectType({
|
||||||
name: 'Place',
|
name: 'Place',
|
||||||
description:
|
description:
|
||||||
'A venue, studio or other place where music is performed, recorded, ' +
|
'A venue, studio or other place where music is performed, recorded, ' +
|
||||||
'engineered, etc.',
|
'engineered, etc.',
|
||||||
|
interfaces: () => [Entity],
|
||||||
fields: () => ({
|
fields: () => ({
|
||||||
id: { type: new GraphQLNonNull(MBID) },
|
id,
|
||||||
name: { type: GraphQLString },
|
name,
|
||||||
disambiguation: { type: GraphQLString },
|
disambiguation,
|
||||||
address: { type: GraphQLString },
|
address: { type: GraphQLString },
|
||||||
area: { type: AreaType },
|
area: { type: Area },
|
||||||
coordinates: {
|
coordinates: { type: Coordinates },
|
||||||
type: new GraphQLObjectType({
|
lifeSpan,
|
||||||
name: 'Coordinates',
|
...fieldWithID('type'),
|
||||||
fields: () => ({
|
events
|
||||||
latitude: { type: GraphQLString },
|
|
||||||
longitude: { type: GraphQLString }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
lifeSpan: { type: LifeSpanType, resolve: getHyphenated },
|
|
||||||
...fieldWithID('type')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const PlacePage = createPageType(Place)
|
||||||
|
export default Place
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,34 @@
|
||||||
|
import { GraphQLObjectType, GraphQLInt, GraphQLBoolean } from 'graphql/type'
|
||||||
|
import Entity from './entity'
|
||||||
import {
|
import {
|
||||||
GraphQLObjectType,
|
id,
|
||||||
GraphQLNonNull,
|
title,
|
||||||
GraphQLString,
|
disambiguation,
|
||||||
GraphQLInt,
|
artistCredit,
|
||||||
GraphQLBoolean,
|
artists,
|
||||||
GraphQLList
|
releases,
|
||||||
} from 'graphql/type'
|
relations,
|
||||||
import MBID from './mbid'
|
createPageType
|
||||||
import ArtistCreditType from './artist-credit'
|
} from './helpers'
|
||||||
import ReleaseType from './release'
|
|
||||||
import { getByline } from './helpers'
|
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
const Recording = new GraphQLObjectType({
|
||||||
name: 'Recording',
|
name: 'Recording',
|
||||||
description:
|
description:
|
||||||
'Represents a unique mix or edit. Has title, artist credit, duration, ' +
|
'Represents a unique mix or edit. Has title, artist credit, duration, ' +
|
||||||
'list of PUIDs and ISRCs.',
|
'list of PUIDs and ISRCs.',
|
||||||
|
interfaces: () => [Entity],
|
||||||
fields: () => ({
|
fields: () => ({
|
||||||
id: { type: new GraphQLNonNull(MBID) },
|
id,
|
||||||
title: { type: GraphQLString },
|
title,
|
||||||
disambiguation: { type: GraphQLString },
|
disambiguation,
|
||||||
|
artistCredit,
|
||||||
length: { type: GraphQLInt },
|
length: { type: GraphQLInt },
|
||||||
video: { type: GraphQLBoolean },
|
video: { type: GraphQLBoolean },
|
||||||
artists: {
|
artists,
|
||||||
type: new GraphQLList(ArtistCreditType),
|
releases,
|
||||||
resolve: data => data['artist-credit']
|
relations
|
||||||
},
|
|
||||||
artistByline: { type: GraphQLString, resolve: getByline },
|
|
||||||
releases: { type: new GraphQLList(ReleaseType) }
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const RecordingPage = createPageType(Recording)
|
||||||
|
export default Recording
|
||||||
|
|
|
||||||
43
src/types/relation.js
Normal file
43
src/types/relation.js
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
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
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { GraphQLObjectType } from 'graphql/type'
|
import { GraphQLObjectType } from 'graphql/type'
|
||||||
import DateType from './date'
|
import { DateType } from './scalars'
|
||||||
import AreaType from './area'
|
import Area from './area'
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
export default new GraphQLObjectType({
|
||||||
name: 'ReleaseEvent',
|
name: 'ReleaseEvent',
|
||||||
|
|
@ -9,7 +9,7 @@ export default new GraphQLObjectType({
|
||||||
'particular label, catalog number, barcode, and what release format ' +
|
'particular label, catalog number, barcode, and what release format ' +
|
||||||
'was used.',
|
'was used.',
|
||||||
fields: () => ({
|
fields: () => ({
|
||||||
area: { type: AreaType },
|
area: { type: Area },
|
||||||
date: { type: DateType }
|
date: { type: DateType }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,38 @@
|
||||||
|
import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql/type'
|
||||||
|
import Entity from './entity'
|
||||||
|
import { DateType } from './scalars'
|
||||||
import {
|
import {
|
||||||
GraphQLObjectType,
|
id,
|
||||||
GraphQLNonNull,
|
title,
|
||||||
GraphQLString,
|
disambiguation,
|
||||||
GraphQLList
|
artistCredit,
|
||||||
} from 'graphql/type'
|
artists,
|
||||||
import MBID from './mbid'
|
releases,
|
||||||
import DateType from './date'
|
relations,
|
||||||
import ArtistCreditType from './artist-credit'
|
getHyphenated,
|
||||||
import ReleaseType from './release'
|
fieldWithID,
|
||||||
import { getHyphenated, fieldWithID, getByline } from './helpers'
|
createPageType
|
||||||
|
} from './helpers'
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
const ReleaseGroup = new GraphQLObjectType({
|
||||||
name: 'ReleaseGroup',
|
name: 'ReleaseGroup',
|
||||||
description:
|
description:
|
||||||
'Represents an abstract "album" (or "single", or "EP") entity. ' +
|
'Represents an abstract "album" (or "single", or "EP") entity. ' +
|
||||||
'Technically it’s a group of releases, with a specified type.',
|
'Technically it’s a group of releases, with a specified type.',
|
||||||
|
interfaces: () => [Entity],
|
||||||
fields: () => ({
|
fields: () => ({
|
||||||
id: { type: new GraphQLNonNull(MBID) },
|
id,
|
||||||
title: { type: GraphQLString },
|
title,
|
||||||
disambiguation: { type: GraphQLString },
|
disambiguation,
|
||||||
|
artistCredit,
|
||||||
firstReleaseDate: { type: DateType, resolve: getHyphenated },
|
firstReleaseDate: { type: DateType, resolve: getHyphenated },
|
||||||
...fieldWithID('primaryType'),
|
...fieldWithID('primaryType'),
|
||||||
...fieldWithID('secondaryTypes', { type: new GraphQLList(GraphQLString) }),
|
...fieldWithID('secondaryTypes', { type: new GraphQLList(GraphQLString) }),
|
||||||
artists: {
|
artists,
|
||||||
type: new GraphQLList(ArtistCreditType),
|
releases,
|
||||||
resolve: data => data['artist-credit']
|
relations
|
||||||
},
|
|
||||||
artistByline: { type: GraphQLString, resolve: getByline },
|
|
||||||
releases: {
|
|
||||||
type: new GraphQLList(ReleaseType),
|
|
||||||
args: {
|
|
||||||
type: { type: GraphQLString },
|
|
||||||
status: { type: GraphQLString }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const ReleaseGroupPage = createPageType(ReleaseGroup)
|
||||||
|
export default ReleaseGroup
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,51 @@
|
||||||
|
import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql/type'
|
||||||
|
import Entity from './entity'
|
||||||
|
import { DateType } from './scalars'
|
||||||
|
import ReleaseEvent from './release-event'
|
||||||
import {
|
import {
|
||||||
GraphQLObjectType,
|
id,
|
||||||
GraphQLNonNull,
|
title,
|
||||||
GraphQLString,
|
disambiguation,
|
||||||
GraphQLList
|
artistCredit,
|
||||||
} from 'graphql/type'
|
artists,
|
||||||
import MBID from './mbid'
|
labels,
|
||||||
import DateType from './date'
|
recordings,
|
||||||
import ArtistCreditType from './artist-credit'
|
releaseGroups,
|
||||||
import ReleaseEventType from './release-event'
|
relations,
|
||||||
import { getHyphenated, fieldWithID, getByline } from './helpers'
|
getHyphenated,
|
||||||
|
fieldWithID,
|
||||||
|
createPageType
|
||||||
|
} from './helpers'
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
const Release = new GraphQLObjectType({
|
||||||
name: 'Release',
|
name: 'Release',
|
||||||
description:
|
description:
|
||||||
'Real-world release object you can buy in your music store. It has ' +
|
'Real-world release object you can buy in your music store. It has ' +
|
||||||
'release date and country, list of catalog number and label pairs, ' +
|
'release date and country, list of catalog number and label pairs, ' +
|
||||||
'packaging type and release status.',
|
'packaging type and release status.',
|
||||||
|
interfaces: () => [Entity],
|
||||||
fields: () => ({
|
fields: () => ({
|
||||||
id: { type: new GraphQLNonNull(MBID) },
|
id,
|
||||||
title: { type: GraphQLString },
|
title,
|
||||||
artists: {
|
disambiguation,
|
||||||
type: new GraphQLList(ArtistCreditType),
|
artistCredit,
|
||||||
resolve: data => data['artist-credit']
|
|
||||||
},
|
|
||||||
artistByline: { type: GraphQLString, resolve: getByline },
|
|
||||||
releaseEvents: {
|
releaseEvents: {
|
||||||
type: new GraphQLList(ReleaseEventType),
|
type: new GraphQLList(ReleaseEvent),
|
||||||
resolve: getHyphenated
|
resolve: getHyphenated
|
||||||
},
|
},
|
||||||
disambiguation: { type: GraphQLString },
|
|
||||||
date: { type: DateType },
|
date: { type: DateType },
|
||||||
country: { type: GraphQLString },
|
country: { type: GraphQLString },
|
||||||
barcode: { type: GraphQLString },
|
barcode: { type: GraphQLString },
|
||||||
...fieldWithID('status'),
|
...fieldWithID('status'),
|
||||||
...fieldWithID('packaging'),
|
...fieldWithID('packaging'),
|
||||||
quality: { type: GraphQLString }
|
quality: { type: GraphQLString },
|
||||||
|
artists,
|
||||||
|
labels,
|
||||||
|
recordings,
|
||||||
|
releaseGroups,
|
||||||
|
relations
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const ReleasePage = createPageType(Release)
|
||||||
|
export default Release
|
||||||
|
|
|
||||||
188
src/types/scalars.js
Normal file
188
src/types/scalars.js
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
import { Kind } from 'graphql/language'
|
||||||
|
import { GraphQLScalarType } from 'graphql/type'
|
||||||
|
|
||||||
|
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
||||||
|
|
||||||
|
function validateMBID (value) {
|
||||||
|
if (typeof value === 'string' && uuid.test(value)) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
throw new TypeError(`Malformed MBID: ${value}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePositive (value) {
|
||||||
|
if (value >= 0) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
throw new TypeError(`Expected positive value: ${value}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
scalar Date
|
||||||
|
*/
|
||||||
|
export const DateType = new GraphQLScalarType({
|
||||||
|
name: 'Date',
|
||||||
|
description:
|
||||||
|
'Year, month (optional), and day (optional) in YYYY-MM-DD format.',
|
||||||
|
serialize: value => value,
|
||||||
|
parseValue: value => value,
|
||||||
|
parseLiteral (ast) {
|
||||||
|
if (ast.kind === Kind.STRING) {
|
||||||
|
return ast.value
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
scalar Degrees
|
||||||
|
*/
|
||||||
|
export const Degrees = new GraphQLScalarType({
|
||||||
|
name: 'Degrees',
|
||||||
|
description: 'Decimal degrees, used for latitude and longitude.',
|
||||||
|
serialize: value => value,
|
||||||
|
parseValue: value => value,
|
||||||
|
parseLiteral (ast) {
|
||||||
|
if (ast.kind === Kind.STRING) {
|
||||||
|
return ast.value
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
scalar Duration
|
||||||
|
*/
|
||||||
|
export const Duration = new GraphQLScalarType({
|
||||||
|
name: 'Duration',
|
||||||
|
description: 'A length of time, in milliseconds.',
|
||||||
|
serialize: validatePositive,
|
||||||
|
parseValue: validatePositive,
|
||||||
|
parseLiteral (ast) {
|
||||||
|
if (ast.kind === Kind.INT) {
|
||||||
|
return validatePositive(parseInt(ast.value, 10))
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
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.',
|
||||||
|
serialize: value => value,
|
||||||
|
parseValue: value => value,
|
||||||
|
parseLiteral (ast) {
|
||||||
|
if (ast.kind === Kind.STRING) {
|
||||||
|
return ast.value
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
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.',
|
||||||
|
serialize: value => value,
|
||||||
|
parseValue: value => value,
|
||||||
|
parseLiteral (ast) {
|
||||||
|
if (ast.kind === Kind.STRING) {
|
||||||
|
return ast.value
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
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.',
|
||||||
|
serialize: value => value,
|
||||||
|
parseValue: value => value,
|
||||||
|
parseLiteral (ast) {
|
||||||
|
if (ast.kind === Kind.STRING) {
|
||||||
|
return ast.value
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
scalar Locale
|
||||||
|
*/
|
||||||
|
export const Locale = new GraphQLScalarType({
|
||||||
|
name: 'Locale',
|
||||||
|
description: 'Language code, optionally with country and encoding.',
|
||||||
|
serialize: value => value,
|
||||||
|
parseValue: value => value,
|
||||||
|
parseLiteral (ast) {
|
||||||
|
if (ast.kind === Kind.STRING) {
|
||||||
|
return ast.value
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
scalar Time
|
||||||
|
*/
|
||||||
|
export const Time = new GraphQLScalarType({
|
||||||
|
name: 'Time',
|
||||||
|
description: 'A time of day, in 24-hour hh:mm notation.',
|
||||||
|
serialize: value => value,
|
||||||
|
parseValue: value => value,
|
||||||
|
parseLiteral (ast) {
|
||||||
|
if (ast.kind === Kind.STRING) {
|
||||||
|
return ast.value
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
scalar URLString
|
||||||
|
*/
|
||||||
|
export const URLString = new GraphQLScalarType({
|
||||||
|
name: 'URLString',
|
||||||
|
description: 'Description',
|
||||||
|
serialize: value => value,
|
||||||
|
parseValue: value => value,
|
||||||
|
parseLiteral (ast) {
|
||||||
|
if (ast.kind === Kind.STRING) {
|
||||||
|
return ast.value
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
scalar MBID
|
||||||
|
*/
|
||||||
|
export const MBID = new GraphQLScalarType({
|
||||||
|
name: 'MBID',
|
||||||
|
description:
|
||||||
|
'The `MBID` scalar represents MusicBrainz identifiers, which are ' +
|
||||||
|
'36-character UUIDs.',
|
||||||
|
serialize: validateMBID,
|
||||||
|
parseValue: validateMBID,
|
||||||
|
parseLiteral (ast) {
|
||||||
|
if (ast.kind === Kind.STRING) {
|
||||||
|
return validateMBID(ast.value)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
0
src/types/series.js
Normal file
0
src/types/series.js
Normal file
21
src/types/url.js
Normal file
21
src/types/url.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { GraphQLObjectType, GraphQLNonNull } from 'graphql/type'
|
||||||
|
import Entity from './entity'
|
||||||
|
import { URLString } from './scalars'
|
||||||
|
import { id, relations, createPageType } 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.',
|
||||||
|
interfaces: () => [Entity],
|
||||||
|
fields: () => ({
|
||||||
|
id,
|
||||||
|
resource: { type: new GraphQLNonNull(URLString) },
|
||||||
|
relations
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export const URLPage = createPageType(URL)
|
||||||
|
export default URL
|
||||||
|
|
@ -1,23 +1,32 @@
|
||||||
|
import { GraphQLObjectType, GraphQLString, GraphQLList } from 'graphql/type'
|
||||||
|
import Entity from './entity'
|
||||||
import {
|
import {
|
||||||
GraphQLObjectType,
|
id,
|
||||||
GraphQLNonNull,
|
title,
|
||||||
GraphQLString,
|
disambiguation,
|
||||||
GraphQLList
|
artists,
|
||||||
} from 'graphql/type'
|
relations,
|
||||||
import MBID from './mbid'
|
fieldWithID,
|
||||||
import { fieldWithID } from './helpers'
|
createPageType
|
||||||
|
} from './helpers'
|
||||||
|
|
||||||
export default new GraphQLObjectType({
|
const Work = new GraphQLObjectType({
|
||||||
name: 'Work',
|
name: 'Work',
|
||||||
description:
|
description:
|
||||||
'A distinct intellectual or artistic creation, which can be expressed in ' +
|
'A distinct intellectual or artistic creation, which can be expressed in ' +
|
||||||
'the form of one or more audio recordings',
|
'the form of one or more audio recordings',
|
||||||
|
interfaces: () => [Entity],
|
||||||
fields: () => ({
|
fields: () => ({
|
||||||
id: { type: new GraphQLNonNull(MBID) },
|
id,
|
||||||
title: { type: GraphQLString },
|
title,
|
||||||
disambiguation: { type: GraphQLString },
|
disambiguation,
|
||||||
iswcs: { type: new GraphQLList(GraphQLString) },
|
iswcs: { type: new GraphQLList(GraphQLString) },
|
||||||
language: { type: GraphQLString },
|
language: { type: GraphQLString },
|
||||||
...fieldWithID('type')
|
...fieldWithID('type'),
|
||||||
|
artists,
|
||||||
|
relations
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const WorkPage = createPageType(Work)
|
||||||
|
export default Work
|
||||||
|
|
|
||||||
30
src/util.js
30
src/util.js
|
|
@ -1,7 +1,35 @@
|
||||||
|
import util from 'util'
|
||||||
|
|
||||||
export function getFields (info) {
|
export function getFields (info) {
|
||||||
const selections = info.fieldASTs[0].selectionSet.selections
|
if (info.kind !== 'Field') {
|
||||||
|
info = info.fieldASTs[0]
|
||||||
|
}
|
||||||
|
const selections = info.selectionSet.selections
|
||||||
return selections.reduce((fields, selection) => {
|
return selections.reduce((fields, selection) => {
|
||||||
fields[selection.name.value] = selection
|
fields[selection.name.value] = selection
|
||||||
return fields
|
return fields
|
||||||
}, {})
|
}, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function prettyPrint (obj, { depth = 5,
|
||||||
|
colors = true,
|
||||||
|
breakLength = 120 } = {}) {
|
||||||
|
console.log(util.inspect(obj, { depth, colors, breakLength }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toFilteredArray (obj) {
|
||||||
|
return (Array.isArray(obj) ? obj : [obj]).filter(x => x)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extendIncludes (includes, moreIncludes) {
|
||||||
|
includes = toFilteredArray(includes)
|
||||||
|
moreIncludes = toFilteredArray(moreIncludes)
|
||||||
|
const seen = {}
|
||||||
|
return includes.concat(moreIncludes).filter(x => {
|
||||||
|
if (seen[x]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
seen[x] = true
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue