Added GraphQL queries for alerts and notifications.

This commit is contained in:
DebaucheryLibrarian 2025-04-01 02:14:36 +02:00
parent 2121c51ae6
commit fe1a9ed26b
4 changed files with 135 additions and 33 deletions

View File

@ -195,6 +195,7 @@ export async function createAlert(alert, reqUser) {
export async function removeAlert(alertId, reqUser) { export async function removeAlert(alertId, reqUser) {
const alert = await knex('alerts') const alert = await knex('alerts')
.where('alerts.id', alertId) .where('alerts.id', alertId)
.where('alerts.user_id', reqUser.id)
.select( .select(
'alerts.id', 'alerts.id',
knex.raw('coalesce(array_agg(distinct alerts_actors.actor_id) filter (where alerts_actors.actor_id is not null), \'{}\') as actor_ids'), 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') .groupBy('alerts.id')
.first(); .first();
await knex('alerts') if (!alert) {
throw new HttpError(`Could not find alert ${alertId}`, 404);
}
const [removed] = await knex('alerts')
.where('id', alertId) .where('id', alertId)
.where('user_id', reqUser.id) .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.actor_ids.length > 0 && knex.schema.refreshMaterializedView('alerts_users_actors'),
alert.tag_ids.length > 0 && knex.schema.refreshMaterializedView('alerts_users_tags'), alert.tag_ids.length > 0 && knex.schema.refreshMaterializedView('alerts_users_tags'),
alert.entity_ids?.length > 0 && knex.schema.refreshMaterializedView('alerts_users_entities'), 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) { 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 scenes = await fetchScenesById(notifications.map((notification) => notification.scene_id));
const curatedNotifications = notifications.map((notification) => curateNotification(notification, scenes));
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,
};
});
return { return {
notifications: curatedNotifications, notifications: curatedNotifications,
@ -293,18 +307,27 @@ export async function fetchNotifications(reqUser, options = {}) {
} }
export async function updateNotification(notificationId, updatedNotification, reqUser) { export async function updateNotification(notificationId, updatedNotification, reqUser) {
await knex('notifications') const [updated] = await knex('notifications')
.where('id', notificationId) .where('id', notificationId)
.where('user_id', reqUser.id) .where('user_id', reqUser.id)
.update({ .update({
seen: updatedNotification.seen, 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) { export async function updateNotifications(updatedNotification, reqUser) {
await knex('notifications') const updatedCount = await knex('notifications')
.where('user_id', reqUser.id) .where('user_id', reqUser.id)
.update({ .update({
seen: updatedNotification.seen, seen: updatedNotification.seen,
}); });
return updatedCount;
} }

View File

@ -16,6 +16,10 @@ export const alertsSchema = `
alert( alert(
id: Int! id: Int!
): Alert ): Alert
notifications(
limit: Int = 10
): Notifications
} }
extend type Mutation { extend type Mutation {
@ -32,6 +36,17 @@ export const alertsSchema = `
email: Boolean = false email: Boolean = false
stashes: [Int!] stashes: [Int!]
): Alert ): Alert
removeAlert(id: Int!): Alert
updateNotification(
id: Int!
seen: Boolean
): Int!
updateNotifications(
seen: Boolean
): Int!
} }
type AlertAnd { type AlertAnd {
@ -100,6 +115,23 @@ export const alertsSchema = `
matches: [AlertMatch] matches: [AlertMatch]
stashes: [AlertStash] 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) { export async function fetchAlertsApi(req, res) {
@ -121,7 +153,6 @@ export async function createAlertApi(req, res) {
} }
export async function createAlertGraphql(query, req) { export async function createAlertGraphql(query, req) {
console.log('CREATE ALERT', query);
const alert = await createAlert(query, req.user); const alert = await createAlert(query, req.user);
return alert; return alert;
@ -133,6 +164,12 @@ export async function removeAlertApi(req, res) {
res.status(204).send(); 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) { export async function fetchNotificationsApi(req, res) {
const notifications = await fetchNotifications(req.user, { const notifications = await fetchNotifications(req.user, {
limit: req.query.limit || 10, limit: req.query.limit || 10,
@ -141,18 +178,38 @@ export async function fetchNotificationsApi(req, res) {
res.send(notifications); 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) { export async function updateNotificationsApi(req, res) {
await updateNotifications(req.body, req.user); await updateNotifications(req.body, req.user);
res.status(204).send(); 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) { export async function updateNotificationApi(req, res) {
await updateNotification(req.params.notificationId, req.body, req.user); await updateNotification(req.params.notificationId, req.body, req.user);
res.status(204).send(); 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(); export const router = Router();
router.get('/api/alerts', fetchAlertsApi); router.get('/api/alerts', fetchAlertsApi);

View File

@ -50,6 +50,10 @@ import {
alertsSchema, alertsSchema,
fetchAlertsGraphql, fetchAlertsGraphql,
createAlertGraphql, createAlertGraphql,
removeAlertGraphql,
fetchNotificationsGraphql,
updateNotificationGraphql,
updateNotificationsGraphql,
} from './alerts.js'; } from './alerts.js';
import { verifyKey } from '../auth.js'; import { verifyKey } from '../auth.js';
@ -133,6 +137,7 @@ export async function graphqlApi(req, res) {
stashes: async (query) => fetchUserStashesGraphql(query, req), stashes: async (query) => fetchUserStashesGraphql(query, req),
stash: async (query) => fetchStashGraphql(query, req), stash: async (query) => fetchStashGraphql(query, req),
alerts: async (query) => fetchAlertsGraphql(query, req), alerts: async (query) => fetchAlertsGraphql(query, req),
notifications: async (query) => fetchNotificationsGraphql(query, req),
// stash mutation // stash mutation
createStash: async (query) => createStashGraphql(query, req), createStash: async (query) => createStashGraphql(query, req),
updateStash: async (query) => updateStashGraphql(query, req), updateStash: async (query) => updateStashGraphql(query, req),
@ -145,6 +150,9 @@ export async function graphqlApi(req, res) {
unstashMovie: async (query) => unstashMovieGraphql(query, req), unstashMovie: async (query) => unstashMovieGraphql(query, req),
// alert mutation // alert mutation
createAlert: async (query) => createAlertGraphql(query, req), createAlert: async (query) => createAlertGraphql(query, req),
removeAlert: async (query) => removeAlertGraphql(query, req),
updateNotification: async (query) => updateNotificationGraphql(query, req),
updateNotifications: async (query) => updateNotificationsGraphql(query, req),
}, },
}); });

View File

@ -133,6 +133,7 @@ export const scenesSchema = `
covers: [Media!]! covers: [Media!]!
movies: [Release!]! movies: [Release!]!
stashes: [Stash!] stashes: [Stash!]
isStashed(stash: String!): Boolean
} }
type Tag { type Tag {
@ -173,6 +174,19 @@ function getScope(query) {
return 'latest'; 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) { export async function fetchScenesGraphql(query, req) {
const mainEntity = query.entities?.find((entity) => entity.charAt(0) !== '!'); const mainEntity = query.entities?.find((entity) => entity.charAt(0) !== '!');
@ -223,7 +237,7 @@ export async function fetchScenesGraphql(query, req) {
}, req.user); }, req.user);
return { return {
nodes: scenes, nodes: scenes.map((scene) => attachResolvers(scene)),
total, total,
/* restrict until deemed essential for 3rd party apps /* restrict until deemed essential for 3rd party apps
aggregates: { aggregates: {
@ -252,10 +266,10 @@ export async function fetchScenesByIdGraphql(query, req) {
}); });
if (query.ids) { if (query.ids) {
return scenes; return scenes.map((scene) => attachResolvers(scene));
} }
return scenes[0]; return attachResolvers(scenes[0]);
} }
async function fetchSceneRevisionsApi(req, res) { async function fetchSceneRevisionsApi(req, res) {