From 90f02aa1c56dc759410010f39263ad63205bba12 Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Wed, 25 Sep 2024 13:10:41 -0400 Subject: [PATCH] feat: make notification id logic --- api/package-lock.json | 39 ++++++++++++ api/package.json | 1 + .../utils/files/safe-ini-serializer.test.ts | 62 ++++++++++++++----- .../notifications/notifications.service.ts | 32 +++++++++- 4 files changed, 115 insertions(+), 19 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index c8db593a1..06736291c 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -46,6 +46,7 @@ "dockerode": "^3.3.5", "dotenv": "^16.4.5", "express": "^4.19.2", + "filenamify": "^6.0.0", "find-process": "^1.4.7", "global-agent": "^3.0.0", "graphql": "^16.8.1", @@ -10186,6 +10187,31 @@ "license": "MIT", "optional": true }, + "node_modules/filename-reserved-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", + "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/filenamify": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz", + "integrity": "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==", + "dependencies": { + "filename-reserved-regex": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fill-range": { "version": "7.0.1", "license": "MIT", @@ -26222,6 +26248,19 @@ "version": "1.0.0", "optional": true }, + "filename-reserved-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", + "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==" + }, + "filenamify": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz", + "integrity": "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==", + "requires": { + "filename-reserved-regex": "^3.0.0" + } + }, "fill-range": { "version": "7.0.1", "requires": { diff --git a/api/package.json b/api/package.json index 0e3fa8fd4..80667e0a4 100644 --- a/api/package.json +++ b/api/package.json @@ -97,6 +97,7 @@ "dockerode": "^3.3.5", "dotenv": "^16.4.5", "express": "^4.19.2", + "filenamify": "^6.0.0", "find-process": "^1.4.7", "global-agent": "^3.0.0", "graphql": "^16.8.1", diff --git a/api/src/__test__/core/utils/files/safe-ini-serializer.test.ts b/api/src/__test__/core/utils/files/safe-ini-serializer.test.ts index 87b3a82a2..e2e519369 100644 --- a/api/src/__test__/core/utils/files/safe-ini-serializer.test.ts +++ b/api/src/__test__/core/utils/files/safe-ini-serializer.test.ts @@ -4,32 +4,32 @@ import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-seria import { Serializer } from 'multi-ini'; test('MultiIni breaks when serializing an object with a boolean inside', async () => { - const objectToSerialize = { - root: { - anonMode: false, - }, - }; - const serializer = new Serializer({ keep_quotes: false }); - expect(serializer.serialize(objectToSerialize)).toMatchInlineSnapshot(` + const objectToSerialize = { + root: { + anonMode: false, + }, + }; + const serializer = new Serializer({ keep_quotes: false }); + expect(serializer.serialize(objectToSerialize)).toMatchInlineSnapshot(` "[root] anonMode=false " - `) + `); }); test('MultiIni can safely serialize an object with a boolean inside', async () => { - const objectToSerialize = { - root: { - anonMode: false, - }, - }; - expect(safelySerializeObjectToIni(objectToSerialize)).toMatchInlineSnapshot(` + const objectToSerialize = { + root: { + anonMode: false, + }, + }; + expect(safelySerializeObjectToIni(objectToSerialize)).toMatchInlineSnapshot(` "[root] anonMode="false" " `); - const result = safelySerializeObjectToIni(objectToSerialize); - expect(parse(result)).toMatchInlineSnapshot(` + const result = safelySerializeObjectToIni(objectToSerialize); + expect(parse(result)).toMatchInlineSnapshot(` { "root": { "anonMode": false, @@ -37,3 +37,33 @@ test('MultiIni can safely serialize an object with a boolean inside', async () = } `); }); + +test('Can serialize top-level fields', async () => { + const objectToSerialize = { + id: 'an-id', + message: 'hello-world', + number: 1, + float: 1.1, + flag: true, + flag2: false, + item: undefined, + missing: null, + empty: {}, + }; + + const expected = ` + "id=an-id + message=hello-world + number=1 + float=1.1 + flag="true" + flag2="false" + [empty] + " + ` + .split('\n') + .map((line) => line.trim()) + .join('\n'); + + expect(safelySerializeObjectToIni({ objectToSerialize })).toMatchInlineSnapshot(expected); +}); diff --git a/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts b/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts index 5cf929a5e..65f83e0a8 100644 --- a/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts +++ b/api/src/unraid-api/graph/resolvers/notifications/notifications.service.ts @@ -19,7 +19,9 @@ import { FSWatcher, watch } from 'chokidar'; import { FileLoadStatus } from '@app/store/types'; import { pubsub, PUBSUB_CHANNEL } from '@app/core/pubsub'; import { fileExists } from '@app/core/utils/files/file-exists'; -import { safelySerializeObjectToIni as encodeIni } from '@app/core/utils/files/safe-ini-serializer'; +// import { safelySerializeObjectToIni as encodeIni } from '@app/core/utils/files/safe-ini-serializer'; +import { encode as encodeIni } from 'ini'; +import { v7 as uuidv7 } from 'uuid'; @Injectable() export class NotificationsService { @@ -131,12 +133,13 @@ export class NotificationsService { *------------------------------------------------------------------------**/ public async createNotification(data: NotificationData): Promise { - // const id: string = this.makeNotificationId(); - const id: string = '_DEV_CUSTOM_NOTIFICATION_1234.notify'; // placeholder + const id: string = await this.makeNotificationId(data.title); const path = join(this.paths().UNREAD, id); const fileData = this.makeNotificationFileData(data); + this.logger.debug(`[createNotification] FileData: ${JSON.stringify(fileData, null, 4)}`); const ini = encodeIni(fileData); + // this.logger.debug(`[createNotification] INI: ${ini}`); await writeFile(path, ini); // await this.addToOverview(notification); @@ -144,6 +147,29 @@ export class NotificationsService { return { ...data, id, type: NotificationType.UNREAD, timestamp: fileData.timestamp }; } + private async makeNotificationId(eventTitle: string, replacement = '_'): Promise { + const { default: filenamify } = await import('filenamify'); + const allWhitespace = /\s+/g; + // replace symbols & whitespace with underscores + const prefix = filenamify(eventTitle, { replacement }).replace(allWhitespace, replacement); + + /**----------------------- + * Why UUIDv7? + * + * So we can sort notifications chronologically + * without having to read the contents of the files. + * + * This makes it more annoying to manually distinguish id's because + * the start of the uuid encodes the timestamp, and the random bits + * are at the end, so the first few chars of each uuid might be relatively common. + * + * See https://uuid7.com/ for an overview of UUIDv7 + * See https://park.is/blog_posts/20240803_extracting_timestamp_from_uuid_v7/ for how + * timestamps are encoded + *------------------------**/ + return `${prefix}_${uuidv7()}.notify`; + } + private makeNotificationFileData(notification: NotificationData): NotificationIni { const { title, subject, description, link, importance } = notification; const secondsSinceUnixEpoch = Math.floor(Date.now() / 1_000);