From edb10c6d1a8b0b324d84264c17105f7cb08b3859 Mon Sep 17 00:00:00 2001 From: DebaucheryLibrarian Date: Fri, 30 Aug 2024 02:28:44 +0200 Subject: [PATCH] Expanded GraphQL API with scenes entities and actors. --- package-lock.json | 71 ++++++++++++++++++++++++++++++ package.json | 1 + src/actors.js | 11 ++--- src/entities.js | 3 +- src/web/actors.js | 103 +++++++++++++++++++++++++++++++++++++++++++- src/web/entities.js | 35 ++++++++++++--- src/web/graphql.js | 29 ++++++++++++- src/web/scenes.js | 31 +++++-------- 8 files changed, 250 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 43c3268..90b1626 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "express-session": "^1.18.0", "floating-vue": "^5.2.2", "graphql": "^16.9.0", + "graphql-parse-resolve-info": "^4.13.0", "ip-cidr": "^4.0.0", "js-cookie": "^3.0.5", "knex": "^3.1.0", @@ -6319,6 +6320,42 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/graphql-parse-resolve-info": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/graphql-parse-resolve-info/-/graphql-parse-resolve-info-4.13.0.tgz", + "integrity": "sha512-VVJ1DdHYcR7hwOGQKNH+QTzuNgsLA8l/y436HtP9YHoX6nmwXRWq3xWthU3autMysXdm0fQUbhTZCx0W9ICozw==", + "dependencies": { + "debug": "^4.1.1", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=8.6" + }, + "peerDependencies": { + "graphql": ">=0.9 <0.14 || ^14.0.2 || ^15.4.0 || ^16.3.0" + } + }, + "node_modules/graphql-parse-resolve-info/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/graphql-parse-resolve-info/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -9446,6 +9483,11 @@ "json5": "lib/cli.js" } }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -14891,6 +14933,30 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==" }, + "graphql-parse-resolve-info": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/graphql-parse-resolve-info/-/graphql-parse-resolve-info-4.13.0.tgz", + "integrity": "sha512-VVJ1DdHYcR7hwOGQKNH+QTzuNgsLA8l/y436HtP9YHoX6nmwXRWq3xWthU3autMysXdm0fQUbhTZCx0W9ICozw==", + "requires": { + "debug": "^4.1.1", + "tslib": "^2.0.1" + }, + "dependencies": { + "debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -17114,6 +17180,11 @@ } } }, + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index 406045f..ca3819e 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "express-session": "^1.18.0", "floating-vue": "^5.2.2", "graphql": "^16.9.0", + "graphql-parse-resolve-info": "^4.13.0", "ip-cidr": "^4.0.0", "js-cookie": "^3.0.5", "knex": "^3.1.0", diff --git a/src/actors.js b/src/actors.js index 5ffc8a4..4e6e54e 100644 --- a/src/actors.js +++ b/src/actors.js @@ -69,6 +69,7 @@ export function curateActor(actor, context = {}) { })), createdAt: actor.created_at, updatedAt: actor.updated_at, + scenes: actor.scenes, likes: actor.stashed, stashes: context.stashes?.map((stash) => curateStash(stash)) || [], ...context.append?.[actor.id], @@ -163,16 +164,16 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) { return curatedActors; } -function curateOptions(options) { - if (options?.limit > 120) { +function curateOptions(options = {}) { + if (options.limit > 120) { throw new HttpError('Limit must be <= 120', 400); } return { - page: options?.page || 1, - limit: options?.limit || 30, + page: options.page || 1, + limit: options.limit || 30, aggregateCountries: true, - requireAvatar: options?.requireAvatar || false, + requireAvatar: options.requireAvatar || false, order: [escape(options.order?.[0]) || 'name', escape(options.order?.[1]) || 'asc'], }; } diff --git a/src/entities.js b/src/entities.js index c1029af..4acb301 100644 --- a/src/entities.js +++ b/src/entities.js @@ -35,7 +35,7 @@ export function curateEntity(entity, context) { }; } -export async function fetchEntities(options) { +export async function fetchEntities(options = {}) { const entities = await knex('entities') .select('entities.*', knex.raw('row_to_json(parents) as parent')) .modify((builder) => { @@ -74,6 +74,7 @@ export async function fetchEntities(options) { }) .leftJoin('entities as parents', 'parents.id', 'entities.parent_id') .orderBy(...(options.order || ['name', 'asc'])) + .offset((options.page - 1) * options.limit) .limit(options.limit || 1000); return entities.map((entityEntry) => curateEntity(entityEntry)); diff --git a/src/web/actors.js b/src/web/actors.js index dcb1dfd..be3e98f 100644 --- a/src/web/actors.js +++ b/src/web/actors.js @@ -1,4 +1,7 @@ -import { fetchActors } from '../actors.js'; +import { + fetchActors, + fetchActorsById, +} from '../actors.js'; export function curateActorsQuery(query) { return { @@ -36,3 +39,101 @@ export async function fetchActorsApi(req, res) { total, }); } + +export const actorsSchema = ` + extend type Query { + actors( + query: String + limit: Int! = 30 + page: Int! = 1 + order: [String] + ): ActorsResult + + actor( + id: Int! + ): Actor + + actorsById( + ids: [Int]! + ): [Actor] + } + + type Country { + alpha2: String + name: String + } + + type Location { + country: Country + city: String + state: String + } + + type ActorsResult { + nodes: [Actor] + total: Int + } + + type Actor { + id: Int! + name: String + slug: String + gender: String + dateOfBirth: Date + age: Int + origin: Location + residence: Location + height: Int + bust: String + hip: Int + waist: Int + naturalBoobs: Boolean + eyes: String + hairColor: String + hasPiercings: Boolean + hasTattoos: Boolean + tattoos: String + piercings: String + scenes: Int + likes: Int + } +`; + +function curateGraphqlActor(actor) { + return { + ...actor, + age: actor.ageFromBirth, + height: actor.height?.metric, + weight: actor.weight?.metric, + }; +} + +export async function fetchActorsGraphql(query, _req) { + const { + actors, + total, + } = await fetchActors(query, { + limit: query.limit, + page: query.page, + order: query.order, + aggregateCountries: false, + }); + + return { + nodes: actors.map((actor) => curateGraphqlActor(actor)), + total, + }; +} + +export async function fetchActorsByIdGraphql(query, _req, _info) { + const actors = await fetchActorsById([].concat(query.id, query.ids).filter(Boolean)); + const curatedActors = actors.map((actor) => curateGraphqlActor(actor)); + + console.log(actors); + + if (query.ids) { + return curatedActors; + } + + return curatedActors[0]; +} diff --git a/src/web/entities.js b/src/web/entities.js index d4c8c72..5a1d8ae 100644 --- a/src/web/entities.js +++ b/src/web/entities.js @@ -1,8 +1,12 @@ +import { parseResolveInfo } from 'graphql-parse-resolve-info'; + import { fetchEntities, fetchEntitiesById, } from '../entities.js'; +import { getIdsBySlug } from '../cache.js'; + export async function fetchEntitiesApi(req, res) { const entities = await fetchEntities(req.query); @@ -13,25 +17,37 @@ export const entitiesSchema = ` extend type Query { entities( query: String + type: String + order: [String] limit: Int! = 30 page: Int! = 1 ): EntitiesResult entity( - id: Int! + slug: String! ): Entity + + entitiesBySlug( + slugs: [String]! + ): [Entity] + + entitiesById( + ids: [Int]! + ): [Entity] } type EntitiesResult { nodes: [Entity] - total: Int } type Entity { id: Int! name: String slug: String + url: String + type: String parent: Entity + children: [Entity] } `; @@ -43,8 +59,17 @@ export async function fetchEntitiesGraphql(query, _req) { }; } -export async function fetchEntitiesByIdGraphql(query, _req) { - const [entity] = await fetchEntitiesById([query.id]); +export async function fetchEntitiesByIdGraphql(query, req, info) { + const entityIds = query.ids || await getIdsBySlug([].concat(query.slug, query.slugs).filter(Boolean), 'entities'); + const parsedContext = parseResolveInfo(info); - return entity; + const entities = await fetchEntitiesById(entityIds, { + includeChildren: Object.hasOwn(parsedContext.fieldsByTypeName.Entity, 'children'), + }); + + if (query.slugs || query.ids) { + return entities; + } + + return entities[0]; } diff --git a/src/web/graphql.js b/src/web/graphql.js index fcaa0d9..13b7885 100644 --- a/src/web/graphql.js +++ b/src/web/graphql.js @@ -1,3 +1,5 @@ +import { format } from 'date-fns'; + import { graphql, buildSchema, @@ -16,6 +18,12 @@ import { fetchEntitiesByIdGraphql, } from './entities.js'; +import { + actorsSchema, + fetchActorsGraphql, + fetchActorsByIdGraphql, +} from './actors.js'; + const schema = buildSchema(` type Query { movies( @@ -26,6 +34,7 @@ const schema = buildSchema(` scalar Date ${scenesSchema} + ${actorsSchema} ${entitiesSchema} `); @@ -40,6 +49,17 @@ const DateTimeScalar = new GraphQLScalarType({ }, }); +const DateScalar = new GraphQLScalarType({ + name: 'Date', + serialize(value) { + if (value instanceof Date) { + return format(value, 'yyyy-MM-dd'); + } + + return value; + }, +}); + export async function graphqlApi(req, res) { const data = await graphql({ schema, @@ -47,12 +67,19 @@ export async function graphqlApi(req, res) { variableValues: req.body.variables, resolvers: { DateTimeScalar, + DateScalar, }, rootValue: { scenes: async (query) => fetchScenesGraphql(query, req), scene: async (query) => fetchScenesByIdGraphql(query, req), + scenesById: async (query) => fetchScenesByIdGraphql(query, req), + actors: async (query) => fetchActorsGraphql(query, req), + actor: async (query, args, info) => fetchActorsByIdGraphql(query, req, info), + actorsById: async (query, args, info) => fetchActorsByIdGraphql(query, req, info), entities: async (query) => fetchEntitiesGraphql(query, req), - entity: async (query) => fetchEntitiesByIdGraphql(query, req), + entity: async (query, args, info) => fetchEntitiesByIdGraphql(query, req, info), + entitiesBySlug: async (query, args, info) => fetchEntitiesByIdGraphql(query, req, info), + entitiesById: async (query, args, info) => fetchEntitiesByIdGraphql(query, req, info), }, }); diff --git a/src/web/scenes.js b/src/web/scenes.js index 3f10134..1339846 100644 --- a/src/web/scenes.js +++ b/src/web/scenes.js @@ -85,6 +85,10 @@ export const scenesSchema = ` scene( id: Int! ): Release + + scenesById( + ids: [Int]! + ): [Release] } type ReleasesAggregate { @@ -115,12 +119,6 @@ export const scenesSchema = ` movies: [Release] } - type Actor { - id: Int! - name: String - slug: String - } - type Tag { id: Int! name: String @@ -184,8 +182,6 @@ export async function fetchScenesGraphql(query, req) { aggregate: false, }, req.user); - console.log(query); - return { nodes: scenes, total, @@ -197,24 +193,17 @@ export async function fetchScenesGraphql(query, req) { }, */ }; - - /* - return { - scenes, - aggActors, - aggTags, - aggChannels, - limit, - total, - }; - */ } export async function fetchScenesByIdGraphql(query, req) { - const [scene] = await fetchScenesById([query.id], { + const scenes = await fetchScenesById([].concat(query.id, query.ids).filter(Boolean), { reqUser: req.user, includePartOf: true, }); - return scene; + if (query.ids) { + return scenes; + } + + return scenes[0]; }