diff --git a/package-lock.json b/package-lock.json index ba4f6a5e..d1241bce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index b838d4c6..f307b28c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/releases.js b/src/releases.js index 6de5c366..e9c86310 100644 --- a/src/releases.js +++ b/src/releases.js @@ -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) { diff --git a/src/web/graphql.js b/src/web/graphql.js new file mode 100644 index 00000000..2c048bd2 --- /dev/null +++ b/src/web/graphql.js @@ -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 }; diff --git a/src/web/postgraphile.js b/src/web/postgraphile.js new file mode 100644 index 00000000..3cc6b165 --- /dev/null +++ b/src/web/postgraphile.js @@ -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, + ], + }, +); diff --git a/src/web/server.js b/src/web/server.js index 9108bc1f..dc1479d1 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -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));