Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ export class RCloneMutations {
deleteRCloneRemote!: boolean;
}

@ObjectType({
description: 'Notification related mutations',
})
export class NotificationMutations {}

@ObjectType()
export class RootMutations {
@Field(() => ArrayMutations, { description: 'Array related mutations' })
Expand All @@ -59,4 +64,7 @@ export class RootMutations {

@Field(() => RCloneMutations, { description: 'RClone related mutations' })
rclone: RCloneMutations = new RCloneMutations();

@Field(() => NotificationMutations, { description: 'Notification related mutations' })
notifications: NotificationMutations = new NotificationMutations();
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ApiKeyMutations,
ArrayMutations,
DockerMutations,
NotificationMutations,
ParityCheckMutations,
RCloneMutations,
RootMutations,
Expand Down Expand Up @@ -41,4 +42,9 @@ export class RootMutationsResolver {
rclone(): RCloneMutations {
return new RCloneMutations();
}

@Mutation(() => NotificationMutations, { name: 'notifications' })
notifications(): NotificationMutations {
return new NotificationMutations();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Field, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql';
import { Field, ID, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql';

import { Node } from '@unraid/shared/graphql.model.js';
import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator';
Expand All @@ -14,6 +14,18 @@ export enum NotificationImportance {
WARNING = 'WARNING',
}

export enum NotificationJobState {
QUEUED = 'QUEUED',
RUNNING = 'RUNNING',
SUCCEEDED = 'SUCCEEDED',
FAILED = 'FAILED',
}

export enum NotificationJobOperation {
ARCHIVE_ALL = 'ARCHIVE_ALL',
DELETE = 'DELETE',
}

// Register enums with GraphQL
registerEnumType(NotificationType, {
name: 'NotificationType',
Expand All @@ -23,6 +35,14 @@ registerEnumType(NotificationImportance, {
name: 'NotificationImportance',
});

registerEnumType(NotificationJobState, {
name: 'NotificationJobState',
});

registerEnumType(NotificationJobOperation, {
name: 'NotificationJobOperation',
});

@InputType('NotificationFilter')
export class NotificationFilter {
@Field(() => NotificationImportance, { nullable: true })
Expand Down Expand Up @@ -164,4 +184,28 @@ export class Notifications extends Node {
@Field(() => [Notification])
@IsNotEmpty()
list!: Notification[];

@Field(() => NotificationJob, { nullable: true })
job?: NotificationJob;
}

@ObjectType()
export class NotificationJob {
@Field(() => ID)
id!: string;

@Field(() => NotificationJobOperation)
operation!: NotificationJobOperation;

@Field(() => NotificationJobState)
state!: NotificationJobState;

@Field(() => Int)
processed!: number;

@Field(() => Int)
total!: number;

@Field({ nullable: true })
error?: string | null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Args, ResolveField, Resolver } from '@nestjs/graphql';

import { AuthAction, Resource } from '@unraid/shared/graphql.model.js';
import { PrefixedID } from '@unraid/shared/prefixed-id-scalar.js';
import { UsePermissions } from '@unraid/shared/use-permissions.directive.js';

import { AppError } from '@app/core/errors/app-error.js';
import {
NotificationImportance,
NotificationJob,
NotificationJobState,
NotificationMutations,
NotificationOverview,
NotificationType,
} from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js';
Comment on lines +8 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix NotificationMutations import to resolve CI failure

CI error shows NotificationMutations is not exported from notifications.model.ts, and in this file it is imported from that module. The type is actually defined in mutation.model.ts, so this import is invalid and breaks the build.

Update the imports to pull NotificationMutations from the correct module:

-import {
-    NotificationImportance,
-    NotificationJob,
-    NotificationJobState,
-    NotificationMutations,
-    NotificationOverview,
-    NotificationType,
-} from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js';
+import {
+    NotificationImportance,
+    NotificationJob,
+    NotificationJobState,
+    NotificationOverview,
+    NotificationType,
+} from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js';
+import { NotificationMutations } from '@app/unraid-api/graph/resolvers/mutation/mutation.model.js';
🧰 Tools
🪛 GitHub Actions: CI - Main (API)

[error] 12-12: "NotificationMutations" is not exported by "src/unraid-api/graph/resolvers/notifications/notifications.model.ts", imported by "src/unraid-api/graph/resolvers/notifications/notifications.mutations.resolver.ts".

🤖 Prompt for AI Agents
In
api/src/unraid-api/graph/resolvers/notifications/notifications.mutations.resolver.ts
around lines 8 to 15, the import list incorrectly pulls NotificationMutations
from notifications.model.js (which doesn't export it) causing CI to fail; remove
NotificationMutations from the import statement here and add an import of
NotificationMutations from the correct module (mutation.model.ts) using the
appropriate relative path, then run type-check/build to verify the error is
resolved.

import { NotificationsService } from '@app/unraid-api/graph/resolvers/notifications/notifications.service.js';

@Resolver(() => NotificationMutations)
export class NotificationMutationsResolver {
constructor(private readonly notificationsService: NotificationsService) {}

@ResolveField(() => NotificationOverview)
@UsePermissions({
action: AuthAction.DELETE_ANY,
resource: Resource.NOTIFICATIONS,
})
public async delete(
@Args('id', { type: () => PrefixedID }) id: string,
@Args('type', { type: () => NotificationType }) type: NotificationType
): Promise<NotificationOverview> {
const { overview } = await this.notificationsService.deleteNotification({ id, type });
return overview;
}

@ResolveField(() => NotificationJob)
@UsePermissions({
action: AuthAction.DELETE_ANY,
resource: Resource.NOTIFICATIONS,
})
public async startArchiveAll(
@Args('importance', { type: () => NotificationImportance, nullable: true })
importance?: NotificationImportance
): Promise<NotificationJob> {
return this.notificationsService.startArchiveAllJob(importance);
}

@ResolveField(() => NotificationJob)
@UsePermissions({
action: AuthAction.DELETE_ANY,
resource: Resource.NOTIFICATIONS,
})
public async startDeleteAll(
@Args('type', { type: () => NotificationType, nullable: true }) type?: NotificationType
): Promise<NotificationJob> {
const job = await this.notificationsService.startDeleteAllJob(type);
if (job.state === NotificationJobState.FAILED) {
throw new AppError(job.error ?? 'Failed to delete notifications', 500);
}
return job;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
NotificationData,
NotificationFilter,
NotificationImportance,
NotificationJob,
NotificationOverview,
Notifications,
NotificationType,
Expand Down Expand Up @@ -41,6 +42,11 @@ export class NotificationsResolver {
return this.notificationsService.getOverview();
}

@ResolveField(() => NotificationJob, { nullable: true })
public job(@Args('id') jobId: string): NotificationJob {
return this.notificationsService.getJob(jobId);
}

@ResolveField(() => [Notification])
public async list(
@Args('filter', { type: () => NotificationFilter })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ import {
NotificationData,
NotificationFilter,
NotificationImportance,
NotificationJob,
NotificationJobOperation,
NotificationJobState,
NotificationOverview,
NotificationType,
} from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js';
import { validateObject } from '@app/unraid-api/graph/resolvers/validation.utils.js';
import { BackgroundJobsService } from '@app/unraid-api/graph/services/background-jobs.service.js';
import { SortFn } from '@app/unraid-api/types/util.js';
import { batchProcess, formatDatetime, isFulfilled, isRejected, unraidTimestamp } from '@app/utils.js';

Expand Down Expand Up @@ -55,7 +59,36 @@ export class NotificationsService {
},
};

constructor() {
private createJob(operation: NotificationJobOperation, total: number): NotificationJob {
const state = total === 0 ? NotificationJobState.SUCCEEDED : NotificationJobState.QUEUED;
return this.backgroundJobs.createJob<NotificationJobOperation, NotificationJobState>({
operation,
total,
initialState: state,
prefix: operation.toLowerCase(),
});
}

private updateJob(job: NotificationJob) {
this.backgroundJobs.updateJob(job);
}

private async runJob(job: NotificationJob, work: () => Promise<void>) {
await this.backgroundJobs.runJob<NotificationJobState>({
job,
work,
runningState: NotificationJobState.RUNNING,
successState: NotificationJobState.SUCCEEDED,
failureState: NotificationJobState.FAILED,
logContext: 'notification-job',
});
}

public getJob(jobId: string): NotificationJob {
return this.backgroundJobs.getJob<NotificationJobOperation, NotificationJobState>(jobId);
}

constructor(private readonly backgroundJobs: BackgroundJobsService) {
this.path = getters.dynamix().notify!.path;
void this.getNotificationsWatcher(this.path);
}
Expand Down Expand Up @@ -323,6 +356,30 @@ export class NotificationsService {
return this.getOverview();
}

public async startDeleteAllJob(type?: NotificationType): Promise<NotificationJob> {
const targets = type ? [type] : [NotificationType.ARCHIVE, NotificationType.UNREAD];
const queue: Array<{ id: string; type: NotificationType }> = [];

for (const targetType of targets) {
const ids = await this.listFilesInFolder(this.paths()[targetType]);
ids.forEach((id) => queue.push({ id, type: targetType }));
}

const job = this.createJob(NotificationJobOperation.DELETE, queue.length);

setImmediate(() =>
this.runJob(job, async () => {
for (const entry of queue) {
await this.deleteNotification({ id: entry.id, type: entry.type });
job.processed += 1;
this.updateJob(job);
}
})
);

return job;
}

/**
* Deletes all notifications from disk while preserving the directory structure.
* Resets overview stats to zero.
Expand Down Expand Up @@ -485,6 +542,35 @@ export class NotificationsService {
return { ...stats, overview: overviewSnapshot };
}

public async startArchiveAllJob(importance?: NotificationImportance): Promise<NotificationJob> {
const { UNREAD } = this.paths();
const unreads = await this.listFilesInFolder(UNREAD);
const [notifications] = await this.loadNotificationsFromPaths(
unreads,
importance ? { importance } : {}
);
const job = this.createJob(NotificationJobOperation.ARCHIVE_ALL, notifications.length);

setImmediate(() =>
this.runJob(job, async () => {
const overviewSnapshot = this.getOverview();
const archive = this.moveNotification({
from: NotificationType.UNREAD,
to: NotificationType.ARCHIVE,
snapshot: overviewSnapshot,
});

for (const notification of notifications) {
await archive(notification);
job.processed += 1;
this.updateJob(job);
}
})
);

return job;
}

public async unarchiveAll(importance?: NotificationImportance) {
const { ARCHIVE } = this.paths();

Expand Down
4 changes: 4 additions & 0 deletions api/src/unraid-api/graph/resolvers/resolvers.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { InfoModule } from '@app/unraid-api/graph/resolvers/info/info.module.js'
import { LogsModule } from '@app/unraid-api/graph/resolvers/logs/logs.module.js';
import { MetricsModule } from '@app/unraid-api/graph/resolvers/metrics/metrics.module.js';
import { RootMutationsResolver } from '@app/unraid-api/graph/resolvers/mutation/mutation.resolver.js';
import { NotificationMutationsResolver } from '@app/unraid-api/graph/resolvers/notifications/notifications.mutations.resolver.js';
import { NotificationsResolver } from '@app/unraid-api/graph/resolvers/notifications/notifications.resolver.js';
import { NotificationsService } from '@app/unraid-api/graph/resolvers/notifications/notifications.service.js';
import { OnlineResolver } from '@app/unraid-api/graph/resolvers/online/online.resolver.js';
Expand All @@ -29,6 +30,7 @@ import { VarsResolver } from '@app/unraid-api/graph/resolvers/vars/vars.resolver
import { VmMutationsResolver } from '@app/unraid-api/graph/resolvers/vms/vms.mutations.resolver.js';
import { VmsResolver } from '@app/unraid-api/graph/resolvers/vms/vms.resolver.js';
import { VmsService } from '@app/unraid-api/graph/resolvers/vms/vms.service.js';
import { BackgroundJobsService } from '@app/unraid-api/graph/services/background-jobs.service.js';
import { ServicesModule } from '@app/unraid-api/graph/services/services.module.js';
import { ServicesResolver } from '@app/unraid-api/graph/services/services.resolver.js';
import { SharesResolver } from '@app/unraid-api/graph/shares/shares.resolver.js';
Expand Down Expand Up @@ -57,8 +59,10 @@ import { MeResolver } from '@app/unraid-api/graph/user/user.resolver.js';
ConfigResolver,
FlashResolver,
MeResolver,
NotificationMutationsResolver,
NotificationsResolver,
NotificationsService,
BackgroundJobsService,
OnlineResolver,
OwnerResolver,
RegistrationResolver,
Expand Down
Loading
Loading