From e6c52002f007624b326e401de68c234d2a31226a Mon Sep 17 00:00:00 2001 From: DebaucheryLibrarian Date: Fri, 16 Oct 2020 23:00:03 +0200 Subject: [PATCH] Added tags and entities to REST API.. --- src/entities.js | 104 +++++++++++++++++++++++++++++++++++--------- src/networks.js | 81 ---------------------------------- src/tags.js | 97 +++++++++++++++++++++++++++++++++++++++++ src/web/entities.js | 29 ++++++++++++ src/web/networks.js | 26 ----------- src/web/server.js | 25 +++++++++++ src/web/tags.js | 42 ++++++------------ 7 files changed, 249 insertions(+), 155 deletions(-) delete mode 100644 src/networks.js create mode 100644 src/web/entities.js delete mode 100644 src/web/networks.js diff --git a/src/entities.js b/src/entities.js index 6a5e3b8d..9c63988b 100644 --- a/src/entities.js +++ b/src/entities.js @@ -4,7 +4,6 @@ const config = require('config'); const argv = require('./argv'); const knex = require('./knex'); -const whereOr = require('./utils/where-or'); function curateEntity(entity, includeParameters = false) { if (!entity) { @@ -29,6 +28,15 @@ function curateEntity(entity, includeParameters = false) { }, includeParameters)); } + if (entity.tags) { + curatedEntity.tags = entity.tags.map(tag => ({ + id: tag.id, + name: tag.name, + slug: tag.slug, + priority: tag.priority, + })); + } + return curatedEntity; } @@ -102,33 +110,89 @@ async function fetchIncludedEntities() { return curatedNetworks; } -async function fetchChannels(queryObject) { - const sites = await knex('sites') - .where(builder => whereOr(queryObject, 'sites', builder)) - .select( - 'sites.*', - 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.description as network_description', 'networks.parameters as network_parameters', - ) - .leftJoin('networks', 'sites.network_id', 'networks.id') - .limit(100); +async function fetchEntity(entityId, type) { + const entity = await knex('entities') + .select(knex.raw(` + entities.*, + COALESCE(json_agg(children) FILTER (WHERE children.id IS NOT NULL), '[]') as children, + COALESCE(json_agg(tags) FILTER (WHERE tags.id IS NOT NULL), '[]') as tags, + row_to_json(parents) as parent + `)) + .modify((queryBuilder) => { + if (Number(entityId)) { + queryBuilder.where('entities.id', entityId); + return; + } - return curateEntities(sites); + if (type) { + queryBuilder + .where('entities.slug', entityId) + .where('entities.type', type); + + return; + } + + throw new Error('Invalid ID or unspecified entity type'); + }) + .leftJoin('entities as parents', 'parents.id', 'entities.parent_id') + .leftJoin('entities as children', 'children.parent_id', 'entities.id') + .leftJoin('entities_tags', 'entities_tags.entity_id', 'entities.id') + .leftJoin('tags', 'tags.id', 'entities_tags.tag_id') + .groupBy('entities.id', 'parents.id') + .first(); + + return curateEntity(entity); } -async function fetchChannelsFromReleases() { - const sites = await knex('releases') - .select('site_id', '') - .leftJoin('sites', 'sites.id', 'releases.site_id') - .groupBy('sites.id') - .limit(100); +async function fetchEntities(type, limit) { + const entities = await knex('entities') + .select(knex.raw(` + entities.*, + COALESCE(json_agg(tags) FILTER (WHERE tags.id IS NOT NULL), '[]') as tags, + row_to_json(parents) as parent + `)) + .modify((queryBuilder) => { + if (type) { + queryBuilder.where('entities.type', type); + } + }) + .leftJoin('entities as parents', 'parents.id', 'entities.parent_id') + .leftJoin('entities_tags', 'entities_tags.entity_id', 'entities.id') + .leftJoin('tags', 'tags.id', 'entities_tags.tag_id') + .groupBy('entities.id', 'parents.id') + .limit(limit || 100); - return curateEntities(sites); + return curateEntities(entities); } +async function searchEntities(query, type, limit) { + const entities = await knex + .select(knex.raw(` + entities.*, + COALESCE(json_agg(tags) FILTER (WHERE tags.id IS NOT NULL), '[]') as tags, + row_to_json(parents) as parent + `)) + .from(knex.raw('search_entities(?) as entities', [query])) + .modify((queryBuilder) => { + if (type) { + queryBuilder.where('entities.type', type); + } + }) + .leftJoin('entities as parents', 'parents.id', 'entities.parent_id') + .leftJoin('entities_tags', 'entities_tags.entity_id', 'entities.id') + .leftJoin('tags', 'tags.id', 'entities_tags.tag_id') + .groupBy('entities.id', 'parents.id') + .limit(limit || 100); + + return curateEntities(entities); +} + + module.exports = { curateEntity, curateEntities, fetchIncludedEntities, - fetchChannels, - fetchChannelsFromReleases, + fetchEntity, + fetchEntities, + searchEntities, }; diff --git a/src/networks.js b/src/networks.js deleted file mode 100644 index 549bd1a3..00000000 --- a/src/networks.js +++ /dev/null @@ -1,81 +0,0 @@ -'use strict'; - -const knex = require('./knex'); -const whereOr = require('./utils/where-or'); -const { fetchSites } = require('./sites'); - -async function curateNetwork(network, includeParameters = false, includeSites = true, includeStudios = false) { - const curatedNetwork = { - id: network.id, - name: network.name, - url: network.url, - description: network.description, - slug: network.slug, - parameters: includeParameters ? network.parameters : null, - }; - - if (includeSites) { - curatedNetwork.sites = await fetchSites({ network_id: network.id }); - } - - if (includeStudios) { - const studios = await knex('studios').where({ network_id: network.id }); - - curatedNetwork.studios = studios.map(studio => ({ - id: studio.id, - name: studio.name, - url: studio.url, - description: studio.description, - slug: studio.slug, - })); - } - - return curatedNetwork; -} - -function curateNetworks(releases) { - return Promise.all(releases.map(async release => curateNetwork(release))); -} - -async function findNetworkByUrl(url) { - const { hostname } = new URL(url); - const domain = hostname.replace(/^www./, ''); - - const network = await knex('networks') - .where('networks.url', 'like', `%${domain}`) - .orWhere('networks.url', url) - .first(); - - if (network) { - return curateNetwork(network, true); - } - - return null; -} - -async function fetchNetworks(queryObject) { - const releases = await knex('networks') - .where(builder => whereOr(queryObject, 'networks', builder)) - .limit(100); - - return curateNetworks(releases); -} - -async function fetchNetworksFromReleases() { - const releases = await knex('releases') - .select('site_id', '') - .leftJoin('sites', 'sites.id', 'releases.site_id') - .leftJoin('networks', 'networks.id', 'sites.network_id') - .groupBy('networks.id') - .limit(100); - - return curateNetworks(releases); -} - -module.exports = { - curateNetwork, - curateNetworks, - fetchNetworks, - fetchNetworksFromReleases, - findNetworkByUrl, -}; diff --git a/src/tags.js b/src/tags.js index d8cb94af..c1146071 100644 --- a/src/tags.js +++ b/src/tags.js @@ -4,6 +4,75 @@ const knex = require('./knex'); const slugify = require('./utils/slugify'); const bulkInsert = require('./utils/bulk-insert'); +function curateTagMedia(media) { + if (!media) { + return null; + } + + return { + id: media.id, + path: media.path, + thumbnail: media.thumbnail, + lazy: media.lazy, + comment: media.comment, + credit: media.credit, + }; +} + +function curateTag(tag) { + if (!tag) { + return null; + } + + const curatedTag = { + id: tag.id, + name: tag.name, + slug: tag.slug, + description: tag.description, + priority: tag.priority, + group: curateTag(tag.group), + aliasFor: curateTag(tag.alias), + aliases: (tag.aliases || []).map(aliasTag => curateTag(aliasTag)), + }; + + if (tag.poster) { + curatedTag.poster = curateTagMedia(tag.poster); + } + + if (tag.photos) { + curatedTag.photos = tag.photos.map(photo => curateTagMedia(photo)); + } + + return curatedTag; +} + +function withRelations(queryBuilder, withMedia) { + queryBuilder + .select(knex.raw(` + tags.*, + COALESCE(json_agg(DISTINCT aliases) FILTER (WHERE aliases.id IS NOT NULL), '[]') as aliases, + row_to_json(tags_groups) as group, + row_to_json(roots) as alias + `)) + .leftJoin('tags_groups', 'tags_groups.id', 'tags.group_id') + .leftJoin('tags as roots', 'roots.id', 'tags.alias_for') + .leftJoin('tags as aliases', 'aliases.alias_for', 'tags.id') + .groupBy('tags.id', 'tags_groups.id', 'roots.id'); + + if (withMedia) { + queryBuilder + .select(knex.raw(` + row_to_json(posters) as poster, + COALESCE(json_agg(DISTINCT photos) FILTER (WHERE photos.id IS NOT NULL), '[]') as photos + `)) + .leftJoin('tags_posters', 'tags_posters.tag_id', 'tags.id') + .leftJoin('tags_photos', 'tags_photos.tag_id', 'tags.id') + .leftJoin('media as posters', 'posters.id', 'tags_posters.media_id') + .leftJoin('media as photos', 'photos.id', 'tags_photos.media_id') + .groupBy('posters.id'); + } +} + async function matchReleaseTags(releases) { const rawTags = releases .map(release => release.tags).flat() @@ -82,6 +151,34 @@ async function associateReleaseTags(releases, type = 'release') { await bulkInsert(`${type}s_tags`, tagAssociations, false); } +async function fetchTag(tagId) { + const tag = await knex('tags') + .modify(queryBuilder => withRelations(queryBuilder, true)) + .where((builder) => { + if (Number(tagId)) { + builder.where('tags.id', tagId); + return; + } + + builder + .where('tags.name', tagId) + .orWhere('tags.slug', tagId); + }) + .first(); + + return curateTag(tag); +} + +async function fetchTags(limit = 100) { + const tags = await knex('tags') + .modify(queryBuilder => withRelations(queryBuilder, false)) + .limit(limit); + + return tags.map(tag => curateTag(tag)); +} + module.exports = { associateReleaseTags, + fetchTag, + fetchTags, }; diff --git a/src/web/entities.js b/src/web/entities.js new file mode 100644 index 00000000..62b94a13 --- /dev/null +++ b/src/web/entities.js @@ -0,0 +1,29 @@ +'use strict'; + +const { fetchEntity, fetchEntities, searchEntities } = require('../entities'); + +async function fetchEntityApi(req, res, type) { + const entity = await fetchEntity(req.params.entityId, type || req.query.type); + + if (entity) { + res.send({ entity }); + return; + } + + res.status(404).send({ entity: null }); +} + +async function fetchEntitiesApi(req, res, type) { + const query = req.query.query || req.query.q; + + const entities = query + ? await searchEntities(query, type || req.query.type, req.query.limit) + : await fetchEntities(type || req.query.type, req.query.limit); + + res.send({ entities }); +} + +module.exports = { + fetchEntity: fetchEntityApi, + fetchEntities: fetchEntitiesApi, +}; diff --git a/src/web/networks.js b/src/web/networks.js deleted file mode 100644 index d41b38b0..00000000 --- a/src/web/networks.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -const { fetchNetworks, fetchNetworksFromReleases } = require('../networks'); - -async function fetchNetworksApi(req, res) { - const networkId = typeof req.params.networkId === 'number' ? req.params.networkId : undefined; // null will literally include NULL results - const networkSlug = typeof req.params.networkId === 'string' ? req.params.networkId : undefined; - - const networks = await fetchNetworks({ - id: networkId, - slug: networkSlug, - }); - - res.send(networks); -} - -async function fetchNetworksFromReleasesApi(req, res) { - const networks = await fetchNetworksFromReleases(); - - res.send(networks); -} - -module.exports = { - fetchNetworks: fetchNetworksApi, - fetchNetworksFromReleases: fetchNetworksFromReleasesApi, -}; diff --git a/src/web/server.js b/src/web/server.js index a6fc2158..62b06015 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -26,6 +26,16 @@ const { fetchActors, } = require('./actors'); +const { + fetchEntity, + fetchEntities, +} = require('./entities'); + +const { + fetchTag, + fetchTags, +} = require('./tags'); + async function initServer() { const app = express(); const router = Router(); @@ -78,6 +88,21 @@ async function initServer() { router.get('/api/actors', fetchActors); router.get('/api/actors/:actorId', fetchActor); + router.get('/api/entities', async (req, res) => fetchEntities(req, res, null)); + router.get('/api/entities/:entityId', async (req, res) => fetchEntity(req, res, null)); + + router.get('/api/channels', async (req, res) => fetchEntities(req, res, 'channel')); + router.get('/api/channels/:entityId', async (req, res) => fetchEntity(req, res, 'channel')); + + router.get('/api/networks', async (req, res) => fetchEntities(req, res, 'network')); + router.get('/api/networks/:entityId', async (req, res) => fetchEntity(req, res, 'network')); + + router.get('/api/studios', async (req, res) => fetchEntities(req, res, 'studio')); + router.get('/api/studios/:entityId', async (req, res) => fetchEntity(req, res, 'studio')); + + router.get('/api/tags', fetchTags); + router.get('/api/tags/:tagId', fetchTag); + router.get('*', (req, res) => { res.render(path.join(__dirname, '../../assets/index.ejs'), { env: JSON.stringify({ diff --git a/src/web/tags.js b/src/web/tags.js index b935b5fa..3842d46d 100644 --- a/src/web/tags.js +++ b/src/web/tags.js @@ -1,40 +1,26 @@ 'use strict'; -const { fetchTags } = require('../tags'); +const { fetchTag, fetchTags } = require('../tags'); -async function fetchTagsApi(req, res) { - const tagId = typeof req.params.tagId === 'number' ? req.params.tagId : undefined; // null will literally include NULL results - const tagSlug = typeof req.params.tagId === 'string' ? req.params.tagId : undefined; +async function fetchTagApi(req, res) { + const tag = await fetchTag(req.params.tagId); - if (tagId || tagSlug) { - const tags = await fetchTags({ - id: tagId, - slug: tagSlug, - }, null, req.query.limit); - - if (tags.length > 0) { - res.send(tags[0]); - return; - } - - res.status(404).send(); + if (tag) { + res.send({ tag }); return; } - const query = {}; - const groupsQuery = {}; - - if (req.query.priority) query.priority = req.query.priority.split(','); - if (req.query.slug) query.slug = req.query.slug.split(','); - if (req.query.group) { - groupsQuery.slug = req.query.group.split(','); - } - - const tags = await fetchTags(query, groupsQuery, req.query.limit); - - res.send(tags); + res.status(404).send({ tag: null }); } +async function fetchTagsApi(req, res) { + const tags = await fetchTags(req.query.limit); + + res.send({ tags }); +} + + module.exports = { + fetchTag: fetchTagApi, fetchTags: fetchTagsApi, };