diff --git a/README.md b/README.md index ed42e9f..015162b 100644 --- a/README.md +++ b/README.md @@ -324,9 +324,13 @@ schema. The schema was originally designed to be more user-friendly, but in the end I decided that being compatible with Relay was a worthwhile feature. I agree, it’s ugly. -Don’t forget, though, that you can use [GraphQL aliases][aliases] to rename -fields to your liking. For example, the following query renames `edges`, `node`, -and `mbid` to `results`, `releaseGroup`, and `id`, respectively: +The GraphBrainz schema includes an extra `nodes` field on every connection type. +If you only want the nodes and no other fields on `edges`, you can use `nodes` +as a shortcut. + +Don’t forget that you can also use [GraphQL aliases][aliases] to rename fields +to your liking. For example, the following query renames `edges`, `node`, and +`mbid` to `results`, `releaseGroup`, and `id`, respectively: ```graphql query ChristmasAlbums { diff --git a/docs/schema.md b/docs/schema.md index d2da91e..c8e897e 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -99,6 +99,10 @@ type AreaConnection { # A list of edges. edges: [AreaEdge] + # A list of nodes in the connection (without going through the + # `edges` field). + nodes: [Area] + # A count of the total number of items in this connection, # ignoring pagination. totalCount: Int @@ -233,6 +237,10 @@ type ArtistConnection { # A list of edges. edges: [ArtistEdge] + # A list of nodes in the connection (without going through the + # `edges` field). + nodes: [Artist] + # A count of the total number of items in this connection, # ignoring pagination. totalCount: Int @@ -557,6 +565,10 @@ type CollectionConnection { # A list of edges. edges: [CollectionEdge] + # A list of nodes in the connection (without going through the + # `edges` field). + nodes: [Collection] + # A count of the total number of items in this connection, # ignoring pagination. totalCount: Int @@ -755,6 +767,10 @@ type EventConnection { # A list of edges. edges: [EventEdge] + # A list of nodes in the connection (without going through the + # `edges` field). + nodes: [Event] + # A count of the total number of items in this connection, # ignoring pagination. totalCount: Int @@ -824,6 +840,10 @@ type InstrumentConnection { # A list of edges. edges: [InstrumentEdge] + # A list of nodes in the connection (without going through the + # `edges` field). + nodes: [Instrument] + # A count of the total number of items in this connection, # ignoring pagination. totalCount: Int @@ -952,6 +972,10 @@ type LabelConnection { # A list of edges. edges: [LabelEdge] + # A list of nodes in the connection (without going through the + # `edges` field). + nodes: [Label] + # A count of the total number of items in this connection, # ignoring pagination. totalCount: Int @@ -1193,6 +1217,10 @@ type PlaceConnection { # A list of edges. edges: [PlaceEdge] + # A list of nodes in the connection (without going through the + # `edges` field). + nodes: [Place] + # A count of the total number of items in this connection, # ignoring pagination. totalCount: Int @@ -1323,6 +1351,10 @@ type RecordingConnection { # A list of edges. edges: [RecordingEdge] + # A list of nodes in the connection (without going through the + # `edges` field). + nodes: [Recording] + # A count of the total number of items in this connection, # ignoring pagination. totalCount: Int @@ -1393,6 +1425,10 @@ type RelationshipConnection { # A list of edges. edges: [RelationshipEdge] + # A list of nodes in the connection (without going through the + # `edges` field). + nodes: [Relationship] + # A count of the total number of items in this connection, # ignoring pagination. totalCount: Int @@ -1730,6 +1766,10 @@ type ReleaseConnection { # A list of edges. edges: [ReleaseEdge] + # A list of nodes in the connection (without going through the + # `edges` field). + nodes: [Release] + # A count of the total number of items in this connection, # ignoring pagination. totalCount: Int @@ -1897,6 +1937,10 @@ type ReleaseGroupConnection { # A list of edges. edges: [ReleaseGroupEdge] + # A list of nodes in the connection (without going through the + # `edges` field). + nodes: [ReleaseGroup] + # A count of the total number of items in this connection, # ignoring pagination. totalCount: Int @@ -2191,6 +2235,10 @@ type SeriesConnection { # A list of edges. edges: [SeriesEdge] + # A list of nodes in the connection (without going through the + # `edges` field). + nodes: [Series] + # A count of the total number of items in this connection, # ignoring pagination. totalCount: Int @@ -2228,6 +2276,10 @@ type TagConnection { # A list of edges. edges: [TagEdge] + # A list of nodes in the connection (without going through the + # `edges` field). + nodes: [Tag] + # A count of the total number of items in this connection, # ignoring pagination. totalCount: Int @@ -2327,6 +2379,10 @@ type WorkConnection { # A list of edges. edges: [WorkEdge] + # A list of nodes in the connection (without going through the + # `edges` field). + nodes: [Work] + # A count of the total number of items in this connection, # ignoring pagination. totalCount: Int diff --git a/docs/types.md b/docs/types.md index d59a287..b441519 100644 --- a/docs/types.md +++ b/docs/types.md @@ -514,6 +514,16 @@ Information to aid in pagination. A list of edges. + + + +nodes +[Area] + + +A list of nodes in the connection (without going through the +`edges` field). + @@ -950,6 +960,16 @@ Information to aid in pagination. A list of edges. + + + +nodes +[Artist] + + +A list of nodes in the connection (without going through the +`edges` field). + @@ -2053,6 +2073,16 @@ Information to aid in pagination. A list of edges. + + + +nodes +[Collection] + + +A list of nodes in the connection (without going through the +`edges` field). + @@ -2576,6 +2606,16 @@ Information to aid in pagination. A list of edges. + + + +nodes +[Event] + + +A list of nodes in the connection (without going through the +`edges` field). + @@ -2809,6 +2849,16 @@ Information to aid in pagination. A list of edges. + + + +nodes +[Instrument] + + +A list of nodes in the connection (without going through the +`edges` field). + @@ -3136,6 +3186,16 @@ Information to aid in pagination. A list of edges. + + + +nodes +[Label] + + +A list of nodes in the connection (without going through the +`edges` field). + @@ -3870,6 +3930,16 @@ Information to aid in pagination. A list of edges. + + + +nodes +[Place] + + +A list of nodes in the connection (without going through the +`edges` field). + @@ -4241,6 +4311,16 @@ Information to aid in pagination. A list of edges. + + + +nodes +[Recording] + + +A list of nodes in the connection (without going through the +`edges` field). + @@ -4455,6 +4535,16 @@ Information to aid in pagination. A list of edges. + + + +nodes +[Relationship] + + +A list of nodes in the connection (without going through the +`edges` field). + @@ -5594,6 +5684,16 @@ Information to aid in pagination. A list of edges. + + + +nodes +[Release] + + +A list of nodes in the connection (without going through the +`edges` field). + @@ -6102,6 +6202,16 @@ Information to aid in pagination. A list of edges. + + + +nodes +[ReleaseGroup] + + +A list of nodes in the connection (without going through the +`edges` field). + @@ -6722,6 +6832,16 @@ Information to aid in pagination. A list of edges. + + + +nodes +[Series] + + +A list of nodes in the connection (without going through the +`edges` field). + @@ -6849,6 +6969,16 @@ Information to aid in pagination. A list of edges. + + + +nodes +[Tag] + + +A list of nodes in the connection (without going through the +`edges` field). + @@ -7172,6 +7302,16 @@ Information to aid in pagination. A list of edges. + + + +nodes +[Work] + + +A list of nodes in the connection (without going through the +`edges` field). + diff --git a/schema.json b/schema.json index 6429802..bea27de 100644 --- a/schema.json +++ b/schema.json @@ -1239,6 +1239,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nodes", + "description": "A list of nodes in the connection (without going through the\n`edges` field).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Artist", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "totalCount", "description": "A count of the total number of items in this connection,\nignoring pagination.", @@ -1999,6 +2015,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nodes", + "description": "A list of nodes in the connection (without going through the\n`edges` field).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Recording", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "totalCount", "description": "A count of the total number of items in this connection,\nignoring pagination.", @@ -2664,6 +2696,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nodes", + "description": "A list of nodes in the connection (without going through the\n`edges` field).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Release", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "totalCount", "description": "A count of the total number of items in this connection,\nignoring pagination.", @@ -3898,6 +3946,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nodes", + "description": "A list of nodes in the connection (without going through the\n`edges` field).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Label", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "totalCount", "description": "A count of the total number of items in this connection,\nignoring pagination.", @@ -5356,6 +5420,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nodes", + "description": "A list of nodes in the connection (without going through the\n`edges` field).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Relationship", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "totalCount", "description": "A count of the total number of items in this connection,\nignoring pagination.", @@ -5621,6 +5701,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nodes", + "description": "A list of nodes in the connection (without going through the\n`edges` field).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Collection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "totalCount", "description": "A count of the total number of items in this connection,\nignoring pagination.", @@ -6254,6 +6350,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nodes", + "description": "A list of nodes in the connection (without going through the\n`edges` field).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Area", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "totalCount", "description": "A count of the total number of items in this connection,\nignoring pagination.", @@ -6360,6 +6472,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nodes", + "description": "A list of nodes in the connection (without going through the\n`edges` field).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Event", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "totalCount", "description": "A count of the total number of items in this connection,\nignoring pagination.", @@ -6781,6 +6909,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nodes", + "description": "A list of nodes in the connection (without going through the\n`edges` field).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Tag", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "totalCount", "description": "A count of the total number of items in this connection,\nignoring pagination.", @@ -6926,6 +7070,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nodes", + "description": "A list of nodes in the connection (without going through the\n`edges` field).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Instrument", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "totalCount", "description": "A count of the total number of items in this connection,\nignoring pagination.", @@ -7240,6 +7400,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nodes", + "description": "A list of nodes in the connection (without going through the\n`edges` field).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Place", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "totalCount", "description": "A count of the total number of items in this connection,\nignoring pagination.", @@ -7668,6 +7844,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nodes", + "description": "A list of nodes in the connection (without going through the\n`edges` field).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "ReleaseGroup", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "totalCount", "description": "A count of the total number of items in this connection,\nignoring pagination.", @@ -8246,6 +8438,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nodes", + "description": "A list of nodes in the connection (without going through the\n`edges` field).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Series", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "totalCount", "description": "A count of the total number of items in this connection,\nignoring pagination.", @@ -8532,6 +8740,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nodes", + "description": "A list of nodes in the connection (without going through the\n`edges` field).", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Work", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "totalCount", "description": "A count of the total number of items in this connection,\nignoring pagination.", diff --git a/src/resolvers.js b/src/resolvers.js index 0012aa4..9a5c55a 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -127,9 +127,11 @@ export function resolveBrowse (root, { [`${singularName}-count`]: arrayLength = arraySlice.length } = list const meta = { sliceStart, arrayLength } + const connection = connectionFromArraySlice(arraySlice, { first, after }, meta) return { + nodes: connection.edges.map(edge => edge.node), totalCount: arrayLength, - ...connectionFromArraySlice(arraySlice, { first, after }, meta) + ...connection } }) } @@ -155,14 +157,17 @@ export function resolveSearch (root, { count: arrayLength } = list const meta = { sliceStart, arrayLength } - const connection = { - totalCount: arrayLength, - ...connectionFromArraySlice(arraySlice, { first, after }, meta) - } + const connection = connectionFromArraySlice(arraySlice, { first, after }, meta) // Move the `score` field up to the edge object and make sure it's a // number (MusicBrainz returns a string). - connection.edges.forEach(edge => { edge.score = +edge.node.score }) - return connection + const edges = connection.edges.map(edge => ({ ...edge, score: +edge.node.score })) + const connectionWithExtras = { + nodes: edges.map(edge => edge.node), + totalCount: arrayLength, + ...connection, + edges + } + return connectionWithExtras }) } @@ -179,9 +184,11 @@ export function resolveRelationship (rels, args, context, info) { if (args.typeID != null) { matches = matches.filter(rel => rel['type-id'] === args.typeID) } + const connection = connectionFromArray(matches, args) return { + nodes: connection.edges.map(edge => edge.node), totalCount: matches.length, - ...connectionFromArray(matches, args) + ...connection } } @@ -214,9 +221,11 @@ export function createSubqueryResolver ({ inc, key } = {}, handler = value => va export function resolveDiscReleases (disc, args, context, info) { const { releases } = disc if (releases != null) { + const connection = connectionFromArray(releases, args) return { + nodes: connection.edges.map(edge => edge.node), totalCount: releases.length, - ...connectionFromArray(releases, args) + ...connection } } args = { ...args, discID: disc.id } diff --git a/src/types/helpers.js b/src/types/helpers.js index e2b37fa..f8a8131 100644 --- a/src/types/helpers.js +++ b/src/types/helpers.js @@ -263,10 +263,14 @@ export const releaseGroups = linkedQuery(ReleaseGroupConnection, { }) export const series = linkedQuery(SeriesConnection) export const tags = linkedQuery(TagConnection, { - resolve: createSubqueryResolver({}, (value = [], args) => ({ - totalCount: value.length, - ...connectionFromArray(value, args) - })) + resolve: createSubqueryResolver({}, (value = [], args) => { + const connection = connectionFromArray(value, args) + return { + nodes: connection.edges.map(edge => edge.node), + totalCount: value.length, + ...connection + } + }) }) export const works = linkedQuery(WorkConnection) @@ -285,7 +289,14 @@ these results were found through a search.` export function connectionWithExtras (nodeType) { return connectionDefinitions({ nodeType, - connectionFields: () => ({ totalCount }), + connectionFields: () => ({ + nodes: { + type: new GraphQLList(nodeType), + description: `A list of nodes in the connection (without going through the +\`edges\` field).` + }, + totalCount + }), edgeFields: () => ({ score }) }).connectionType } diff --git a/test/fixtures/6143276fc750d9f7ad6a6a2f9fb2e025 b/test/fixtures/6143276fc750d9f7ad6a6a2f9fb2e025 new file mode 100644 index 0000000..ed34ee9 Binary files /dev/null and b/test/fixtures/6143276fc750d9f7ad6a6a2f9fb2e025 differ diff --git a/test/fixtures/6143276fc750d9f7ad6a6a2f9fb2e025.headers b/test/fixtures/6143276fc750d9f7ad6a6a2f9fb2e025.headers new file mode 100644 index 0000000..a34a294 --- /dev/null +++ b/test/fixtures/6143276fc750d9f7ad6a6a2f9fb2e025.headers @@ -0,0 +1,29 @@ +{ + "statusCode": 200, + "headers": { + "date": "Sat, 07 Oct 2017 02:16:50 GMT", + "content-type": "application/json; charset=utf-8", + "transfer-encoding": "chunked", + "connection": "keep-alive", + "keep-alive": "timeout=15", + "vary": "Accept-Encoding", + "x-ratelimit-limit": "1200", + "x-ratelimit-remaining": "1052", + "x-ratelimit-reset": "1507342611", + "server": "Plack::Handler::Starlet", + "etag": "W/\"511ce70a5d7fc09685033f0a9cc9363f\"", + "access-control-allow-origin": "*", + "content-encoding": "gzip" + }, + "url": "http://musicbrainz.org:80/ws/2/artist/b10bbbfc-cf9e-42e0-be17-e2c3e1d2600d?inc=artist-rels&fmt=json", + "time": 402, + "request": { + "method": "GET", + "headers": { + "User-Agent": "graphbrainz/6.0.0 ( https://github.com/exogen/graphbrainz )", + "host": "musicbrainz.org", + "accept-encoding": "gzip, deflate", + "accept": "application/json" + } + } +} \ No newline at end of file diff --git a/test/fixtures/77534a6e7fb7ddb83df8a7c91f2ebcc4 b/test/fixtures/77534a6e7fb7ddb83df8a7c91f2ebcc4 new file mode 100644 index 0000000..7814234 Binary files /dev/null and b/test/fixtures/77534a6e7fb7ddb83df8a7c91f2ebcc4 differ diff --git a/test/fixtures/77534a6e7fb7ddb83df8a7c91f2ebcc4.headers b/test/fixtures/77534a6e7fb7ddb83df8a7c91f2ebcc4.headers new file mode 100644 index 0000000..57f7fee --- /dev/null +++ b/test/fixtures/77534a6e7fb7ddb83df8a7c91f2ebcc4.headers @@ -0,0 +1,29 @@ +{ + "statusCode": 200, + "headers": { + "date": "Sat, 07 Oct 2017 02:16:49 GMT", + "content-type": "application/json; charset=utf-8", + "transfer-encoding": "chunked", + "connection": "keep-alive", + "keep-alive": "timeout=15", + "vary": "Accept-Encoding", + "x-ratelimit-limit": "1200", + "x-ratelimit-remaining": "755", + "x-ratelimit-reset": "1507342609", + "server": "Plack::Handler::Starlet", + "etag": "W/\"dfb1258ab1119ada8197e91d3da43002\"", + "access-control-allow-origin": "*", + "content-encoding": "gzip" + }, + "url": "http://musicbrainz.org:80/ws/2/artist?release=2ac3cbf2-f0d0-3678-af5f-b62dcb051bc0&fmt=json", + "time": 659, + "request": { + "method": "GET", + "headers": { + "User-Agent": "graphbrainz/6.0.0 ( https://github.com/exogen/graphbrainz )", + "host": "musicbrainz.org", + "accept-encoding": "gzip, deflate", + "accept": "application/json" + } + } +} \ No newline at end of file diff --git a/test/schema.js b/test/schema.js index 8c79db5..fffb6ef 100644 --- a/test/schema.js +++ b/test/schema.js @@ -190,6 +190,70 @@ test('supports deeply nested queries', testData, ` t.is(labels.edges[0].node.releases.edges.length, 1) }) +test('connections have a nodes shortcut field', testData, ` + query AppleRecordsMarriages { + search { + labels(query: "Apple Records", first: 1) { + nodes { + name + disambiguation + country + releases(first: 1) { + nodes { + title + date + artists { + nodes { + name + ...bandMembers + } + } + } + } + } + } + } + } + + fragment bandMembers on Artist { + relationships { + artists(direction: "backward", type: "member of band") { + nodes { + type + target { + ... on Artist { + name + ...marriages + } + } + } + } + } + } + + fragment marriages on Artist { + relationships { + artists(type: "married") { + nodes { + type + direction + begin + end + target { + ... on Artist { + name + } + } + } + } + } + } +`, (t, data) => { + const { labels } = data.search + t.true(labels.nodes.length > 0) + t.is(labels.nodes[0].releases.nodes.length, 1) +}) + // FIXME: https://github.com/graphql/graphql-js/issues/910 test('throws an error if given a malformed MBID', testThrows, ` {