@tensorify.io/sdk
Version:
TypeScript SDK for developing Tensorify plugins with comprehensive validation, frontend enforcement, and publishing tools
545 lines • 20.8 kB
JavaScript
;
/**
* TensorifyPlugin - The core abstract class for all Tensorify plugins
*
* This is the single class that all plugin developers must extend.
* It enforces frontend requirements, provides validation, and handles
* manifest generation for the CLI publishing system.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.TensorifyPlugin = void 0;
const core_1 = require("../types/core");
const visual_1 = require("../types/visual");
const settings_1 = require("../types/settings");
/**
* Abstract base class for all Tensorify plugins
*
* Every plugin must extend this class and implement the required methods.
* This class enforces frontend visual requirements and provides utilities
* for plugin development.
*/
class TensorifyPlugin {
/** Plugin definition containing all configuration */
definition;
/** Current SDK version */
static SDK_VERSION = "1.0.0";
/** Manifest format version */
static MANIFEST_VERSION = "1.0.0";
/**
* Constructor - Creates a new plugin instance
* @param definition Complete plugin definition
*/
constructor(definition) {
this.definition = definition;
this.validateDefinition();
}
// ========================================
// INPUT ACCESS HELPER
// ========================================
/**
* Helper method to access input data from handles
*
* @param context Code generation context
* @param handleNumber Handle index (0-based)
* @returns Input data from the specified handle
*/
getInput(context, handleNumber) {
return context?.inputData[handleNumber] || null;
}
/**
* Get all input data as an array
*
* @param context Code generation context
* @returns Array of all input data
*/
getAllInputs(context) {
if (!context?.inputData)
return [];
const maxHandle = Math.max(...Object.keys(context.inputData).map(Number));
const inputs = [];
for (let i = 0; i <= maxHandle; i++) {
inputs.push(context.inputData[i] || null);
}
return inputs;
}
// ========================================
// PLUGIN DEFINITION ACCESSORS
// ========================================
/** Get plugin ID (may be undefined if not provided in definition) */
getId() {
return this.definition.id;
}
/** Get plugin name (may be undefined if not provided in definition) */
getName() {
return this.definition.name;
}
/** Get plugin description (may be undefined if not provided in definition) */
getDescription() {
return this.definition.description;
}
/** Get plugin version (may be undefined if not provided in definition) */
getVersion() {
return this.definition.version;
}
/** Get node type (may be undefined if not provided in definition) */
getNodeType() {
return this.definition.nodeType;
}
/** Get visual configuration */
getVisualConfig() {
return this.definition.visual;
}
/** Get input handles */
getInputHandles() {
return this.definition.inputHandles;
}
/** Get output handles */
getOutputHandles() {
return this.definition.outputHandles;
}
/** Get settings fields */
getSettingsFields() {
return this.definition.settingsFields;
}
/** Get settings groups */
getSettingsGroups() {
return this.definition.settingsGroups || [];
}
/** Get capabilities */
getCapabilities() {
return this.definition.capabilities;
}
/** Get requirements */
getRequirements() {
return this.definition.requirements;
}
/** Get complete definition */
getDefinition() {
return { ...this.definition };
}
// ========================================
// VALIDATION METHODS
// ========================================
/**
* Validate plugin settings before code generation
*
* @param settings Settings to validate
* @returns Validation result
*/
validateSettings(settings) {
const errors = [];
const warnings = [];
if (!settings.labelName) {
errors.push({
type: "missing_property",
message: "labelName is required in plugin settings",
path: "labelName",
expected: "string",
actual: settings.labelName,
});
}
// Validate each settings field
for (const field of this.definition.settingsFields) {
const value = settings[field.key];
// Check required fields
if (field.required &&
(value === undefined || value === null || value === "")) {
errors.push({
type: "missing_property",
message: `Required field '${field.key}' is missing`,
path: field.key,
expected: field.dataType,
actual: value,
});
continue;
}
// Skip validation if field is not required and not provided
if (value === undefined || value === null)
continue;
// Validate data type
if (!this.validateFieldDataType(value, field.dataType, field.key)) {
errors.push({
type: "invalid_type",
message: `Invalid type for field '${field.key}'`,
path: field.key,
expected: field.dataType,
actual: typeof value,
});
}
// Validate field-specific rules
if (field.validation) {
const fieldErrors = this.validateFieldRules(value, field);
errors.push(...fieldErrors);
}
}
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
/**
* Validate the plugin definition structure
*/
validateDefinition() {
const errors = [];
// Note: All core metadata (including nodeType) can be derived from package.json
// Only visual configuration is truly required at definition time
// Validate visual config
if (!this.definition.visual) {
errors.push("Plugin visual configuration is required");
}
else {
this.validateVisualConfig(errors);
}
// Validate handles
this.validateHandles(errors);
// Validate settings fields
this.validateSettingsFields(errors);
if (errors.length > 0) {
throw new Error(`Plugin definition validation failed:\n${errors.join("\n")}`);
}
}
/**
* Validate visual configuration
*/
validateVisualConfig(errors) {
const visual = this.definition.visual;
if (!visual.containerType) {
errors.push("Visual containerType is required");
}
if (!visual.size || !visual.size.width || !visual.size.height) {
errors.push("Visual size (width and height) is required");
}
if (visual.size.width < 50 || visual.size.height < 30) {
errors.push("Visual size too small (minimum 50x30)");
}
}
/**
* Validate handle configurations
*/
validateHandles(errors) {
const inputIds = new Set();
const outputIds = new Set();
let hasPrev = false;
let hasNext = false;
// Validate input handles
for (const handle of this.definition.inputHandles) {
if (!handle.id) {
errors.push("Input handle id is required");
continue;
}
if (inputIds.has(handle.id)) {
errors.push(`Duplicate input handle id: ${handle.id}`);
}
inputIds.add(handle.id);
if (!handle.position) {
errors.push(`Input handle ${handle.id} position is required`);
}
if (!handle.viewType) {
errors.push(`Input handle ${handle.id} viewType is required`);
}
if (handle.id === "prev") {
hasPrev = true;
if (handle.position !== visual_1.HandlePosition.LEFT) {
errors.push("Input handle 'prev' must be on the LEFT side");
}
// prev is required for flow, but Start nodes may omit. Since SDK doesn't know node type here,
// enforce required flag present and true to make UI render a required badge.
if (handle.required !== true) {
errors.push("Input handle 'prev' must be required: true");
}
}
}
// Validate output handles
for (const handle of this.definition.outputHandles) {
if (!handle.id) {
errors.push("Output handle id is required");
continue;
}
if (outputIds.has(handle.id)) {
errors.push(`Duplicate output handle id: ${handle.id}`);
}
outputIds.add(handle.id);
if (!handle.position) {
errors.push(`Output handle ${handle.id} position is required`);
}
if (!handle.viewType) {
errors.push(`Output handle ${handle.id} viewType is required`);
}
if (handle.id === "next") {
hasNext = true;
if (handle.position !== visual_1.HandlePosition.RIGHT) {
errors.push("Output handle 'next' must be on the RIGHT side");
}
}
}
// Enforce presence of prev/next handles
if (!hasPrev) {
errors.push("Plugin must define an input handle with id 'prev'");
}
if (!hasNext) {
errors.push("Plugin must define an output handle with id 'next'");
}
}
/**
* Validate settings fields configuration
*/
validateSettingsFields(errors) {
const fieldKeys = new Set();
for (const field of this.definition.settingsFields) {
if (!field.key) {
errors.push("Settings field key is required");
continue;
}
if (fieldKeys.has(field.key)) {
errors.push(`Duplicate settings field key: ${field.key}`);
}
fieldKeys.add(field.key);
if (!field.label) {
errors.push(`Settings field ${field.key} label is required`);
}
if (!field.type) {
errors.push(`Settings field ${field.key} type is required`);
}
if (!field.dataType) {
errors.push(`Settings field ${field.key} dataType is required`);
}
// Validate type compatibility
if (field.type && field.dataType) {
const compatibleTypes = settings_1.UI_TYPE_TO_DATA_TYPE_MAP[field.type];
if (compatibleTypes && !compatibleTypes.includes(field.dataType)) {
errors.push(`Settings field ${field.key} has incompatible type/dataType combination`);
}
}
}
}
/**
* Validate individual field data type
*/
validateFieldDataType(value, expectedType, fieldKey) {
switch (expectedType) {
case settings_1.SettingsDataType.STRING:
return typeof value === "string";
case settings_1.SettingsDataType.NUMBER:
return typeof value === "number" && !isNaN(value);
case settings_1.SettingsDataType.BOOLEAN:
return typeof value === "boolean";
case settings_1.SettingsDataType.ARRAY:
return Array.isArray(value);
case settings_1.SettingsDataType.OBJECT:
return (typeof value === "object" && value !== null && !Array.isArray(value));
default:
return true;
}
}
/**
* Validate field-specific rules
*/
validateFieldRules(value, field) {
const errors = [];
const validation = field.validation;
if (typeof value === "string") {
if (validation.minLength !== undefined &&
value.length < validation.minLength) {
errors.push({
type: "invalid_value",
message: `Field '${field.key}' is too short (minimum ${validation.minLength} characters)`,
path: field.key,
expected: `length >= ${validation.minLength}`,
actual: value.length,
});
}
if (validation.maxLength !== undefined &&
value.length > validation.maxLength) {
errors.push({
type: "invalid_value",
message: `Field '${field.key}' is too long (maximum ${validation.maxLength} characters)`,
path: field.key,
expected: `length <= ${validation.maxLength}`,
actual: value.length,
});
}
if (validation.pattern) {
const regex = new RegExp(validation.pattern);
if (!regex.test(value)) {
errors.push({
type: "invalid_value",
message: `Field '${field.key}' does not match required pattern`,
path: field.key,
expected: validation.pattern,
actual: value,
});
}
}
}
if (typeof value === "number") {
if (validation.min !== undefined && value < validation.min) {
errors.push({
type: "invalid_value",
message: `Field '${field.key}' is too small (minimum ${validation.min})`,
path: field.key,
expected: `>= ${validation.min}`,
actual: value,
});
}
if (validation.max !== undefined && value > validation.max) {
errors.push({
type: "invalid_value",
message: `Field '${field.key}' is too large (maximum ${validation.max})`,
path: field.key,
expected: `<= ${validation.max}`,
actual: value,
});
}
}
return errors;
}
// ========================================
// MANIFEST GENERATION
// ========================================
/**
* Derive plugin ID from package name
* @param packageName Package name (e.g., "@org/my-plugin" or "my-plugin")
* @returns Derived plugin ID
*/
derivePluginId(packageName) {
// Remove scope prefix if present (@org/my-plugin -> my-plugin)
return packageName.replace(/^@[^/]+\//, "");
}
/**
* Derive nodeType from package.json tensorify.pluginType
* @param pluginType Plugin type from package.json tensorify section
* @returns Derived NodeType or default to CUSTOM
*/
deriveNodeType(pluginType) {
if (!pluginType)
return core_1.NodeType.CUSTOM;
// Map string values to NodeType enum
const typeMap = {
custom: core_1.NodeType.CUSTOM,
trainer: core_1.NodeType.TRAINER,
evaluator: core_1.NodeType.EVALUATOR,
model: core_1.NodeType.MODEL,
model_layer: core_1.NodeType.MODEL_LAYER,
dataloader: core_1.NodeType.DATALOADER,
preprocessor: core_1.NodeType.PREPROCESSOR,
postprocessor: core_1.NodeType.POSTPROCESSOR,
augmentation_stack: core_1.NodeType.AUGMENTATION_STACK,
optimizer: core_1.NodeType.OPTIMIZER,
loss_function: core_1.NodeType.LOSS_FUNCTION,
metric: core_1.NodeType.METRIC,
scheduler: core_1.NodeType.SCHEDULER,
regularizer: core_1.NodeType.REGULARIZER,
function: core_1.NodeType.FUNCTION,
pipeline: core_1.NodeType.PIPELINE,
report: core_1.NodeType.REPORT,
};
return typeMap[pluginType.toLowerCase()] || core_1.NodeType.CUSTOM;
}
/**
* Generate frontend manifest for CLI publishing
*
* @param packageInfo Package.json information
* @param entrypointClassName Exact class name for instantiation
* @returns Frontend plugin manifest
*/
generateManifest(packageInfo, entrypointClassName) {
// Derive missing core metadata from package.json
const derivedId = this.definition.id || this.derivePluginId(packageInfo.name);
const derivedName = this.definition.name || packageInfo.name;
const derivedDescription = this.definition.description || packageInfo.description || "";
const derivedVersion = this.definition.version || packageInfo.version;
const derivedNodeType = this.definition.nodeType ||
this.deriveNodeType(packageInfo.tensorify?.pluginType);
// Validate that we have all required information after derivation
if (!derivedId) {
throw new Error("Plugin ID could not be derived from package name");
}
if (!derivedName) {
throw new Error("Plugin name could not be derived from package name");
}
if (!derivedVersion) {
throw new Error("Plugin version could not be derived from package.json");
}
const manifest = {
// Package Information
name: packageInfo.name,
version: packageInfo.version,
description: packageInfo.description || derivedDescription,
author: packageInfo.author || this.definition.author || "",
main: packageInfo.main || "dist/index.js",
entrypointClassName,
keywords: packageInfo.keywords || this.definition.keywords || [],
repository: packageInfo.repository,
pluginType: packageInfo.tensorify?.pluginType,
tensorify: packageInfo.tensorify,
// Frontend Configuration
frontendConfigs: {
id: derivedId,
name: derivedName,
category: derivedNodeType,
nodeType: derivedNodeType,
visual: this.definition.visual,
inputHandles: this.definition.inputHandles,
outputHandles: this.definition.outputHandles,
settingsFields: this.definition.settingsFields,
settingsGroups: this.definition.settingsGroups,
},
// Plugin Metadata
capabilities: this.definition.capabilities,
requirements: this.definition.requirements,
// Generation Metadata
sdkVersion: TensorifyPlugin.SDK_VERSION,
generatedAt: new Date().toISOString(),
manifestVersion: TensorifyPlugin.MANIFEST_VERSION,
};
return manifest;
}
// ========================================
// UTILITY METHODS
// ========================================
/**
* Create default settings object from field definitions
*/
createDefaultSettings() {
const settings = {
variableName: `${this.definition.id || "plugin"}_${Date.now()}`,
labelName: this.definition.name || "Plugin",
};
for (const field of this.definition.settingsFields) {
if (field.defaultValue !== undefined) {
settings[field.key] = field.defaultValue;
}
}
return settings;
}
/**
* Process dynamic label template with settings values
*/
generateDynamicLabel(settings) {
const template = this.definition.visual.labels.dynamicLabelTemplate;
if (!template)
return "";
let result = template;
for (const [key, value] of Object.entries(settings)) {
const placeholder = `{${key}}`;
result = result.replace(new RegExp(placeholder, "g"), String(value));
}
return result;
}
/**
* Get current SDK version
*/
static getSDKVersion() {
return TensorifyPlugin.SDK_VERSION;
}
/**
* Get manifest format version
*/
static getManifestVersion() {
return TensorifyPlugin.MANIFEST_VERSION;
}
}
exports.TensorifyPlugin = TensorifyPlugin;
//# sourceMappingURL=TensorifyPlugin.js.map