Expanded GraphQL API with scenes entities and actors.

This commit is contained in:
DebaucheryLibrarian 2024-08-30 02:28:44 +02:00
parent 706ccf1ab3
commit edb10c6d1a
8 changed files with 250 additions and 34 deletions

71
package-lock.json generated
View File

@ -36,6 +36,7 @@
"express-session": "^1.18.0", "express-session": "^1.18.0",
"floating-vue": "^5.2.2", "floating-vue": "^5.2.2",
"graphql": "^16.9.0", "graphql": "^16.9.0",
"graphql-parse-resolve-info": "^4.13.0",
"ip-cidr": "^4.0.0", "ip-cidr": "^4.0.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"knex": "^3.1.0", "knex": "^3.1.0",
@ -6319,6 +6320,42 @@
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" "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": { "node_modules/has-bigints": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
@ -9446,6 +9483,11 @@
"json5": "lib/cli.js" "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": { "node_modules/tunnel-agent": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "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", "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz",
"integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==" "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": { "has-bigints": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", "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": { "tunnel-agent": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",

View File

@ -36,6 +36,7 @@
"express-session": "^1.18.0", "express-session": "^1.18.0",
"floating-vue": "^5.2.2", "floating-vue": "^5.2.2",
"graphql": "^16.9.0", "graphql": "^16.9.0",
"graphql-parse-resolve-info": "^4.13.0",
"ip-cidr": "^4.0.0", "ip-cidr": "^4.0.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"knex": "^3.1.0", "knex": "^3.1.0",

View File

@ -69,6 +69,7 @@ export function curateActor(actor, context = {}) {
})), })),
createdAt: actor.created_at, createdAt: actor.created_at,
updatedAt: actor.updated_at, updatedAt: actor.updated_at,
scenes: actor.scenes,
likes: actor.stashed, likes: actor.stashed,
stashes: context.stashes?.map((stash) => curateStash(stash)) || [], stashes: context.stashes?.map((stash) => curateStash(stash)) || [],
...context.append?.[actor.id], ...context.append?.[actor.id],
@ -163,16 +164,16 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
return curatedActors; return curatedActors;
} }
function curateOptions(options) { function curateOptions(options = {}) {
if (options?.limit > 120) { if (options.limit > 120) {
throw new HttpError('Limit must be <= 120', 400); throw new HttpError('Limit must be <= 120', 400);
} }
return { return {
page: options?.page || 1, page: options.page || 1,
limit: options?.limit || 30, limit: options.limit || 30,
aggregateCountries: true, aggregateCountries: true,
requireAvatar: options?.requireAvatar || false, requireAvatar: options.requireAvatar || false,
order: [escape(options.order?.[0]) || 'name', escape(options.order?.[1]) || 'asc'], order: [escape(options.order?.[0]) || 'name', escape(options.order?.[1]) || 'asc'],
}; };
} }

View File

@ -35,7 +35,7 @@ export function curateEntity(entity, context) {
}; };
} }
export async function fetchEntities(options) { export async function fetchEntities(options = {}) {
const entities = await knex('entities') const entities = await knex('entities')
.select('entities.*', knex.raw('row_to_json(parents) as parent')) .select('entities.*', knex.raw('row_to_json(parents) as parent'))
.modify((builder) => { .modify((builder) => {
@ -74,6 +74,7 @@ export async function fetchEntities(options) {
}) })
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id') .leftJoin('entities as parents', 'parents.id', 'entities.parent_id')
.orderBy(...(options.order || ['name', 'asc'])) .orderBy(...(options.order || ['name', 'asc']))
.offset((options.page - 1) * options.limit)
.limit(options.limit || 1000); .limit(options.limit || 1000);
return entities.map((entityEntry) => curateEntity(entityEntry)); return entities.map((entityEntry) => curateEntity(entityEntry));

View File

@ -1,4 +1,7 @@
import { fetchActors } from '../actors.js'; import {
fetchActors,
fetchActorsById,
} from '../actors.js';
export function curateActorsQuery(query) { export function curateActorsQuery(query) {
return { return {
@ -36,3 +39,101 @@ export async function fetchActorsApi(req, res) {
total, 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];
}

View File

@ -1,8 +1,12 @@
import { parseResolveInfo } from 'graphql-parse-resolve-info';
import { import {
fetchEntities, fetchEntities,
fetchEntitiesById, fetchEntitiesById,
} from '../entities.js'; } from '../entities.js';
import { getIdsBySlug } from '../cache.js';
export async function fetchEntitiesApi(req, res) { export async function fetchEntitiesApi(req, res) {
const entities = await fetchEntities(req.query); const entities = await fetchEntities(req.query);
@ -13,25 +17,37 @@ export const entitiesSchema = `
extend type Query { extend type Query {
entities( entities(
query: String query: String
type: String
order: [String]
limit: Int! = 30 limit: Int! = 30
page: Int! = 1 page: Int! = 1
): EntitiesResult ): EntitiesResult
entity( entity(
id: Int! slug: String!
): Entity ): Entity
entitiesBySlug(
slugs: [String]!
): [Entity]
entitiesById(
ids: [Int]!
): [Entity]
} }
type EntitiesResult { type EntitiesResult {
nodes: [Entity] nodes: [Entity]
total: Int
} }
type Entity { type Entity {
id: Int! id: Int!
name: String name: String
slug: String slug: String
url: String
type: String
parent: Entity parent: Entity
children: [Entity]
} }
`; `;
@ -43,8 +59,17 @@ export async function fetchEntitiesGraphql(query, _req) {
}; };
} }
export async function fetchEntitiesByIdGraphql(query, _req) { export async function fetchEntitiesByIdGraphql(query, req, info) {
const [entity] = await fetchEntitiesById([query.id]); 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];
} }

View File

@ -1,3 +1,5 @@
import { format } from 'date-fns';
import { import {
graphql, graphql,
buildSchema, buildSchema,
@ -16,6 +18,12 @@ import {
fetchEntitiesByIdGraphql, fetchEntitiesByIdGraphql,
} from './entities.js'; } from './entities.js';
import {
actorsSchema,
fetchActorsGraphql,
fetchActorsByIdGraphql,
} from './actors.js';
const schema = buildSchema(` const schema = buildSchema(`
type Query { type Query {
movies( movies(
@ -26,6 +34,7 @@ const schema = buildSchema(`
scalar Date scalar Date
${scenesSchema} ${scenesSchema}
${actorsSchema}
${entitiesSchema} ${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) { export async function graphqlApi(req, res) {
const data = await graphql({ const data = await graphql({
schema, schema,
@ -47,12 +67,19 @@ export async function graphqlApi(req, res) {
variableValues: req.body.variables, variableValues: req.body.variables,
resolvers: { resolvers: {
DateTimeScalar, DateTimeScalar,
DateScalar,
}, },
rootValue: { rootValue: {
scenes: async (query) => fetchScenesGraphql(query, req), scenes: async (query) => fetchScenesGraphql(query, req),
scene: async (query) => fetchScenesByIdGraphql(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), 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),
}, },
}); });

View File

@ -85,6 +85,10 @@ export const scenesSchema = `
scene( scene(
id: Int! id: Int!
): Release ): Release
scenesById(
ids: [Int]!
): [Release]
} }
type ReleasesAggregate { type ReleasesAggregate {
@ -115,12 +119,6 @@ export const scenesSchema = `
movies: [Release] movies: [Release]
} }
type Actor {
id: Int!
name: String
slug: String
}
type Tag { type Tag {
id: Int! id: Int!
name: String name: String
@ -184,8 +182,6 @@ export async function fetchScenesGraphql(query, req) {
aggregate: false, aggregate: false,
}, req.user); }, req.user);
console.log(query);
return { return {
nodes: scenes, nodes: scenes,
total, total,
@ -197,24 +193,17 @@ export async function fetchScenesGraphql(query, req) {
}, },
*/ */
}; };
/*
return {
scenes,
aggActors,
aggTags,
aggChannels,
limit,
total,
};
*/
} }
export async function fetchScenesByIdGraphql(query, req) { 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, reqUser: req.user,
includePartOf: true, includePartOf: true,
}); });
return scene; if (query.ids) {
return scenes;
}
return scenes[0];
} }