feature/IO-3096-GlobalNotifications - Checkpoint, Ratify notifications tb table.

This commit is contained in:
Dave Richer
2025-02-24 12:52:10 -05:00
parent a5cf81bd28
commit 0f067fc503
16 changed files with 101 additions and 82 deletions

View File

@@ -4893,46 +4893,78 @@
- name: job - name: job
using: using:
foreign_key_constraint_on: jobid foreign_key_constraint_on: jobid
insert_permissions:
- role: user
permission:
check:
job:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- scenario_meta
- scenario_text
- fcm_text
- created_at
- read
- updated_at
- associationid
- id
- jobid
comment: ""
select_permissions: select_permissions:
- role: user - role: user
permission: permission:
columns: columns:
- associationid - scenario_meta
- scenario_text
- fcm_text
- created_at - created_at
- fcm_data - read
- fcm_message - updated_at
- fcm_title - associationid
- id - id
- jobid - jobid
- meta
- read
- ui_translation_meta
- ui_translation_string
- updated_at
filter: filter:
association: job:
_and: bodyshop:
- active: associations:
_eq: true _and:
- user: - user:
authid: authid:
_eq: X-Hasura-User-Id _eq: X-Hasura-User-Id
- active:
_eq: true
comment: "" comment: ""
update_permissions: update_permissions:
- role: user - role: user
permission: permission:
columns: columns:
- meta - scenario_meta
- scenario_text
- fcm_text
- created_at
- read - read
filter: - updated_at
association: - associationid
_and: - id
- active: - jobid
_eq: true filter: {}
- user: check:
authid: job:
_eq: X-Hasura-User-Id bodyshop:
check: null associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
comment: "" comment: ""
- table: - table:
name: owners name: owners

View File

@@ -0,0 +1,3 @@
comment on column "public"."notifications"."html_body" is E'Real Time Notifications System';
alter table "public"."notifications" alter column "html_body" drop not null;
alter table "public"."notifications" add column "html_body" text;

View File

@@ -0,0 +1 @@
alter table "public"."notifications" drop column "html_body" cascade;

View File

@@ -0,0 +1,4 @@
comment on column "public"."notifications"."fcm_data" is E'Real Time Notifications System';
alter table "public"."notifications" alter column "fcm_data" set default jsonb_build_object();
alter table "public"."notifications" alter column "fcm_data" drop not null;
alter table "public"."notifications" add column "fcm_data" jsonb;

View File

@@ -0,0 +1 @@
alter table "public"."notifications" drop column "fcm_data" cascade;

View File

@@ -0,0 +1,3 @@
comment on column "public"."notifications"."fcm_message" is E'Real Time Notifications System';
alter table "public"."notifications" alter column "fcm_message" drop not null;
alter table "public"."notifications" add column "fcm_message" text;

View File

@@ -0,0 +1 @@
alter table "public"."notifications" drop column "fcm_message" cascade;

View File

@@ -0,0 +1,3 @@
comment on column "public"."notifications"."ui_translation_string" is E'Real Time Notifications System';
alter table "public"."notifications" alter column "ui_translation_string" drop not null;
alter table "public"."notifications" add column "ui_translation_string" text;

View File

@@ -0,0 +1 @@
alter table "public"."notifications" drop column "ui_translation_string" cascade;

View File

@@ -0,0 +1 @@
alter table "public"."notifications" rename column "fcm_text" to "fcm_title";

View File

@@ -0,0 +1 @@
alter table "public"."notifications" rename column "fcm_title" to "fcm_text";

View File

@@ -0,0 +1 @@
alter table "public"."notifications" rename column "scenario_text" to "ui_translation_meta";

View File

@@ -0,0 +1 @@
alter table "public"."notifications" rename column "ui_translation_meta" to "scenario_text";

View File

@@ -0,0 +1 @@
alter table "public"."notifications" rename column "scenario_meta" to "meta";

View File

@@ -0,0 +1 @@
alter table "public"."notifications" rename column "meta" to "scenario_meta";

View File

@@ -20,7 +20,7 @@ const RATE_LIMITER_DURATION = APP_CONSOLIDATION_DELAY * 0.1; // 6 seconds (tenth
let addQueue; let addQueue;
let consolidateQueue; let consolidateQueue;
// GraphQL mutation to insert notifications // Updated GraphQL mutation to insert notifications with the new schema
const INSERT_NOTIFICATIONS_MUTATION = ` const INSERT_NOTIFICATIONS_MUTATION = `
mutation INSERT_NOTIFICATIONS($objects: [notifications_insert_input!]!) { mutation INSERT_NOTIFICATIONS($objects: [notifications_insert_input!]!) {
insert_notifications(objects: $objects) { insert_notifications(objects: $objects) {
@@ -29,61 +29,34 @@ const INSERT_NOTIFICATIONS_MUTATION = `
id id
jobid jobid
associationid associationid
ui_translation_string scenario_text
ui_translation_meta fcm_text
html_body scenario_meta
} }
} }
} }
`; `;
/** /**
* Builds an HTML unordered list from an array of notification bodies. * Builds the scenario_text, fcm_text, and scenario_meta for a batch of notifications.
* *
* @param {Array<Object>} notifications - Array of notification objects with a 'body' field. * @param {Array<Object>} notifications - Array of notification objects with 'body' and 'variables'.
* @returns {string} HTML string representing an unordered list of bodies. * @returns {Object} An object with 'scenario_text', 'fcm_text', and 'scenario_meta'.
*/ */
const buildHtmlBody = (notifications) => { const buildNotificationContent = (notifications) => {
const listItems = notifications.map((n) => `<li>${n.body}</li>`).join(""); const scenarioText = notifications.map((n) => n.body); // Array of text entries
return `<ul>${listItems}</ul>`; const fcmText = scenarioText.join(". "); // Concatenated text with period separator
}; const scenarioMeta = notifications.map((n) => n.variables || {}); // Array of metadata objects
/** return {
* Determines the key and variables for a batch of notifications. scenario_text: scenarioText,
* fcm_text: fcmText ? `${fcmText}.` : null, // Add trailing period if non-empty, otherwise null
* @param {Array<Object>} notifications - Array of notification objects with 'key' and 'variables'. scenario_meta: scenarioMeta
* @returns {Object} An object with 'key' and 'variables' properties. };
*/
const determineKeyAndVariables = (notifications) => {
if (notifications.length === 1) {
// Single notification: use the original key and variables
return {
key: notifications[0].key,
variables: notifications[0].variables
};
} else {
// Multiple notifications: use a generic key and consolidate variables with their original keys
return {
key: "notifications.job.multipleChanges",
variables: {
variables: notifications.map((n) => ({
key: n.key, // Include the original key in each variables object
...n.variables
}))
}
};
}
}; };
/** /**
* Initializes the notification queues and workers for adding and consolidating notifications. * Initializes the notification queues and workers for adding and consolidating notifications.
*
* @param {Object} options - Configuration options for queue initialization.
* @param {Object} options.pubClient - Redis client instance for queue communication.
* @param {Object} options.logger - Logger instance for logging events and debugging.
* @param {Object} options.redisHelpers - Utility functions for Redis operations.
* @param {Object} options.ioRedis - Socket.io Redis adapter for real-time event emission.
* @returns {Queue} The initialized `addQueue` instance for dispatching notifications.
*/ */
const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => { const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
if (!addQueue || !consolidateQueue) { if (!addQueue || !consolidateQueue) {
@@ -195,14 +168,13 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
const employeeId = userRecipients[0]?.employeeId; const employeeId = userRecipients[0]?.employeeId;
for (const [bodyShopId, notifications] of Object.entries(bodyShopData)) { for (const [bodyShopId, notifications] of Object.entries(bodyShopData)) {
const { key, variables } = determineKeyAndVariables(notifications); const { scenario_text, fcm_text, scenario_meta } = buildNotificationContent(notifications);
const htmlBody = buildHtmlBody(notifications);
notificationInserts.push({ notificationInserts.push({
jobid: jobId, jobid: jobId,
associationid: employeeId || null, associationid: employeeId || null,
ui_translation_string: key, scenario_text: JSON.stringify(scenario_text), // JSONB requires stringified input
ui_translation_meta: JSON.stringify(variables), fcm_text: fcm_text,
html_body: htmlBody scenario_meta: JSON.stringify(scenario_meta) // JSONB requires stringified input
}); });
notificationIdMap.set(`${user}:${bodyShopId}`, null); notificationIdMap.set(`${user}:${bodyShopId}`, null);
} }
@@ -301,9 +273,6 @@ const loadAppQueue = async ({ pubClient, logger, redisHelpers, ioRedis }) => {
/** /**
* Retrieves the initialized `addQueue` instance. * Retrieves the initialized `addQueue` instance.
*
* @returns {Queue} The `addQueue` instance for adding notifications.
* @throws {Error} If `addQueue` is not initialized (i.e., `loadAppQueue` wasnt called).
*/ */
const getQueue = () => { const getQueue = () => {
if (!addQueue) throw new Error("Add queue not initialized. Ensure loadAppQueue is called during bootstrap."); if (!addQueue) throw new Error("Add queue not initialized. Ensure loadAppQueue is called during bootstrap.");
@@ -312,11 +281,6 @@ const getQueue = () => {
/** /**
* Dispatches notifications to the `addQueue` for processing. * Dispatches notifications to the `addQueue` for processing.
*
* @param {Object} options - Options for dispatching notifications.
* @param {Array} options.appsToDispatch - Array of notification objects to dispatch.
* @param {Object} options.logger - Logger instance for logging dispatch events.
* @returns {Promise<void>} Resolves when all notifications are added to the queue.
*/ */
const dispatchAppsToQueue = async ({ appsToDispatch, logger }) => { const dispatchAppsToQueue = async ({ appsToDispatch, logger }) => {
const appQueue = getQueue(); const appQueue = getQueue();