Add project scaffolding and documentation
Add .env.example with configuration for database, Redis, security, SMTP, workers, and plugins. Add .gitignore for Python, Node.js, Next.js, Docker volumes, and IDE files. Add MIT License. Update README.md with feature overview, quick start guide, architecture description, plugin system documentation, security details, backup/restore instructions, and developer setup. Add Alembic configuration files and placeholder directories for API, web, worker, and plugin components
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@nexadash/plugin-sdk",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "NexaDash Plugin SDK for external developers",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.6.0",
|
||||
"vitest": "^2.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./types";
|
||||
export * from "./manifest";
|
||||
export * from "./widgets";
|
||||
export * from "./validation";
|
||||
@@ -0,0 +1,30 @@
|
||||
import { PluginManifest } from "./types";
|
||||
|
||||
export function createManifest(manifest: PluginManifest): PluginManifest {
|
||||
const defaults: PluginManifest = {
|
||||
id: manifest.id,
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
nexadashPluginApi: manifest.nexadashPluginApi || "1.0",
|
||||
category: "Generic",
|
||||
permissions: [],
|
||||
settingsSchema: {},
|
||||
credentialsSchema: {},
|
||||
widgets: [],
|
||||
apiRoutes: [],
|
||||
hasFrontend: true,
|
||||
healthcheck: {
|
||||
method: "GET",
|
||||
intervalSeconds: 60,
|
||||
timeoutSeconds: 30,
|
||||
},
|
||||
};
|
||||
return { ...defaults, ...manifest };
|
||||
}
|
||||
|
||||
export const REQUIRED_MANIFEST_FIELDS = [
|
||||
"id",
|
||||
"name",
|
||||
"version",
|
||||
"nexadashPluginApi",
|
||||
];
|
||||
@@ -0,0 +1,77 @@
|
||||
export interface PluginManifest {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
nexadashPluginApi: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
category?: string;
|
||||
icon?: string;
|
||||
permissions?: string[];
|
||||
settingsSchema?: Record<string, any>;
|
||||
credentialsSchema?: Record<string, any>;
|
||||
widgets?: WidgetDefinition[];
|
||||
apiRoutes?: string[];
|
||||
hasFrontend?: boolean;
|
||||
healthcheck?: HealthcheckDefinition;
|
||||
}
|
||||
|
||||
export interface WidgetDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
defaultWidth?: number;
|
||||
defaultHeight?: number;
|
||||
minWidth?: number;
|
||||
minHeight?: number;
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
settingsSchema?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface HealthcheckDefinition {
|
||||
endpoint?: string;
|
||||
method?: "GET" | "POST" | "PUT" | "DELETE";
|
||||
intervalSeconds?: number;
|
||||
timeoutSeconds?: number;
|
||||
}
|
||||
|
||||
export interface PluginContext<T = any> {
|
||||
settings: Record<string, any>;
|
||||
credentials: T;
|
||||
baseUrl: string;
|
||||
headers: Record<string, string>;
|
||||
fetch: (path: string, options?: RequestInit) => Promise<any>;
|
||||
log: (level: "info" | "warn" | "error", message: string, meta?: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
export interface PluginConnector<T = any> {
|
||||
healthcheck(context: PluginContext<T>): Promise<HealthcheckResult>;
|
||||
fetchWidgetData(
|
||||
context: PluginContext<T>,
|
||||
widgetType: string,
|
||||
settings: Record<string, any>
|
||||
): Promise<any>;
|
||||
}
|
||||
|
||||
export interface HealthcheckResult {
|
||||
status: "ok" | "warning" | "error" | "unknown";
|
||||
message?: string;
|
||||
details?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface JsonSchemaProperty {
|
||||
type: "string" | "number" | "boolean" | "array" | "object";
|
||||
title?: string;
|
||||
description?: string;
|
||||
default?: any;
|
||||
enum?: any[];
|
||||
format?: string;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
properties?: Record<string, JsonSchemaProperty>;
|
||||
required?: string[];
|
||||
items?: JsonSchemaProperty;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { REQUIRED_MANIFEST_FIELDS } from "./manifest";
|
||||
import { PluginManifest } from "./types";
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export function validateManifest(manifest: any): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!manifest || typeof manifest !== "object") {
|
||||
return { valid: false, errors: ["Manifest must be an object"] };
|
||||
}
|
||||
|
||||
for (const field of REQUIRED_MANIFEST_FIELDS) {
|
||||
if (!manifest[field] || typeof manifest[field] !== "string") {
|
||||
errors.push(`Missing or invalid required field: ${field}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.id && !/^[a-z0-9-]+$/.test(manifest.id)) {
|
||||
errors.push("Plugin id must be lowercase alphanumeric with hyphens");
|
||||
}
|
||||
|
||||
if (manifest.version && !/^\d+\.\d+\.\d+/.test(manifest.version)) {
|
||||
errors.push("Version must follow semantic versioning (e.g. 1.0.0)");
|
||||
}
|
||||
|
||||
if (manifest.widgets && !Array.isArray(manifest.widgets)) {
|
||||
errors.push("widgets must be an array");
|
||||
}
|
||||
|
||||
if (manifest.permissions && !Array.isArray(manifest.permissions)) {
|
||||
errors.push("permissions must be an array");
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
export function isValidPluginApiVersion(version: string): boolean {
|
||||
return version === "1.0";
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { WidgetDefinition } from "./types";
|
||||
|
||||
export function defineWidget(widget: WidgetDefinition): WidgetDefinition {
|
||||
return {
|
||||
defaultWidth: 2,
|
||||
defaultHeight: 2,
|
||||
minWidth: 1,
|
||||
minHeight: 1,
|
||||
settingsSchema: {},
|
||||
...widget,
|
||||
};
|
||||
}
|
||||
|
||||
export const commonWidgetSizes = {
|
||||
small: { defaultWidth: 1, defaultHeight: 1 },
|
||||
medium: { defaultWidth: 2, defaultHeight: 2 },
|
||||
large: { defaultWidth: 3, defaultHeight: 2 },
|
||||
wide: { defaultWidth: 4, defaultHeight: 2 },
|
||||
tall: { defaultWidth: 2, defaultHeight: 4 },
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { createManifest, defineWidget, validateManifest, REQUIRED_MANIFEST_FIELDS } from "../src";
|
||||
|
||||
const baseManifest = {
|
||||
id: "test-plugin",
|
||||
name: "Test Plugin",
|
||||
version: "1.0.0",
|
||||
nexadashPluginApi: "1.0",
|
||||
};
|
||||
|
||||
describe("plugin-sdk manifest", () => {
|
||||
it("createManifest applies defaults", () => {
|
||||
const manifest = createManifest(baseManifest);
|
||||
expect(manifest.name).toBe("Test Plugin");
|
||||
expect(manifest.nexadashPluginApi).toBe("1.0");
|
||||
expect(manifest.widgets).toEqual([]);
|
||||
expect(manifest.hasFrontend).toBe(false);
|
||||
});
|
||||
|
||||
it("defineWidget applies defaults", () => {
|
||||
const widget = defineWidget({
|
||||
id: "test-widget",
|
||||
name: "Test Widget",
|
||||
});
|
||||
expect(widget.defaultWidth).toBe(2);
|
||||
expect(widget.defaultHeight).toBe(2);
|
||||
});
|
||||
|
||||
it("validateManifest validates required fields", () => {
|
||||
const result = validateManifest(createManifest(baseManifest));
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("validateManifest rejects invalid ids", () => {
|
||||
const result = validateManifest(
|
||||
createManifest({
|
||||
...baseManifest,
|
||||
id: "Test Plugin",
|
||||
})
|
||||
);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("REQUIRED_MANIFEST_FIELDS", () => {
|
||||
it("contains required keys", () => {
|
||||
expect(REQUIRED_MANIFEST_FIELDS).toContain("id");
|
||||
expect(REQUIRED_MANIFEST_FIELDS).toContain("name");
|
||||
expect(REQUIRED_MANIFEST_FIELDS).toContain("version");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user