refactor(api): use nestjs modules as the api plugin interface (#1321)

Changes plugin interface to expect Nest modules instead of a custom
plain JS object.

## Summary by CodeRabbit

- **New Features**
- Introduced updated health check plugins, including a new connect
plugin exposing a GraphQL health query and configuration logging.

- **Refactor**
- Streamlined the overall plugin registration and management across API,
CLI, and GraphQL modules for improved clarity and logging.
- Simplified the `CliModule` and `PluginService` to reduce complexity
and enhance maintainability.

- **Chores**
- Updated dependency configurations and build scripts while introducing
new TypeScript setups to enhance maintainability and consistency.
- Added new `package.json` and `tsconfig.json` files for the
`unraid-api-plugin-connect` and `unraid-api-plugin-health` projects.
- Modified GitHub Actions workflow to update tag format for pull
requests.
This commit is contained in:
Pujit Mehrotra
2025-04-08 10:08:32 -04:00
committed by GitHub
parent 8a5b23856c
commit f65788aa94
17 changed files with 373 additions and 340 deletions

View File

@@ -337,7 +337,7 @@ jobs:
uses: ./.github/workflows/build-plugin.yml
with:
RELEASE_CREATED: false
TAG: ${{ github.event.pull_request.number }}
TAG: PR${{ github.event.pull_request.number }}
BUCKET_PATH: ${{ github.event.pull_request.number && format('unraid-api/tag/PR{0}', github.event.pull_request.number) || 'unraid-api' }}
BASE_URL: "https://preview.dl.unraid.net/unraid-api"
secrets:

View File

@@ -62,6 +62,7 @@
"@jsonforms/core": "^3.5.1",
"@nestjs/apollo": "^13.0.3",
"@nestjs/common": "^11.0.11",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.11",
"@nestjs/graphql": "^13.0.3",
"@nestjs/passport": "^11.0.0",

View File

@@ -20,16 +20,7 @@ const getUnraidApiLocation = async () => {
};
try {
// Register plugins and create a dynamic module configuration
const dynamicModule = await CliModule.registerWithPlugins();
// Create a new class that extends CliModule with the dynamic configuration
const DynamicCliModule = class extends CliModule {
static module = dynamicModule.module;
static imports = dynamicModule.imports;
static providers = dynamicModule.providers;
};
await CommandFactory.run(DynamicCliModule, {
await CommandFactory.run(CliModule, {
cliName: 'unraid-api',
logger: LOG_LEVEL === 'TRACE' ? new LogService() : false, // - enable this to see nest initialization issues
completion: {

View File

@@ -47,7 +47,7 @@ import { UnraidFileModifierModule } from '@app/unraid-api/unraid-file-modifier/u
},
]),
UnraidFileModifierModule,
PluginModule.registerPlugins(),
PluginModule.register(),
],
controllers: [],
providers: [

View File

@@ -1,6 +1,4 @@
import { DynamicModule, Module, Provider, Type } from '@nestjs/common';
import { CommandRunner } from 'nest-commander';
import { Module } from '@nestjs/common';
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
import { AddApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/add-api-key.questions.js';
@@ -26,9 +24,7 @@ import { StatusCommand } from '@app/unraid-api/cli/status.command.js';
import { StopCommand } from '@app/unraid-api/cli/stop.command.js';
import { SwitchEnvCommand } from '@app/unraid-api/cli/switch-env.command.js';
import { VersionCommand } from '@app/unraid-api/cli/version.command.js';
import { ApiPluginDefinition } from '@app/unraid-api/plugin/plugin.interface.js';
import { PluginModule } from '@app/unraid-api/plugin/plugin.module.js';
import { PluginService } from '@app/unraid-api/plugin/plugin.service.js';
import { PluginCliModule } from '@app/unraid-api/plugin/plugin.module.js';
const DEFAULT_COMMANDS = [
ApiKeyCommand,
@@ -60,43 +56,8 @@ const DEFAULT_PROVIDERS = [
ApiKeyService,
] as const;
type PluginProvider = Provider & {
provide: string | symbol | Type<any>;
useValue?: ApiPluginDefinition;
};
@Module({
imports: [PluginModule],
imports: [PluginCliModule.register()],
providers: [...DEFAULT_COMMANDS, ...DEFAULT_PROVIDERS],
})
export class CliModule {
/**
* Get all registered commands
* @returns Array of registered command classes
*/
static getCommands(): Type<CommandRunner>[] {
return [...DEFAULT_COMMANDS];
}
/**
* Register the module with plugin support
* @returns DynamicModule configuration including plugin commands
*/
static async registerWithPlugins(): Promise<DynamicModule> {
const pluginModule = await PluginModule.registerPlugins();
// Get commands from plugins
const pluginCommands: Type<CommandRunner>[] = [];
for (const provider of (pluginModule.providers || []) as PluginProvider[]) {
if (provider.provide !== PluginService && provider.useValue?.commands) {
pluginCommands.push(...provider.useValue.commands);
}
}
return {
module: CliModule,
imports: [pluginModule],
providers: [...DEFAULT_COMMANDS, ...DEFAULT_PROVIDERS, ...pluginCommands],
};
}
}
export class CliModule {}

View File

@@ -15,12 +15,10 @@ import {
import { GraphQLLong } from '@app/graphql/resolvers/graphql-type-long.js';
import { loadTypeDefs } from '@app/graphql/schema/loadTypesDefs.js';
import { getters } from '@app/store/index.js';
import { AuthModule } from '@app/unraid-api/auth/auth.module.js';
import { idPrefixPlugin } from '@app/unraid-api/graph/id-prefix-plugin.js';
import { ResolversModule } from '@app/unraid-api/graph/resolvers/resolvers.module.js';
import { sandboxPlugin } from '@app/unraid-api/graph/sandbox-plugin.js';
import { getAuthEnumTypeDefs } from '@app/unraid-api/graph/utils/auth-enum.utils.js';
import { PluginModule } from '@app/unraid-api/plugin/plugin.module.js';
import { PluginService } from '@app/unraid-api/plugin/plugin.service.js';
@Module({
@@ -28,12 +26,10 @@ import { PluginService } from '@app/unraid-api/plugin/plugin.service.js';
ResolversModule,
GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
imports: [PluginModule, AuthModule],
inject: [PluginService],
useFactory: async (pluginService: PluginService) => {
const plugins = await pluginService.getGraphQLConfiguration();
useFactory: async () => {
const pluginSchemas = await PluginService.getGraphQLSchemas();
const authEnumTypeDefs = getAuthEnumTypeDefs();
const typeDefs = print(await loadTypeDefs([plugins.typeDefs, authEnumTypeDefs]));
const typeDefs = print(await loadTypeDefs([...pluginSchemas, authEnumTypeDefs]));
const resolvers = {
DateTime: DateTimeResolver,
JSON: JSONResolver,
@@ -41,9 +37,7 @@ import { PluginService } from '@app/unraid-api/plugin/plugin.service.js';
Port: PortResolver,
URL: URLResolver,
UUID: UUIDResolver,
...plugins.resolvers,
};
return {
introspection: getters.config()?.local?.sandbox === 'yes',
playground: false,

View File

@@ -1,15 +1,6 @@
import { Logger, Type } from '@nestjs/common';
import { CommandRunner } from 'nest-commander';
import type { Constructor } from 'type-fest';
import { z } from 'zod';
import { ApiStore } from '@app/store/index.js';
export interface PluginMetadata {
name: string;
description: string;
}
const asyncArray = () => z.function().returns(z.promise(z.array(z.any())));
const asyncString = () => z.function().returns(z.promise(z.string()));
const asyncVoid = () => z.function().returns(z.promise(z.void()));
@@ -32,30 +23,44 @@ const resolverTypeMap = z.record(
);
const asyncResolver = () => z.function().returns(z.promise(resolverTypeMap));
/** Warning: unstable API. The config mechanism and API may soon change. */
export const apiPluginSchema = z.object({
_type: z.literal('UnraidApiPlugin'),
name: z.string(),
description: z.string(),
commands: z.array(z.custom<Type<CommandRunner>>()),
registerGraphQLResolvers: asyncResolver().optional(),
registerGraphQLTypeDefs: asyncString().optional(),
registerRESTControllers: asyncArray().optional(),
registerRESTRoutes: asyncArray().optional(),
registerServices: asyncArray().optional(),
registerCronJobs: asyncArray().optional(),
// These schema definitions are picked up as nest modules as well.
onModuleInit: asyncVoid().optional(),
onModuleDestroy: asyncVoid().optional(),
});
type NestModule = Constructor<unknown>;
const isClass = (value: unknown): value is NestModule => {
return typeof value === 'function' && value.toString().startsWith('class');
};
/** Warning: unstable API. The config mechanism and API may soon change. */
export type ApiPluginDefinition = z.infer<typeof apiPluginSchema>;
/** format of module exports from a nestjs plugin */
export const apiNestPluginSchema = z
.object({
adapter: z.literal('nestjs'),
ApiModule: z
.custom<NestModule>(isClass, {
message: 'Invalid NestJS module: expected a class constructor',
})
.optional(),
CliModule: z
.custom<NestModule>(isClass, {
message: 'Invalid NestJS module: expected a class constructor',
})
.optional(),
graphqlSchemaExtension: asyncString().optional(),
})
.superRefine((data, ctx) => {
// Ensure that at least one of ApiModule or CliModule is defined.
if (!data.ApiModule && !data.CliModule) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'At least one of ApiModule or CliModule must be defined',
path: ['ApiModule', 'CliModule'],
});
}
// If graphqlSchemaExtension is provided, ensure that ApiModule is defined.
if (data.graphqlSchemaExtension && !data.ApiModule) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'If graphqlSchemaExtension is provided, ApiModule must be defined',
path: ['graphqlSchemaExtension'],
});
}
});
// todo: the blocker to publishing this type is the 'ApiStore' type.
// It pulls in a lot of irrelevant types (e.g. graphql types) and triggers js transpilation of everything related to the store.
// If we can isolate the type, we can publish it to npm and developers can use it as a dev dependency.
/**
* Represents a subclass of UnraidAPIPlugin that can be instantiated.
*/
export type ConstructablePlugin = (options: { store: ApiStore; logger: Logger }) => ApiPluginDefinition;
export type ApiNestPluginDefinition = z.infer<typeof apiNestPluginSchema>;

View File

@@ -7,14 +7,41 @@ export class PluginModule {
private static readonly logger = new Logger(PluginModule.name);
constructor(private readonly pluginService: PluginService) {}
static async registerPlugins(): Promise<DynamicModule> {
static async register(): Promise<DynamicModule> {
const plugins = await PluginService.getPlugins();
const providers = plugins.map((result) => result.provider);
const apiModules = plugins
.filter((plugin) => plugin.ApiModule)
.map((plugin) => plugin.ApiModule!);
const pluginList = apiModules.map((plugin) => plugin.name).join(', ');
PluginModule.logger.log(`Found ${apiModules.length} API plugins: ${pluginList}`);
return {
module: PluginModule,
providers: [PluginService, ...providers],
exports: [PluginService, ...providers.map((p) => p.provide)],
imports: [...apiModules],
providers: [PluginService],
exports: [PluginService],
global: true,
};
}
}
@Module({})
export class PluginCliModule {
private static readonly logger = new Logger(PluginCliModule.name);
static async register(): Promise<DynamicModule> {
const plugins = await PluginService.getPlugins();
const cliModules = plugins
.filter((plugin) => plugin.CliModule)
.map((plugin) => plugin.CliModule!);
const cliList = cliModules.map((plugin) => plugin.name).join(', ');
PluginCliModule.logger.log(`Found ${cliModules.length} CLI plugins: ${cliList}`);
return {
module: PluginCliModule,
imports: [...cliModules],
};
}
}

View File

@@ -1,225 +1,80 @@
import { Injectable, Logger, Provider, Type } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { pascalCase } from 'change-case';
import type { SetRequired } from 'type-fest';
import { parse } from 'graphql';
import type {
ApiPluginDefinition,
ConstructablePlugin,
} from '@app/unraid-api/plugin/plugin.interface.js';
import { getPackageJsonDependencies as getPackageDependencies } from '@app/environment.js';
import { store } from '@app/store/index.js';
import { apiPluginSchema } from '@app/unraid-api/plugin/plugin.interface.js';
import type { ApiNestPluginDefinition } from '@app/unraid-api/plugin/plugin.interface.js';
import { getPackageJson } from '@app/environment.js';
import { apiNestPluginSchema } from '@app/unraid-api/plugin/plugin.interface.js';
import { batchProcess } from '@app/utils.js';
type CustomProvider = Provider & {
provide: string | symbol | Type<any>;
};
type PluginProvider = {
provider: CustomProvider;
pluginInstance: ApiPluginDefinition;
};
@Injectable()
export class PluginService {
private pluginProviders: PluginProvider[] | undefined;
private loadingPromise: Promise<PluginProvider[]> | undefined;
private static readonly logger = new Logger(PluginService.name);
constructor() {
this.loadPlugins();
}
private get plugins() {
return this.pluginProviders?.map((plugin) => plugin.pluginInstance) ?? [];
}
async loadPlugins() {
// If plugins are already loaded, return them
if (this.pluginProviders?.length) {
return this.pluginProviders;
}
// If getPlugins() is already loading, return its promise
if (this.loadingPromise) {
return this.loadingPromise;
}
this.loadingPromise = PluginService.getPlugins()
.then((plugins) => {
if (!this.pluginProviders?.length) {
this.pluginProviders = plugins;
const pluginNames = this.plugins.map((plugin) => plugin.name);
PluginService.logger.debug(
`Registered ${pluginNames.length} plugins: ${pluginNames.join(', ')}`
);
} else {
PluginService.logger.debug(
`${plugins.length} plugins already registered. Skipping registration.`
);
}
return this.pluginProviders;
})
.catch((error) => {
PluginService.logger.error('Error registering plugins', error);
return [];
})
.finally(() => {
// clear loading state
this.loadingPromise = undefined;
});
return this.loadingPromise;
}
private static isPluginFactory(factory: unknown): factory is ConstructablePlugin {
return typeof factory === 'function';
}
private static async getPluginFromPackage(pluginPackage: string): Promise<{
provider: CustomProvider;
pluginInstance: ApiPluginDefinition;
}> {
const moduleImport = await import(/* @vite-ignore */ pluginPackage);
const pluginName = pascalCase(pluginPackage);
const PluginFactory = moduleImport.default || moduleImport[pluginName];
if (!PluginService.isPluginFactory(PluginFactory)) {
throw new Error(`Invalid plugin from ${pluginPackage}. Must export a factory function.`);
}
const logger = new Logger(PluginFactory.name);
const validation = apiPluginSchema.safeParse(PluginFactory({ store, logger }));
if (!validation.success) {
throw new Error(`Invalid plugin from ${pluginPackage}: ${validation.error}`);
}
const pluginInstance = validation.data;
return {
provider: {
provide: PluginFactory.name,
useValue: pluginInstance,
},
pluginInstance,
};
}
private static plugins: Promise<ApiNestPluginDefinition[]> | undefined;
static async getPlugins() {
/** All api plugins must be npm packages whose name starts with this prefix */
const pluginPrefix = 'unraid-api-plugin-';
// All api plugins must be installed as dependencies of the unraid-api package
/** list of npm packages that are unraid-api plugins */
const plugins = getPackageDependencies()?.filter((pkgName) => pkgName.startsWith(pluginPrefix));
if (!plugins) {
PluginService.logger.warn('Could not load dependencies from the Unraid-API package.json');
// Fail silently: Return the module without plugins
return [];
}
PluginService.plugins ??= PluginService.importPlugins();
return PluginService.plugins;
}
const failedPlugins: string[] = [];
const { data: pluginProviders } = await batchProcess(plugins, async (pluginPackage) => {
static async getGraphQLSchemas() {
const plugins = (await PluginService.getPlugins()).filter(
(plugin): plugin is SetRequired<ApiNestPluginDefinition, 'graphqlSchemaExtension'> =>
plugin.graphqlSchemaExtension !== undefined
);
const { data: schemas } = await batchProcess(plugins, async (plugin) => {
try {
return await PluginService.getPluginFromPackage(pluginPackage);
const schema = await plugin.graphqlSchemaExtension();
// Validate schema by parsing it - this will throw if invalid
parse(schema);
return schema;
} catch (error) {
failedPlugins.push(pluginPackage);
PluginService.logger.warn(error);
// we can safely assert ApiModule's presence since we validate the plugin schema upon importing it.
// ApiModule must be defined when graphqlSchemaExtension is defined.
PluginService.logger.error(
`Error parsing GraphQL schema from ${plugin.ApiModule!.name}: ${JSON.stringify(error, null, 2)}`
);
throw error;
}
});
if (failedPlugins.length > 0) {
PluginService.logger.warn(
`${failedPlugins.length} plugins failed to load. Ignoring them: ${failedPlugins.join(', ')}`
);
}
return pluginProviders;
return schemas;
}
async getGraphQLConfiguration() {
await this.loadPlugins();
const plugins = this.plugins;
let combinedResolvers = {};
const typeDefs: string[] = [];
for (const plugin of plugins) {
if (plugin.registerGraphQLResolvers) {
const pluginResolvers = await plugin.registerGraphQLResolvers();
combinedResolvers = {
...combinedResolvers,
...pluginResolvers,
};
}
if (plugin.registerGraphQLTypeDefs) {
const pluginTypeDefs = await plugin.registerGraphQLTypeDefs();
try {
// Validate schema by parsing it - this will throw if invalid
parse(pluginTypeDefs);
typeDefs.push(pluginTypeDefs);
} catch (error) {
const errorMessage = `Plugin ${plugin.name} returned an unusable GraphQL type definition: ${JSON.stringify(
pluginTypeDefs
)}`;
PluginService.logger.warn(errorMessage);
}
}
private static async importPlugins() {
if (PluginService.plugins) {
return PluginService.plugins;
}
const pluginPackages = await PluginService.listPlugins();
const plugins = await batchProcess(pluginPackages, async ([pkgName]) => {
try {
const plugin = await import(/* @vite-ignore */ pkgName);
return apiNestPluginSchema.parse(plugin);
} catch (error) {
PluginService.logger.error(`Plugin from ${pkgName} is invalid`, error);
throw error;
}
});
return {
resolvers: combinedResolvers,
typeDefs: typeDefs.join('\n'),
};
if (plugins.errorOccured) {
PluginService.logger.warn(`Failed to load ${plugins.errors.length} plugins. Ignoring them.`);
}
return plugins.data;
}
async getRESTConfiguration() {
await this.loadPlugins();
const controllers: Type<any>[] = [];
const routes: Record<string, any>[] = [];
for (const plugin of this.plugins) {
if (plugin.registerRESTControllers) {
const pluginControllers = await plugin.registerRESTControllers();
controllers.push(...pluginControllers);
}
if (plugin.registerRESTRoutes) {
const pluginRoutes = await plugin.registerRESTRoutes();
routes.push(...pluginRoutes);
}
private static async listPlugins(): Promise<[string, string][]> {
/** All api plugins must be npm packages whose name starts with this prefix */
const pluginPrefix = 'unraid-api-plugin-';
// All api plugins must be installed as dependencies of the unraid-api package
const { dependencies } = getPackageJson();
if (!dependencies) {
PluginService.logger.warn('Unraid-API dependencies not found; skipping plugins.');
return [];
}
return {
controllers,
routes,
};
}
async getServices() {
await this.loadPlugins();
const services: Type<any>[] = [];
for (const plugin of this.plugins) {
if (plugin.registerServices) {
const pluginServices = await plugin.registerServices();
services.push(...pluginServices);
}
}
return services;
}
async getCronJobs() {
await this.loadPlugins();
const cronJobs: Record<string, any>[] = [];
for (const plugin of this.plugins) {
if (plugin.registerCronJobs) {
const pluginCronJobs = await plugin.registerCronJobs();
cronJobs.push(...pluginCronJobs);
}
}
return cronJobs;
const plugins = Object.entries(dependencies).filter((entry): entry is [string, string] => {
const [pkgName, version] = entry;
return pkgName.startsWith(pluginPrefix) && typeof version === 'string';
});
return plugins;
}
}

View File

@@ -0,0 +1,47 @@
import { Module, Logger, Inject } from "@nestjs/common";
import { ConfigModule, ConfigService, registerAs } from "@nestjs/config";
import { Resolver, Query } from "@nestjs/graphql";
export const adapter = 'nestjs';
export const graphqlSchemaExtension = async () => `
type Query {
health: String
}
`;
@Resolver()
export class HealthResolver {
@Query(() => String)
health() {
// You can replace the return value with your actual health check logic
return 'I am healthy!';
}
}
const config = registerAs("connect", () => ({
demo: true,
}));
@Module({
imports: [ConfigModule.forFeature(config)],
providers: [HealthResolver],
})
class ConnectPluginModule {
logger = new Logger(ConnectPluginModule.name);
private readonly configService: ConfigService;
/**
* @param {ConfigService} configService
*/
constructor(@Inject(ConfigService) configService: ConfigService) {
this.configService = configService;
}
onModuleInit() {
this.logger.log("Connect plugin initialized");
console.log("Connect plugin initialized", this.configService.get('connect'));
}
}
export const ApiModule = ConnectPluginModule;

View File

@@ -0,0 +1,33 @@
{
"name": "unraid-api-plugin-connect",
"version": "1.0.0",
"main": "dist/index.js",
"type": "module",
"files": [
"dist"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc",
"prepare": "npm run build"
},
"keywords": [],
"author": "Lime Technology, Inc. <unraid.net>",
"license": "GPL-2.0-or-later",
"description": "Example Health plugin for Unraid API",
"devDependencies": {
"@nestjs/common": "^11.0.11",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.11",
"@nestjs/graphql": "^13.0.3",
"nest-authz": "^2.14.0",
"typescript": "^5.8.2"
},
"peerDependencies": {
"@nestjs/common": "^11.0.11",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.11",
"@nestjs/graphql": "^13.0.3",
"nest-authz": "^2.14.0"
}
}

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declaration": true,
"sourceMap": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["index.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,33 +0,0 @@
export default ({ store, logger }) => ({
_type: "UnraidApiPlugin",
name: "HealthPlugin",
description: "Health plugin",
commands: [],
async registerGraphQLResolvers() {
return {
Query: {
health: () => {
logger.log("Pinged health");
return "OK";
}
}
};
},
async registerGraphQLTypeDefs() {
return `
type Query {
health: String
}
`;
},
async onModuleInit() {
logger.log("Health plugin initialized");
},
async onModuleDestroy() {
logger.log("Health plugin destroyed");
},
});

View File

@@ -0,0 +1,31 @@
import { Module, Logger } from "@nestjs/common";
import { Resolver, Query } from "@nestjs/graphql";
export const adapter = 'nestjs';
export const graphqlSchemaExtension = async () => `
type Query {
health: String
}
`;
@Resolver()
export class HealthResolver {
@Query(() => String)
health() {
return 'OK';
}
}
@Module({
providers: [HealthResolver],
})
class HealthPlugin {
logger = new Logger(HealthPlugin.name);
onModuleInit() {
this.logger.log("Health plugin initialized");
}
}
export const ApiModule = HealthPlugin;

View File

@@ -1,13 +1,33 @@
{
"name": "unraid-api-plugin-health",
"version": "1.0.0",
"main": "index.js",
"main": "dist/index.js",
"type": "module",
"files": [
"dist"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc",
"prepare": "npm run build"
},
"keywords": [],
"author": "Lime Technology, Inc. <unraid.net>",
"license": "GPL-2.0-or-later",
"description": "Example Health plugin for Unraid API"
"description": "Example Health plugin for Unraid API",
"devDependencies": {
"@nestjs/common": "^11.0.11",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.11",
"@nestjs/graphql": "^13.0.3",
"nest-authz": "^2.14.0",
"typescript": "^5.8.2"
},
"peerDependencies": {
"@nestjs/common": "^11.0.11",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.11",
"@nestjs/graphql": "^13.0.3",
"nest-authz": "^2.14.0"
}
}

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declaration": true,
"sourceMap": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["index.ts"],
"exclude": ["node_modules", "dist"]
}

67
pnpm-lock.yaml generated
View File

@@ -62,6 +62,9 @@ importers:
'@nestjs/common':
specifier: ^11.0.11
version: 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2)
'@nestjs/config':
specifier: ^4.0.2
version: 4.0.2(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2)
'@nestjs/core':
specifier: ^11.0.11
version: 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)
@@ -460,7 +463,47 @@ importers:
specifier: ^8.3.2
version: 8.4.1
packages/unraid-api-plugin-health: {}
packages/unraid-api-plugin-connect:
devDependencies:
'@nestjs/common':
specifier: ^11.0.11
version: 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2)
'@nestjs/config':
specifier: ^4.0.2
version: 4.0.2(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2)
'@nestjs/core':
specifier: ^11.0.11
version: 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)
'@nestjs/graphql':
specifier: ^13.0.3
version: 13.0.4(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(graphql@16.10.0)(reflect-metadata@0.1.14)(ts-morph@24.0.0)
nest-authz:
specifier: ^2.14.0
version: 2.15.0(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)
typescript:
specifier: ^5.8.2
version: 5.8.2
packages/unraid-api-plugin-health:
devDependencies:
'@nestjs/common':
specifier: ^11.0.11
version: 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2)
'@nestjs/config':
specifier: ^4.0.2
version: 4.0.2(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2)
'@nestjs/core':
specifier: ^11.0.11
version: 11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)
'@nestjs/graphql':
specifier: ^13.0.3
version: 13.0.4(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(graphql@16.10.0)(reflect-metadata@0.1.14)(ts-morph@24.0.0)
nest-authz:
specifier: ^2.14.0
version: 2.15.0(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)
typescript:
specifier: ^5.8.2
version: 5.8.2
plugin:
dependencies:
@@ -2530,6 +2573,12 @@ packages:
class-validator:
optional: true
'@nestjs/config@4.0.2':
resolution: {integrity: sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==}
peerDependencies:
'@nestjs/common': ^10.0.0 || ^11.0.0
rxjs: ^7.1.0
'@nestjs/core@11.0.12':
resolution: {integrity: sha512-micQrbh9iL0PuYVx2vsUojuNmMUyqoMCuj7eGAUhvjiZUh4DBLPdxYmJEayCT/equHSiw9vNC95Vm0JigVZ44g==}
engines: {node: '>= 20'}
@@ -5939,6 +5988,10 @@ packages:
resolution: {integrity: sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==}
engines: {node: '>=18'}
dotenv-expand@12.0.1:
resolution: {integrity: sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==}
engines: {node: '>=12'}
dotenv@16.4.7:
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
engines: {node: '>=12'}
@@ -13738,6 +13791,14 @@ snapshots:
tslib: 2.8.1
uid: 2.0.2
'@nestjs/config@4.0.2(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(rxjs@7.8.2)':
dependencies:
'@nestjs/common': 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2)
dotenv: 16.4.7
dotenv-expand: 12.0.1
lodash: 4.17.21
rxjs: 7.8.2
'@nestjs/core@11.0.12(@nestjs/common@11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2))(reflect-metadata@0.1.14)(rxjs@7.8.2)':
dependencies:
'@nestjs/common': 11.0.12(reflect-metadata@0.1.14)(rxjs@7.8.2)
@@ -17749,6 +17810,10 @@ snapshots:
dependencies:
type-fest: 4.38.0
dotenv-expand@12.0.1:
dependencies:
dotenv: 16.4.7
dotenv@16.4.7: {}
dotgitignore@2.1.0: