Added tags and entities to REST API..

This commit is contained in:
DebaucheryLibrarian 2020-10-16 23:00:03 +02:00
parent 3d86e52b25
commit e6c52002f0
7 changed files with 249 additions and 155 deletions

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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,
};

29
src/web/entities.js Normal file
View File

@ -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,
};

View File

@ -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,
};

View File

@ -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({

View File

@ -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,
};