mirror of
https://github.com/unraid/api.git
synced 2026-01-11 02:59:59 -06:00
98 lines
3.6 KiB
TypeScript
98 lines
3.6 KiB
TypeScript
import { Inject, Injectable, Logger } from '@nestjs/common';
|
|
|
|
import type { InternalGraphQLClientFactory } from '@unraid/shared';
|
|
import { ApolloClient, NormalizedCacheObject } from '@apollo/client/core/index.js';
|
|
import { INTERNAL_CLIENT_SERVICE_TOKEN } from '@unraid/shared';
|
|
|
|
import { AdminKeyService } from '@app/unraid-api/cli/admin-key.service.js';
|
|
|
|
/**
|
|
* Internal GraphQL client for CLI commands.
|
|
*
|
|
* This service creates an Apollo client that queries the local API server
|
|
* with admin privileges for CLI operations.
|
|
*/
|
|
@Injectable()
|
|
export class CliInternalClientService {
|
|
private readonly logger = new Logger(CliInternalClientService.name);
|
|
private client: ApolloClient<NormalizedCacheObject> | null = null;
|
|
private creatingClient: Promise<ApolloClient<NormalizedCacheObject>> | null = null;
|
|
|
|
constructor(
|
|
@Inject(INTERNAL_CLIENT_SERVICE_TOKEN)
|
|
private readonly clientFactory: InternalGraphQLClientFactory,
|
|
private readonly adminKeyService: AdminKeyService
|
|
) {}
|
|
|
|
/**
|
|
* Get the admin API key using the AdminKeyService.
|
|
* This ensures the key exists and is available for CLI operations.
|
|
*/
|
|
private async getLocalApiKey(): Promise<string> {
|
|
try {
|
|
return await this.adminKeyService.getOrCreateLocalAdminKey();
|
|
} catch (error) {
|
|
this.logger.error('Failed to get admin API key:', error);
|
|
throw new Error(
|
|
'Unable to get admin API key for internal client. Ensure the API server is running.'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the default CLI client with admin API key.
|
|
* This is for CLI commands that need admin access.
|
|
*/
|
|
public async getClient(): Promise<ApolloClient<NormalizedCacheObject>> {
|
|
// If client already exists, return it
|
|
if (this.client) {
|
|
return this.client;
|
|
}
|
|
|
|
// If another call is already creating the client, wait for it
|
|
if (this.creatingClient) {
|
|
return await this.creatingClient;
|
|
}
|
|
|
|
// Start creating the client with race condition protection
|
|
let creationPromise!: Promise<ApolloClient<NormalizedCacheObject>>;
|
|
// eslint-disable-next-line prefer-const
|
|
creationPromise = (async () => {
|
|
try {
|
|
const client = await this.clientFactory.createClient({
|
|
getApiKey: () => this.getLocalApiKey(),
|
|
enableSubscriptions: false, // CLI doesn't need subscriptions
|
|
});
|
|
|
|
// awaiting *before* checking this.creatingClient is important!
|
|
// by yielding to the event loop, it ensures
|
|
// `this.creatingClient = creationPromise;` is executed before the next check.
|
|
|
|
// This prevents race conditions where the client is assigned to the wrong instance.
|
|
// Only assign client if this creation is still current
|
|
if (this.creatingClient === creationPromise) {
|
|
this.client = client;
|
|
this.logger.debug('Created CLI internal GraphQL client with admin privileges');
|
|
}
|
|
|
|
return client;
|
|
} finally {
|
|
// Only clear if this creation is still current
|
|
if (this.creatingClient === creationPromise) {
|
|
this.creatingClient = null;
|
|
}
|
|
}
|
|
})();
|
|
|
|
this.creatingClient = creationPromise;
|
|
return await creationPromise;
|
|
}
|
|
|
|
public clearClient() {
|
|
// Stop the Apollo client to terminate any active processes
|
|
this.client?.stop();
|
|
this.client = null;
|
|
this.creatingClient = null;
|
|
}
|
|
}
|