diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d7a25846b..3cef5f2ca 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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: diff --git a/api/package.json b/api/package.json index 5b1e6f63d..1f5959544 100644 --- a/api/package.json +++ b/api/package.json @@ -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", diff --git a/api/src/cli.ts b/api/src/cli.ts index 9e79623f2..b4165e0eb 100644 --- a/api/src/cli.ts +++ b/api/src/cli.ts @@ -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: { diff --git a/api/src/unraid-api/app/app.module.ts b/api/src/unraid-api/app/app.module.ts index 47dd873a5..055f68437 100644 --- a/api/src/unraid-api/app/app.module.ts +++ b/api/src/unraid-api/app/app.module.ts @@ -47,7 +47,7 @@ import { UnraidFileModifierModule } from '@app/unraid-api/unraid-file-modifier/u }, ]), UnraidFileModifierModule, - PluginModule.registerPlugins(), + PluginModule.register(), ], controllers: [], providers: [ diff --git a/api/src/unraid-api/cli/cli.module.ts b/api/src/unraid-api/cli/cli.module.ts index 203051c54..4602e733b 100644 --- a/api/src/unraid-api/cli/cli.module.ts +++ b/api/src/unraid-api/cli/cli.module.ts @@ -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; - 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[] { - return [...DEFAULT_COMMANDS]; - } - - /** - * Register the module with plugin support - * @returns DynamicModule configuration including plugin commands - */ - static async registerWithPlugins(): Promise { - const pluginModule = await PluginModule.registerPlugins(); - - // Get commands from plugins - const pluginCommands: Type[] = []; - 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 {} diff --git a/api/src/unraid-api/graph/graph.module.ts b/api/src/unraid-api/graph/graph.module.ts index 5f974c035..0283058e5 100644 --- a/api/src/unraid-api/graph/graph.module.ts +++ b/api/src/unraid-api/graph/graph.module.ts @@ -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({ 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, diff --git a/api/src/unraid-api/plugin/plugin.interface.ts b/api/src/unraid-api/plugin/plugin.interface.ts index 6d6b954a1..7c9d1a7f4 100644 --- a/api/src/unraid-api/plugin/plugin.interface.ts +++ b/api/src/unraid-api/plugin/plugin.interface.ts @@ -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>()), - 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; +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; +/** format of module exports from a nestjs plugin */ +export const apiNestPluginSchema = z + .object({ + adapter: z.literal('nestjs'), + ApiModule: z + .custom(isClass, { + message: 'Invalid NestJS module: expected a class constructor', + }) + .optional(), + CliModule: z + .custom(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; diff --git a/api/src/unraid-api/plugin/plugin.module.ts b/api/src/unraid-api/plugin/plugin.module.ts index e57070ef2..00ce4c936 100644 --- a/api/src/unraid-api/plugin/plugin.module.ts +++ b/api/src/unraid-api/plugin/plugin.module.ts @@ -7,14 +7,41 @@ export class PluginModule { private static readonly logger = new Logger(PluginModule.name); constructor(private readonly pluginService: PluginService) {} - static async registerPlugins(): Promise { + static async register(): Promise { 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 { + 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], + }; + } +} diff --git a/api/src/unraid-api/plugin/plugin.service.ts b/api/src/unraid-api/plugin/plugin.service.ts index 86f79bcfc..1d4ce53e4 100644 --- a/api/src/unraid-api/plugin/plugin.service.ts +++ b/api/src/unraid-api/plugin/plugin.service.ts @@ -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; -}; - -type PluginProvider = { - provider: CustomProvider; - pluginInstance: ApiPluginDefinition; -}; - @Injectable() export class PluginService { - private pluginProviders: PluginProvider[] | undefined; - private loadingPromise: Promise | 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 | 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 => + 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[] = []; - const routes: Record[] = []; - - 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[] = []; - - 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[] = []; - - 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; } } diff --git a/packages/unraid-api-plugin-connect/index.ts b/packages/unraid-api-plugin-connect/index.ts new file mode 100644 index 000000000..9275dd09c --- /dev/null +++ b/packages/unraid-api-plugin-connect/index.ts @@ -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; diff --git a/packages/unraid-api-plugin-connect/package.json b/packages/unraid-api-plugin-connect/package.json new file mode 100644 index 000000000..e04ee53ac --- /dev/null +++ b/packages/unraid-api-plugin-connect/package.json @@ -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. ", + "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" + } +} diff --git a/packages/unraid-api-plugin-connect/tsconfig.json b/packages/unraid-api-plugin-connect/tsconfig.json new file mode 100644 index 000000000..cbf07ab83 --- /dev/null +++ b/packages/unraid-api-plugin-connect/tsconfig.json @@ -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"] +} diff --git a/packages/unraid-api-plugin-health/index.js b/packages/unraid-api-plugin-health/index.js deleted file mode 100644 index 106dac2b3..000000000 --- a/packages/unraid-api-plugin-health/index.js +++ /dev/null @@ -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"); - }, -}); diff --git a/packages/unraid-api-plugin-health/index.ts b/packages/unraid-api-plugin-health/index.ts new file mode 100644 index 000000000..a4756f9fd --- /dev/null +++ b/packages/unraid-api-plugin-health/index.ts @@ -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; diff --git a/packages/unraid-api-plugin-health/package.json b/packages/unraid-api-plugin-health/package.json index 4f2ab8511..1de1f985f 100644 --- a/packages/unraid-api-plugin-health/package.json +++ b/packages/unraid-api-plugin-health/package.json @@ -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. ", "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" + } } diff --git a/packages/unraid-api-plugin-health/tsconfig.json b/packages/unraid-api-plugin-health/tsconfig.json new file mode 100644 index 000000000..cbf07ab83 --- /dev/null +++ b/packages/unraid-api-plugin-health/tsconfig.json @@ -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"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7259cfdc2..937a98a21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: