Added internal GraphQL client, using GraphQL for scenes API.

This commit is contained in:
DebaucheryLibrarian 2021-02-27 03:52:27 +01:00
parent 162e5c2181
commit 2deed3a7eb
6 changed files with 220 additions and 108 deletions

1
package-lock.json generated
View File

@ -42,6 +42,7 @@
"fluent-ffmpeg": "^2.1.2",
"fs-extra": "^7.0.1",
"graphile-utils": "^4.5.6",
"graphql": "^14.6.0",
"iconv-lite": "^0.5.1",
"inquirer": "^7.3.3",
"jsdom": "^16.3.0",

View File

@ -103,6 +103,7 @@
"fluent-ffmpeg": "^2.1.2",
"fs-extra": "^7.0.1",
"graphile-utils": "^4.5.6",
"graphql": "^14.6.0",
"iconv-lite": "^0.5.1",
"inquirer": "^7.3.3",
"jsdom": "^16.3.0",

View File

@ -5,7 +5,78 @@ const inquirer = require('inquirer');
const logger = require('./logger')(__filename);
const knex = require('./knex');
const { flushOrphanedMedia } = require('./media');
const { HttpError } = require('./errors');
const { graphql } = require('./web/graphql');
const releaseFields = `
id
entryId
shootId
title
url
date
description
duration
entity {
id
name
slug
parent {
id
name
slug
}
}
actors: releasesActors {
actor {
id
name
slug
gender
aliasFor
entityId
entryId
}
}
tags: releasesTags {
tag {
id
name
slug
}
}
chapters @include (if: $full) {
id
index
time
duration
title
description
}
poster: releasesPosterByReleaseId {
media {
id
path
thumbnail
s3: isS3
width
height
size
}
}
photos: releasesPhotos @include (if: $full) {
media {
id
path
thumbnail
s3: isS3
width
height
size
}
}
createdAt
`;
function curateRelease(release, withMedia = false, withPoster = true) {
if (!release) {
@ -81,93 +152,100 @@ function curateRelease(release, withMedia = false, withPoster = true) {
};
}
function withRelations(queryBuilder, withMedia = false, withPoster = true) {
queryBuilder
.select(knex.raw(`
releases.id, releases.entry_id, releases.shoot_id, releases.title, releases.url, releases.date, releases.description, releases.duration, releases.created_at,
row_to_json(entities) as entity,
row_to_json(parents) as parent,
COALESCE(json_agg(DISTINCT actors) FILTER (WHERE actors.id IS NOT NULL), '[]') as actors,
COALESCE(json_agg(DISTINCT tags) FILTER (WHERE tags.id IS NOT NULL), '[]') as tags,
COALESCE(json_agg(DISTINCT chapters) FILTER (WHERE chapters.id IS NOT NULL), '[]') as chapters
`))
.leftJoin('entities', 'entities.id', 'releases.entity_id')
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id')
.leftJoin('releases_actors', 'releases_actors.release_id', 'releases.id')
.leftJoin('actors', 'actors.id', 'releases_actors.actor_id')
.leftJoin('releases_tags', 'releases_tags.release_id', 'releases.id')
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
.leftJoin('chapters', 'chapters.release_id', 'releases.id')
.groupBy(knex.raw(`
releases.id, releases.entry_id, releases.shoot_id, releases.title, releases.url, releases.date, releases.description, releases.duration, releases.created_at,
entities.id, parents.id
`));
if (withMedia || withPoster) {
queryBuilder
.select(knex.raw(`
row_to_json(posters) as poster
`))
.leftJoin('releases_posters', 'releases_posters.release_id', 'releases.id')
.leftJoin('media as posters', 'posters.id', 'releases_posters.media_id')
.groupBy('posters.id');
function curateGraphqlRelease(release) {
if (!release) {
return null;
}
if (withMedia) {
queryBuilder
.select(knex.raw(`
row_to_json(trailers) as trailer,
COALESCE(json_agg(DISTINCT photos) FILTER (WHERE photos.id IS NOT NULL), '[]') as photos
`))
.leftJoin('releases_photos', 'releases_photos.release_id', 'releases.id')
.leftJoin('media as photos', 'photos.id', 'releases_photos.media_id')
.leftJoin('releases_trailers', 'releases_trailers.release_id', 'releases.id')
.leftJoin('media as trailers', 'trailers.id', 'releases_trailers.media_id')
.groupBy('posters.id', 'trailers.id');
}
return {
id: release.id,
...(release.relevance && { relevance: release.relevance }),
entryId: release.entryId,
shootId: release.shootId,
title: release.title || null,
url: release.url || null,
date: release.date,
description: release.description || null,
duration: release.duration,
entity: release.entity,
actors: release.actors.map(actor => actor.actor),
tags: release.tags.map(tag => tag.tag),
...(release.chapters && { chapters: release.chapters }),
poster: release.poster?.media || null,
...(release.photos && { photos: release.photos.map(photo => photo.media) }),
trailer: release.trailer?.media || null,
createdAt: release.createdAt,
};
}
async function fetchScene(releaseId) {
const release = await knex('releases')
.where('releases.id', releaseId)
.modify(withRelations, true, true)
.first();
const { release } = await graphql(`
query Release(
$releaseId: Int!
$full: Boolean = true
) {
release(id: $releaseId) {
${releaseFields}
}
}
`, {
releaseId: Number(releaseId),
});
return curateRelease(release, true);
return curateGraphqlRelease(release);
}
async function fetchScenes(limit = 100) {
if (typeof limit !== 'number') {
throw new HttpError('Limit parameter needs to be a number', 400);
}
const { releases } = await graphql(`
query SearchReleases(
$limit: Int = 20
$full: Boolean = false
) {
releases(
first: $limit
orderBy: DATE_DESC
) {
${releaseFields}
}
}
`, {
limit: Math.min(limit, 10000),
});
const releases = await knex('releases')
.modify(withRelations, false, true)
.limit(Math.min(limit, 1000000));
return releases.map(release => curateRelease(release));
return releases.map(release => curateGraphqlRelease(release));
}
async function searchScenes(query, limit = 100, relevance = 0) {
if (typeof limit !== 'number') {
throw new HttpError('Limit parameter needs to be a number', 400);
}
const { releases } = await graphql(`
query SearchReleases(
$query: String!
$limit: Int = 20
$relevance: Float = 0.025
$full: Boolean = false
) {
releases: searchReleases(
query: $query
first: $limit
orderBy: RANK_DESC
filter: {
rank: {
greaterThan: $relevance
}
}
) {
rank
release {
${releaseFields}
}
}
}
`, {
query,
limit,
relevance,
});
if (typeof relevance !== 'number') {
throw new HttpError('Relevance parameter needs to be a number', 400);
}
const releases = await knex
.select(knex.raw('search_results.rank as relevance'))
.from(knex.raw('search_releases(:query) as search_results', { query }))
.leftJoin('releases', 'releases.id', 'search_results.release_id')
.where('search_results.rank', '>=', relevance)
.modify(withRelations, false, true)
.limit(Math.min(limit, 1000000))
.groupBy('search_results.rank')
.orderBy('search_results.rank', 'desc');
return releases.map(release => curateRelease(release));
return releases.map(release => curateGraphqlRelease({ ...release.release, relevance: release.rank }));
}
async function deleteScenes(sceneIds) {

24
src/web/graphql.js Normal file
View File

@ -0,0 +1,24 @@
'use strict';
const { withPostGraphileContext } = require('postgraphile');
const { graphql } = require('graphql');
const pg = require('./postgraphile');
const logger = require('../logger')(__filename);
async function query(graphqlQuery, params) {
return withPostGraphileContext(pg, async (context) => {
const schema = await pg.getGraphQLSchema();
const result = await graphql(schema, graphqlQuery, null, context, params);
if (result.errors?.length > 0) {
logger.error(result.errors);
throw result.errors[0];
}
return result.data;
});
}
module.exports = { graphql: query };

39
src/web/postgraphile.js Normal file
View File

@ -0,0 +1,39 @@
'use strict';
const config = require('config');
const { postgraphile } = require('postgraphile');
const PgConnectionFilterPlugin = require('postgraphile-plugin-connection-filter');
const PgSimplifyInflectorPlugin = require('@graphile-contrib/pg-simplify-inflector');
const PgOrderByRelatedPlugin = require('@graphile-contrib/pg-order-by-related');
const { ActorPlugins, SitePlugins, ReleasePlugins } = require('./plugins/plugins');
const connectionString = `postgres://${config.database.user}:${config.database.password}@${config.database.host}:5432/${config.database.database}`;
module.exports = postgraphile(
connectionString,
'public',
{
// watchPg: true,
dynamicJson: true,
graphiql: true,
enhanceGraphiql: true,
allowExplain: () => true,
// simpleCollections: 'only',
simpleCollections: 'both',
graphileBuildOptions: {
pgOmitListSuffix: true,
connectionFilterRelations: true,
},
appendPlugins: [
PgSimplifyInflectorPlugin,
PgConnectionFilterPlugin,
PgOrderByRelatedPlugin,
...ActorPlugins,
...SitePlugins,
...ReleasePlugins,
],
},
);

View File

@ -3,22 +3,18 @@
const path = require('path');
const config = require('config');
const express = require('express');
const { postgraphile } = require('postgraphile');
const Router = require('express-promise-router');
const bodyParser = require('body-parser');
const session = require('express-session');
const KnexSessionStore = require('connect-session-knex')(session);
const nanoid = require('nanoid');
const PgConnectionFilterPlugin = require('postgraphile-plugin-connection-filter');
const PgSimplifyInflectorPlugin = require('@graphile-contrib/pg-simplify-inflector');
const PgOrderByRelatedPlugin = require('@graphile-contrib/pg-order-by-related');
const logger = require('../logger')(__filename);
const knex = require('../knex');
const { ActorPlugins, SitePlugins, ReleasePlugins } = require('./plugins/plugins');
const errorHandler = require('./error');
const pg = require('./postgraphile');
const {
fetchScene,
fetchScenes,
@ -45,34 +41,7 @@ async function initServer() {
const router = Router();
const store = new KnexSessionStore({ knex });
const connectionString = `postgres://${config.database.user}:${config.database.password}@${config.database.host}:5432/${config.database.database}`;
app.use(postgraphile(
connectionString,
'public',
{
// watchPg: true,
dynamicJson: true,
graphiql: true,
enhanceGraphiql: true,
allowExplain: () => true,
// simpleCollections: 'only',
simpleCollections: 'both',
graphileBuildOptions: {
pgOmitListSuffix: true,
connectionFilterRelations: true,
},
appendPlugins: [
PgSimplifyInflectorPlugin,
PgConnectionFilterPlugin,
PgOrderByRelatedPlugin,
...ActorPlugins,
...SitePlugins,
...ReleasePlugins,
],
},
));
app.use(pg);
app.set('view engine', 'ejs');
router.use('/media', express.static(config.media.path));