mirror of
https://github.com/unraid/api.git
synced 2026-01-04 07:29:48 -06:00
feat: make notification id logic
This commit is contained in:
39
api/package-lock.json
generated
39
api/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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<Notification> {
|
||||
// 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<string> {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user