diff --git a/src/queries/browse.js b/src/queries/browse.js index 84fdf4a..4e7f677 100644 --- a/src/queries/browse.js +++ b/src/queries/browse.js @@ -1,4 +1,6 @@ -import { GraphQLObjectType, GraphQLInt } from 'graphql' +import { GraphQLObjectType } from 'graphql' +import { forwardConnectionArgs } from 'graphql-relay' +import { browseResolver } from '../resolvers' import { MBID, URLString, @@ -12,7 +14,17 @@ import { URLConnection, WorkConnection } from '../types' -import { browseResolver } from '../resolvers' + +function browseQuery (connectionType, args) { + return { + type: connectionType, + args: { + ...forwardConnectionArgs, + ...args + }, + resolve: browseResolver() + } +} export default new GraphQLObjectType({ name: 'BrowseQuery', @@ -20,101 +32,47 @@ export default new GraphQLObjectType({ 'Browse requests are a direct lookup of all the entities directly linked ' + 'to another entity.', fields: { - artists: { - type: ArtistConnection, - 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: EventConnection, - args: { - limit: { type: GraphQLInt }, - offset: { type: GraphQLInt }, - area: { type: MBID }, - artist: { type: MBID }, - place: { type: MBID } - }, - resolve: browseResolver() - }, - labels: { - type: LabelConnection, - args: { - limit: { type: GraphQLInt }, - offset: { type: GraphQLInt }, - area: { type: MBID }, - release: { type: MBID } - }, - resolve: browseResolver() - }, - places: { - type: PlaceConnection, - args: { - limit: { type: GraphQLInt }, - offset: { type: GraphQLInt }, - area: { type: MBID } - }, - resolve: browseResolver() - }, - recordings: { - type: RecordingConnection, - args: { - limit: { type: GraphQLInt }, - offset: { type: GraphQLInt }, - artist: { type: MBID }, - release: { type: MBID } - }, - resolve: browseResolver() - }, - releases: { - type: ReleaseConnection, - 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: ReleaseGroupConnection, - args: { - limit: { type: GraphQLInt }, - offset: { type: GraphQLInt }, - artist: { type: MBID }, - release: { type: MBID } - }, - resolve: browseResolver() - }, - works: { - type: WorkConnection, - args: { - limit: { type: GraphQLInt }, - offset: { type: GraphQLInt }, - artist: { type: MBID } - }, - resolve: browseResolver() - }, - urls: { - type: URLConnection, - args: { - limit: { type: GraphQLInt }, - offset: { type: GraphQLInt }, - resource: { type: URLString } - }, - resolve: browseResolver() - } + artists: browseQuery(ArtistConnection, { + area: { type: MBID }, + recording: { type: MBID }, + release: { type: MBID }, + releaseGroup: { type: MBID }, + work: { type: MBID } + }), + events: browseQuery(EventConnection, { + area: { type: MBID }, + artist: { type: MBID }, + place: { type: MBID } + }), + labels: browseQuery(LabelConnection, { + area: { type: MBID }, + release: { type: MBID } + }), + places: browseQuery(PlaceConnection, { + area: { type: MBID } + }), + recordings: browseQuery(RecordingConnection, { + artist: { type: MBID }, + release: { type: MBID } + }), + releases: browseQuery(ReleaseConnection, { + area: { type: MBID }, + artist: { type: MBID }, + label: { type: MBID }, + track: { type: MBID }, + trackArtist: { type: MBID }, + recording: { type: MBID }, + releaseGroup: { type: MBID } + }), + releaseGroups: browseQuery(ReleaseGroupConnection, { + artist: { type: MBID }, + release: { type: MBID } + }), + works: browseQuery(WorkConnection, { + artist: { type: MBID } + }), + urls: browseQuery(URLConnection, { + resource: { type: URLString } + }) } }) diff --git a/src/queries/lookup.js b/src/queries/lookup.js index fa8022f..a1b8af7 100644 --- a/src/queries/lookup.js +++ b/src/queries/lookup.js @@ -1,4 +1,6 @@ import { GraphQLObjectType } from 'graphql' +import { lookupResolver } from '../resolvers' +import { mbid } from '../types/helpers' import { Area, Artist, @@ -9,10 +11,19 @@ import { Recording, Release, ReleaseGroup, + Series, URL, Work } from '../types' -import { lookupQuery } from '../types/helpers' + +function lookupQuery (entity) { + return { + type: entity, + description: `Look up a specific ${entity.name} by its MBID.`, + args: { mbid }, + resolve: lookupResolver() + } +} export default new GraphQLObjectType({ name: 'LookupQuery', @@ -29,6 +40,7 @@ export default new GraphQLObjectType({ recording: lookupQuery(Recording), release: lookupQuery(Release), releaseGroup: lookupQuery(ReleaseGroup), + series: lookupQuery(Series), url: lookupQuery(URL), work: lookupQuery(Work) } diff --git a/src/queries/search.js b/src/queries/search.js index 6e28f26..4ec9c55 100644 --- a/src/queries/search.js +++ b/src/queries/search.js @@ -1,4 +1,6 @@ -import { GraphQLObjectType } from 'graphql' +import { GraphQLObjectType, GraphQLNonNull, GraphQLString } from 'graphql' +import { forwardConnectionArgs } from 'graphql-relay' +import { searchResolver } from '../resolvers' import { AreaConnection, ArtistConnection, @@ -9,7 +11,17 @@ import { ReleaseGroupConnection, WorkConnection } from '../types' -import { searchQuery } from '../types/helpers' + +function searchQuery (connectionType) { + return { + type: connectionType, + args: { + query: { type: new GraphQLNonNull(GraphQLString) }, + ...forwardConnectionArgs + }, + resolve: searchResolver() + } +} export default new GraphQLObjectType({ name: 'SearchQuery', diff --git a/src/resolvers.js b/src/resolvers.js index 2bee11e..724858c 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -1,5 +1,9 @@ import { toEntityType } from './types/helpers' -import { getOffsetWithDefault, connectionFromArraySlice } from 'graphql-relay' +import { + getOffsetWithDefault, + connectionFromArray, + connectionFromArraySlice +} from 'graphql-relay' import { getFields, extendIncludes } from './util' export function includeRelations (params, info) { @@ -35,34 +39,63 @@ export function includeSubqueries (params, info) { return params } -export function lookupResolver (entityType, extraParams = {}) { - return (root, { id }, { lookupLoader }, info) => { - const params = includeRelations(extraParams, info) - entityType = entityType || toEntityType(info.fieldName) - return lookupLoader.load([entityType, id, params]) +export function lookupResolver () { + return (root, { mbid }, { lookupLoader }, info) => { + const entityType = toEntityType(info.fieldName) + const params = includeRelations({}, info) + return lookupLoader.load([entityType, mbid, params]) } } export function browseResolver () { - return (source, args, { browseLoader }, info) => { + return (source, { first = 25, after, ...args }, { browseLoader }, info) => { const pluralName = toEntityType(info.fieldName) let singularName = pluralName if (pluralName.endsWith('s')) { singularName = pluralName.slice(0, -1) } - const params = args - return browseLoader.load([singularName, params]) + const { type, types, status, statuses, ...moreParams } = args + let params = { + ...moreParams, + type: [], + status: [], + limit: first, + offset: getOffsetWithDefault(after, 0) + } + params = includeSubqueries(params, info) + params = includeRelations(params, info) + if (type) { + params.type.push(type) + } + if (types) { + params.type.push(...types) + } + if (status) { + params.status.push(status) + } + if (statuses) { + params.status.push(...statuses) + } + return browseLoader.load([singularName, params]).then(list => { + const { + [pluralName]: arraySlice, + [`${singularName}-offset`]: sliceStart, + [`${singularName}-count`]: arrayLength + } = list + const meta = { sliceStart, arrayLength } + return connectionFromArraySlice(arraySlice, { first, after }, meta) + }) } } export function searchResolver () { - return (source, args, { searchLoader }, info) => { + return (source, { first = 25, after, ...args }, { searchLoader }, info) => { const pluralName = toEntityType(info.fieldName) let singularName = pluralName if (pluralName.endsWith('s')) { singularName = pluralName.slice(0, -1) } - const { query, first, after, ...params } = args + const { query, ...params } = args params.limit = first params.offset = getOffsetWithDefault(after, 0) return searchLoader.load([singularName, query, params]).then(list => { @@ -78,61 +111,31 @@ export function searchResolver () { } export function relationResolver () { - return (source, { offset = 0, - limit, - direction, - type, - typeID }, { lookupLoader }, info) => { + return (source, args, context, info) => { const targetType = toEntityType(info.fieldName).replace('-', '_') - return source.filter(relation => { + const relations = source.filter(relation => { if (relation['target-type'] !== targetType) { return false } - if (direction != null && relation.direction !== direction) { + if (args.direction != null && relation.direction !== args.direction) { return false } - if (type != null && relation.type !== type) { + if (args.type != null && relation.type !== args.type) { return false } - if (typeID != null && relation['type-id'] !== typeID) { + if (args.typeID != null && relation['type-id'] !== args.typeID) { return false } return true - }).slice(offset, limit == null ? undefined : offset + limit) + }) + return connectionFromArray(relations, args) } } export function linkedResolver () { - return (source, args, { browseLoader }, info) => { - const pluralName = toEntityType(info.fieldName) - let singularName = pluralName - if (pluralName.endsWith('s')) { - singularName = pluralName.slice(0, -1) - } + return (source, args, context, info) => { const parentEntity = toEntityType(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] - }) + args = { ...args, [parentEntity]: source.id } + return browseResolver()(source, args, context, info) } } diff --git a/src/types/helpers.js b/src/types/helpers.js index 51f6ad4..2091ecb 100644 --- a/src/types/helpers.js +++ b/src/types/helpers.js @@ -3,44 +3,36 @@ import pascalCase from 'pascalcase' import { GraphQLObjectType, GraphQLString, - GraphQLInt, GraphQLList, GraphQLNonNull } from 'graphql' -import { globalIdField, forwardConnectionArgs } from 'graphql-relay' +import { + globalIdField, + connectionArgs, + forwardConnectionArgs +} from 'graphql-relay' import { MBID } 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 { ArtistConnection } from './artist' +import { EventConnection } from './event' +import { LabelConnection } from './label' import LifeSpan from './life-span' -import Place from './place' -import Recording from './recording' +import { PlaceConnection } from './place' +import { RecordingConnection } from './recording' import Relation from './relation' -import Release from './release' -import ReleaseGroup from './release-group' -import Work from './work' +import { ReleaseConnection } from './release' +import { ReleaseGroupConnection } from './release-group' +import { WorkConnection } from './work' import { - lookupResolver, linkedResolver, relationResolver, - searchResolver, includeRelations } from '../resolvers' export const toNodeType = pascalCase export const toEntityType = dashify -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 = {}) { config = { type: GraphQLString, @@ -76,29 +68,10 @@ export function getFallback (keys) { } } -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 (connectionType) { - return { - type: connectionType, - args: { - query: { type: new GraphQLNonNull(GraphQLString) }, - ...forwardConnectionArgs - }, - resolve: searchResolver() - } -} - export const id = globalIdField() export const mbid = { type: new GraphQLNonNull(MBID), + description: 'The MBID of the entity.', resolve: source => source.id } export const name = { type: GraphQLString } @@ -107,11 +80,21 @@ export const title = { type: GraphQLString } export const disambiguation = { type: GraphQLString } export const lifeSpan = { type: LifeSpan, resolve: getHyphenated } +function linkedQuery (connectionType, args) { + return { + type: connectionType, + args: { + ...forwardConnectionArgs, + ...args + }, + resolve: linkedResolver() + } +} + export const relation = { type: new GraphQLList(Relation), args: { - limit: { type: GraphQLInt }, - offset: { type: GraphQLInt }, + ...connectionArgs, direction: { type: GraphQLString }, type: { type: GraphQLString }, typeID: { type: MBID } @@ -141,7 +124,7 @@ export const relations = { if (source.relations != null) { return source.relations } - const entityType = dashify(info.parentType.name) + const entityType = toEntityType(info.parentType.name) const id = source.id const params = includeRelations({}, info) return lookupLoader.load([entityType, id, params]).then(entity => { @@ -166,80 +149,22 @@ export const artistCredit = { } } -export const artists = { - type: new GraphQLList(Artist), - args: { - limit: { type: GraphQLInt }, - offset: { type: GraphQLInt } - }, - resolve: linkedResolver() -} +export const artists = linkedQuery(ArtistConnection) +export const events = linkedQuery(EventConnection) +export const labels = linkedQuery(LabelConnection) +export const places = linkedQuery(PlaceConnection) +export const recordings = linkedQuery(RecordingConnection) -export const events = { - type: new GraphQLList(Event), - args: { - limit: { type: GraphQLInt }, - offset: { type: GraphQLInt } - }, - resolve: linkedResolver() -} +export const releases = linkedQuery(ReleaseConnection, { + type: { type: ReleaseGroupType }, + types: { type: new GraphQLList(ReleaseGroupType) }, + status: { type: ReleaseStatus }, + statuses: { type: new GraphQLList(ReleaseStatus) } +}) -export const labels = { - type: new GraphQLList(Label), - args: { - limit: { type: GraphQLInt }, - offset: { type: GraphQLInt } - }, - resolve: linkedResolver() -} +export const releaseGroups = linkedQuery(ReleaseGroupConnection, { + type: { type: ReleaseGroupType }, + types: { type: new GraphQLList(ReleaseGroupType) } +}) -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() -} +export const works = linkedQuery(WorkConnection)