🏰 @statelyjs/schema

Type definitions and schema parsing for Stately applications

npm License

This package provides the foundational type system for Stately's frontend. It parses OpenAPI schemas into a typed AST (Abstract Syntax Tree) that powers the UI's form generation, validation, and type safety.

This 'schema' package is a low-level package used internally by Stately. See @statelyjs/stately for the user facing package.

Installation

pnpm add @statelyjs/schema

Note: Most users should use @statelyjs/ui which re-exports this package with additional UI integration. Use @statelyjs/schema directly only if building custom tooling or plugins without the UI layer.

Core Concepts

Schema Nodes

OpenAPI schemas are parsed into nodes - typed representations of each schema element:

// A string field
{ nodeType: 'string', nullable: false }

// An object with properties
{ 
  nodeType: 'object',
  properties: {
    name: { nodeType: 'string', nullable: false },
    age: { nodeType: 'integer', nullable: true },
  },
  required: ['name'],
}

// A reference to another entity
{ nodeType: 'link', inner: { nodeType: 'ref', $ref: '#/components/schemas/SourceConfig' } }

The Schemas Type

The Schemas type is the single source of truth for your application's type system. It combines:

  1. Config: Your OpenAPI-generated types (components, paths, operations)
  2. Plugins: Type augmentations from installed plugins
  3. Derived Types: EntityData, StateEntry, and other computed types
import type { StatelySchemas, DefineConfig } from '@statelyjs/schema';
import type { components, paths, operations } from './generated/types';

type MySchemas = StatelySchemas<DefineConfig<components, paths, operations, typeof PARSED_SCHEMAS>>;

Plugin System

Plugins extend the schema with additional node types:

import type { DefinePlugin, NodeMap, StatelySchemas } from '@statelyjs/schema';

// Define custom nodes
interface MyNodes extends NodeMap {
  myCustomType: { nodeType: 'myCustomType'; value: string };
}

// Create plugin type
export type MyPlugin = DefinePlugin<'my-plugin', MyNodes>;

// Use with Schemas
type AppSchemas = StatelySchemas<MyConfig, readonly [MyPlugin]>;

Usage

Creating a Stately Runtime

import { createStately } from '@statelyjs/schema';
import { PARSED_SCHEMAS } from './generated/schemas';
import openapiDoc from './openapi.json';

const stately = createStately<MySchemas>(openapiDoc, PARSED_SCHEMAS);

// Access schema utilities
const entityNode = stately.utils.getNode('Pipeline');
const isValid = stately.utils.validate(data, 'Pipeline');

With Code Splitting

When using @statelyjs/codegen with entry points, some schemas are split into a separate runtime bundle for lazy loading. Configure the loader to enable this:

import { createStately } from '@statelyjs/schema';
import { PARSED_SCHEMAS } from './generated/schemas';
import openapiDoc from './openapi.json';

const stately = createStately<MySchemas>(openapiDoc, PARSED_SCHEMAS, {
  // Enable lazy loading for code-split schemas
  runtimeSchemas: () => import('./generated/schemas.runtime').then(m => m.RUNTIME_SCHEMAS),
});

This is optional - if no schemas.runtime.ts was generated, you don't need this option.

With Plugins

import { createStately } from '@statelyjs/schema';
import { filesPlugin } from '@statelyjs/files';

const stately = createStately<MySchemas>(openapiDoc, PARSED_SCHEMAS)
  .withPlugin(filesPlugin());

// Plugin utilities are now available
stately.plugins.files.utils.formatPath('/some/path');

Type Helpers

DefineConfig

Combines your generated types into a schema configuration:

type MyConfig = DefineConfig<
  components,           // OpenAPI components
  DefinePaths<paths>,   // API paths
  DefineOperations<operations>,  // Operations
  typeof PARSED_SCHEMAS // Generated schema nodes
>;

DefinePlugin

Declares a plugin's type augmentations:

type MyPlugin = DefinePlugin<
  'my-plugin',     // Unique name (string literal)
  MyNodes,         // Node map
  MyTypes,         // Additional types
  MyData,          // Data utilities
  MyUtils          // Utility functions
>;

Extracting Types

import type { Schemas } from '@statelyjs/schema';

// Get the StateEntry union (entity type names)
type StateEntry = MySchemas['config']['components']['schemas']['StateEntry'];

// Get entity data types
type EntityData = MySchemas['types']['EntityData'];

// Get all nodes from plugins
type AllNodes = MySchemas['plugin']['AnyNode'];

Integration with @statelyjs/codegen

The @statelyjs/codegen CLI generates the files this package consumes:

pnpx @statelyjs/codegen ./openapi.json ./src/generated

This produces:

  • types.ts - TypeScript interfaces matching your OpenAPI spec
  • schemas.ts - Parsed PARSED_SCHEMAS object for runtime use

Validation

import type { ValidationResult } from '@statelyjs/schema';

const result: ValidationResult = stately.utils.validate(data, 'Pipeline');

if (!result.valid) {
  console.log(result.errors);
  // [{ path: ['name'], message: 'Required field missing' }]
}

API Reference

createStately(openapiDoc, parsedSchemas, createStatelyOptions)

Creates a Stately schema runtime.

isNodeOfType(node, nodeType)

Type guard for checking node types:

if (isNodeOfType(node, NodeType.Primitive)) {
  // node is typed as Primitive 
}

createOperationBindingsFactory()

Creates typed API operation bindings from paths.

License

Apache-2.0

Modules