From fe1a9ed26b5738e3eccc98abd9ac6ca6b7638825 Mon Sep 17 00:00:00 2001 From: DebaucheryLibrarian Date: Tue, 1 Apr 2025 02:14:36 +0200 Subject: [PATCH] Added GraphQL queries for alerts and notifications. --- src/alerts.js | 81 +++++++++++++++++++++++++++++----------------- src/web/alerts.js | 59 ++++++++++++++++++++++++++++++++- src/web/graphql.js | 8 +++++ src/web/scenes.js | 20 ++++++++++-- 4 files changed, 135 insertions(+), 33 deletions(-) diff --git a/src/alerts.js b/src/alerts.js index 84ea6e1..936c9ce 100755 --- a/src/alerts.js +++ b/src/alerts.js @@ -195,6 +195,7 @@ export async function createAlert(alert, reqUser) { export async function removeAlert(alertId, reqUser) { const alert = await knex('alerts') .where('alerts.id', alertId) + .where('alerts.user_id', reqUser.id) .select( 'alerts.id', knex.raw('coalesce(array_agg(distinct alerts_actors.actor_id) filter (where alerts_actors.actor_id is not null), \'{}\') as actor_ids'), @@ -208,16 +209,51 @@ export async function removeAlert(alertId, reqUser) { .groupBy('alerts.id') .first(); - await knex('alerts') + if (!alert) { + throw new HttpError(`Could not find alert ${alertId}`, 404); + } + + const [removed] = await knex('alerts') .where('id', alertId) .where('user_id', reqUser.id) - .delete(); + .delete() + .returning('*'); - await Promise.all([ + if (!removed) { + throw new HttpError(`Could not remove alert ${alertId}`, 404); + } + + // slow not critical for response, don't await + Promise.all([ alert.actor_ids.length > 0 && knex.schema.refreshMaterializedView('alerts_users_actors'), alert.tag_ids.length > 0 && knex.schema.refreshMaterializedView('alerts_users_tags'), alert.entity_ids?.length > 0 && knex.schema.refreshMaterializedView('alerts_users_entities'), ]); + + return curateAlert(removed); +} + +function curateNotification(notification, scenes) { + const scene = scenes.find((sceneX) => sceneX.id === notification.scene_id); + + return { + id: notification.id, + sceneId: notification.scene_id, + scene, + alertId: notification.alert_id, + matchedActors: scene.actors.filter((actor) => notification.alert_actors.includes(actor.id)), + matchedTags: scene.tags.filter((tag) => notification.alert_tags.includes(tag.id)), + matchedEntity: [scene.channel, scene.network].find((entity) => notification.alert_entities.includes(entity?.id)) || null, + matchedExpressions: notification.alert_matches + .filter((match) => new RegExp(match.expression, 'ui').test(scene[match.property])) + .map((match) => ({ + id: match.id, + property: match.property, + expression: match.expression, + })), + isSeen: notification.seen, + createdAt: notification.created_at, + }; } export async function fetchUnseenNotificationsCount(reqUser) { @@ -262,29 +298,7 @@ export async function fetchNotifications(reqUser, options = {}) { ]); const scenes = await fetchScenesById(notifications.map((notification) => notification.scene_id)); - - const curatedNotifications = notifications.map((notification) => { - const scene = scenes.find((sceneX) => sceneX.id === notification.scene_id); - - return { - id: notification.id, - sceneId: notification.scene_id, - scene, - alertId: notification.alert_id, - matchedActors: scene.actors.filter((actor) => notification.alert_actors.includes(actor.id)), - matchedTags: scene.tags.filter((tag) => notification.alert_tags.includes(tag.id)), - matchedEntity: [scene.channel, scene.network].find((entity) => notification.alert_entities.includes(entity?.id)) || null, - matchedExpressions: notification.alert_matches - .filter((match) => new RegExp(match.expression, 'ui').test(scene[match.property])) - .map((match) => ({ - id: match.id, - property: match.property, - expression: match.expression, - })), - isSeen: notification.seen, - createdAt: notification.created_at, - }; - }); + const curatedNotifications = notifications.map((notification) => curateNotification(notification, scenes)); return { notifications: curatedNotifications, @@ -293,18 +307,27 @@ export async function fetchNotifications(reqUser, options = {}) { } export async function updateNotification(notificationId, updatedNotification, reqUser) { - await knex('notifications') + const [updated] = await knex('notifications') .where('id', notificationId) .where('user_id', reqUser.id) .update({ seen: updatedNotification.seen, - }); + }) + .returning('*'); + + if (!updated) { + throw new HttpError(`No notification ${notificationId} found to update`, 404); + } + + return updated.id; } export async function updateNotifications(updatedNotification, reqUser) { - await knex('notifications') + const updatedCount = await knex('notifications') .where('user_id', reqUser.id) .update({ seen: updatedNotification.seen, }); + + return updatedCount; } diff --git a/src/web/alerts.js b/src/web/alerts.js index 541aa6a..b2f39b5 100755 --- a/src/web/alerts.js +++ b/src/web/alerts.js @@ -16,6 +16,10 @@ export const alertsSchema = ` alert( id: Int! ): Alert + + notifications( + limit: Int = 10 + ): Notifications } extend type Mutation { @@ -32,6 +36,17 @@ export const alertsSchema = ` email: Boolean = false stashes: [Int!] ): Alert + + removeAlert(id: Int!): Alert + + updateNotification( + id: Int! + seen: Boolean + ): Int! + + updateNotifications( + seen: Boolean + ): Int! } type AlertAnd { @@ -100,6 +115,23 @@ export const alertsSchema = ` matches: [AlertMatch] stashes: [AlertStash] } + + type Notifications { + notifications: [Notification!]! + unseen: Int! + } + + type Notification { + id: Int! + alertId: Int + sceneId: Int + scene: Release + isSeen: Boolean! + matchedActors: [Actor!]! + matchedTags: [Tag!]! + matchedEntity: Entity + createdAt: Date! + } `; export async function fetchAlertsApi(req, res) { @@ -121,7 +153,6 @@ export async function createAlertApi(req, res) { } export async function createAlertGraphql(query, req) { - console.log('CREATE ALERT', query); const alert = await createAlert(query, req.user); return alert; @@ -133,6 +164,12 @@ export async function removeAlertApi(req, res) { res.status(204).send(); } +export async function removeAlertGraphql(query, req) { + const removedAlert = await removeAlert(query.id, req.user); + + return removedAlert; +} + export async function fetchNotificationsApi(req, res) { const notifications = await fetchNotifications(req.user, { limit: req.query.limit || 10, @@ -141,18 +178,38 @@ export async function fetchNotificationsApi(req, res) { res.send(notifications); } +export async function fetchNotificationsGraphql(query, req) { + const notifications = await fetchNotifications(req.user, { + limit: query.limit || 10, + }); + + return notifications; +} + export async function updateNotificationsApi(req, res) { await updateNotifications(req.body, req.user); res.status(204).send(); } +export async function updateNotificationsGraphql(query, req) { + const updatedCount = await updateNotifications(query, req.user); + + return updatedCount; +} + export async function updateNotificationApi(req, res) { await updateNotification(req.params.notificationId, req.body, req.user); res.status(204).send(); } +export async function updateNotificationGraphql(query, req) { + const updatedNotification = await updateNotification(query.id, query, req.user); + + return updatedNotification; +} + export const router = Router(); router.get('/api/alerts', fetchAlertsApi); diff --git a/src/web/graphql.js b/src/web/graphql.js index c8c29c0..46c654f 100644 --- a/src/web/graphql.js +++ b/src/web/graphql.js @@ -50,6 +50,10 @@ import { alertsSchema, fetchAlertsGraphql, createAlertGraphql, + removeAlertGraphql, + fetchNotificationsGraphql, + updateNotificationGraphql, + updateNotificationsGraphql, } from './alerts.js'; import { verifyKey } from '../auth.js'; @@ -133,6 +137,7 @@ export async function graphqlApi(req, res) { stashes: async (query) => fetchUserStashesGraphql(query, req), stash: async (query) => fetchStashGraphql(query, req), alerts: async (query) => fetchAlertsGraphql(query, req), + notifications: async (query) => fetchNotificationsGraphql(query, req), // stash mutation createStash: async (query) => createStashGraphql(query, req), updateStash: async (query) => updateStashGraphql(query, req), @@ -145,6 +150,9 @@ export async function graphqlApi(req, res) { unstashMovie: async (query) => unstashMovieGraphql(query, req), // alert mutation createAlert: async (query) => createAlertGraphql(query, req), + removeAlert: async (query) => removeAlertGraphql(query, req), + updateNotification: async (query) => updateNotificationGraphql(query, req), + updateNotifications: async (query) => updateNotificationsGraphql(query, req), }, }); diff --git a/src/web/scenes.js b/src/web/scenes.js index 0f6e3b3..44758cd 100644 --- a/src/web/scenes.js +++ b/src/web/scenes.js @@ -133,6 +133,7 @@ export const scenesSchema = ` covers: [Media!]! movies: [Release!]! stashes: [Stash!] + isStashed(stash: String!): Boolean } type Tag { @@ -173,6 +174,19 @@ function getScope(query) { return 'latest'; } +function attachResolvers(scene) { + return { + ...scene, + isStashed(args) { + if (!scene.stashes) { + return null; + } + + return scene.stashes.some((stash) => stash.slug === args.stash) || false; + }, + }; +} + export async function fetchScenesGraphql(query, req) { const mainEntity = query.entities?.find((entity) => entity.charAt(0) !== '!'); @@ -223,7 +237,7 @@ export async function fetchScenesGraphql(query, req) { }, req.user); return { - nodes: scenes, + nodes: scenes.map((scene) => attachResolvers(scene)), total, /* restrict until deemed essential for 3rd party apps aggregates: { @@ -252,10 +266,10 @@ export async function fetchScenesByIdGraphql(query, req) { }); if (query.ids) { - return scenes; + return scenes.map((scene) => attachResolvers(scene)); } - return scenes[0]; + return attachResolvers(scenes[0]); } async function fetchSceneRevisionsApi(req, res) {