@stonecrop/schema
v0.10.13
Published
Stonecrop schema definitions and validation tooling
Readme
@stonecrop/schema
Schema definitions and validation for Stonecrop doctypes, fields, and workflows.
Overview
@stonecrop/schema provides the foundational type system for Stonecrop applications. It defines strongly-typed schemas using Zod for:
- Field definitions (
FieldMeta) - Unified field configuration for forms and tables - Doctype definitions (
DoctypeMeta) - Complete document type schemas - Workflows (
WorkflowMeta) - State machines and action definitions - Validation - Runtime schema validation with detailed error reporting
- DDL Conversion - PostgreSQL DDL to Stonecrop schema transformation
This package is schema-only and has no UI dependencies - it can be used in both frontend and backend contexts.
Installation
# From the monorepo root
rush update
# Or with pnpm
pnpm add @stonecrop/schemaCore Concepts
Field Types
Stonecrop uses semantic field types that remain consistent whether rendered in a form or table:
import { StonecropFieldType, FieldMeta } from '@stonecrop/schema'
// Field types include:
// Text: Data, Text
// Numeric: Int, Float, Decimal, Currency, Quantity
// Boolean: Check
// Date/Time: Date, Time, Datetime, Duration, DateRange
// Structured: JSON, Code
// Relational: Link, Doctype
// Files: Attach
// Selection: SelectField Metadata
FieldMeta is the single source of truth for field definitions:
import { FieldMeta } from '@stonecrop/schema'
const field: FieldMeta = {
fieldname: 'customer_name',
fieldtype: 'Data',
label: 'Customer Name',
required: true,
readOnly: false,
width: '40ch',
align: 'left',
}
// Type-specific options
const linkField: FieldMeta = {
fieldname: 'customer',
fieldtype: 'Link',
label: 'Customer',
options: 'customer', // Target doctype slug
}
const selectField: FieldMeta = {
fieldname: 'status',
fieldtype: 'Select',
label: 'Status',
options: ['Draft', 'Submitted', 'Cancelled'], // Choices array
}
const decimalField: FieldMeta = {
fieldname: 'price',
fieldtype: 'Decimal',
label: 'Price',
options: { precision: 10, scale: 2 }, // Config object
}Doctype Metadata
DoctypeMeta defines a complete doctype with fields, workflow, and inheritance:
import { DoctypeMeta } from '@stonecrop/schema'
const doctype: DoctypeMeta = {
name: 'Sales Order',
slug: 'sales-order',
tableName: 'sales_order',
fields: [
{
fieldname: 'customer',
fieldtype: 'Link',
label: 'Customer',
options: 'customer',
required: true,
},
{
fieldname: 'items',
fieldtype: 'Doctype',
label: 'Order Items',
options: 'sales-order-item', // Child doctype
},
],
workflow: {
states: ['Draft', 'Submitted', 'Cancelled'],
actions: {
submit: {
label: 'Submit',
handler: 'submitOrder',
requiredFields: ['customer', 'items'],
allowedStates: ['Draft'],
},
},
},
}Workflow and Actions
Define state machines and actions for doctypes:
import { WorkflowMeta, ActionDefinition } from '@stonecrop/schema'
const workflow: WorkflowMeta = {
states: ['Draft', 'Pending Approval', 'Approved', 'Rejected'],
actions: {
submit: {
label: 'Submit for Approval',
handler: 'handleSubmit',
requiredFields: ['title', 'description'],
allowedStates: ['Draft'],
confirm: true,
},
approve: {
label: 'Approve',
handler: 'handleApprove',
allowedStates: ['Pending Approval'],
args: { notifyUser: true },
},
},
}Validation
Runtime validation with detailed error reporting:
import { validateField, validateDoctype } from '@stonecrop/schema'
// Validate a field definition
const fieldResult = validateField({
fieldname: 'email',
fieldtype: 'Data',
label: 'Email',
})
if (!fieldResult.success) {
console.error('Validation errors:', fieldResult.errors)
// errors: [{ path: ['fieldname'], message: 'Required' }]
}
// Validate a doctype definition
const doctypeResult = validateDoctype(doctypeData)
if (doctypeResult.success) {
console.log('Doctype is valid!')
}Parse and Validate
Use Zod's parse methods for type-safe validation:
import { parseField, parseDoctype } from '@stonecrop/schema'
try {
const field = parseField(untrustedData)
// TypeScript knows field is FieldMeta
} catch (error) {
console.error('Invalid field:', error)
}
try {
const doctype = parseDoctype(untrustedData)
// TypeScript knows doctype is DoctypeMeta
} catch (error) {
console.error('Invalid doctype:', error)
}GraphQL to Doctype CLI
The stonecrop-schema generate command converts a GraphQL schema into Stonecrop doctype JSON files.
Basic usage
# From a live GraphQL endpoint
stonecrop-schema generate -e http://localhost:3000/graphql -o ./app/doctypes
# From a saved introspection JSON file
stonecrop-schema generate -i introspection.json -o ./app/doctypes
# From an SDL file
stonecrop-schema generate -s schema.graphql -o ./app/doctypesFiltering types
GraphQL schemas (especially PostGraphile) expose many internal types. Use --include to
allowlist exactly the types you need, rather than having to --exclude everything you don't:
# Only generate doctypes for these three types
stonecrop-schema generate -e http://localhost:3000/graphql -o ./app/doctypes \
--include 'SalesOrder,Customer,Item'
# Alternatively, exclude specific types
stonecrop-schema generate -e http://localhost:3000/graphql -o ./app/doctypes \
--exclude 'PageInfo,StonecropActionDefinition'--include and --exclude can be combined: --include is applied first (narrowing the set),
then --exclude removes any remaining unwanted names.
All options
| Flag | Short | Description |
|------|-------|-------------|
| --endpoint <url> | -e | Fetch introspection from a live GraphQL endpoint |
| --introspection <file> | -i | Read from a saved introspection JSON file |
| --sdl <file> | -s | Read from a GraphQL SDL (.graphql) file |
| --output <dir> | -o | Directory to write doctype JSON files (required) |
| --include <types> | | Comma-separated allowlist of type names to generate |
| --exclude <types> | | Comma-separated list of type names to skip |
| --overrides <file> | | JSON file with per-type, per-field overrides |
| --custom-scalars <file> | | JSON file mapping custom scalar names to field templates |
| --include-unmapped | | Retain _graphqlType metadata on fields with no mapping |
| --help | -h | Show help |
Custom scalars
For servers that use non-standard scalars (e.g. PostGraphile's BigFloat, Datetime), provide
a JSON mapping file:
{
"BigFloat": { "component": "ADecimalInput", "fieldtype": "Decimal" },
"Datetime": { "component": "ADatetimeInput", "fieldtype": "Datetime" }
}stonecrop-schema generate -e http://localhost:3000/graphql -o ./app/doctypes \
--custom-scalars custom-scalars.jsonPer-field overrides
Override the generated field definition for specific types and fields:
{
"SalesOrder": {
"totalAmount": { "fieldtype": "Currency", "component": "ACurrencyInput" }
}
}stonecrop-schema generate -e http://localhost:3000/graphql -o ./app/doctypes \
--overrides overrides.jsonDDL Conversion
Convert PostgreSQL DDL statements to Stonecrop doctype schemas:
import { convertSchema, type ConversionOptions } from '@stonecrop/schema'
const ddl = `
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE sales_orders (
id SERIAL PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id),
status VARCHAR(20) DEFAULT 'Draft',
total_amount DECIMAL(10, 2)
);
`
const options: ConversionOptions = {
inheritanceMode: 'flatten', // or 'reference'
useCamelCase: true, // Convert snake_case to camelCase
includeUnmappedMeta: false, // Include unmapped metadata
schema: 'public', // Filter by schema
exclude: ['migrations'], // Exclude tables
typeOverrides: {
status: { fieldtype: 'Select', options: ['Draft', 'Submitted'] },
},
}
const doctypes = convertSchema(ddl, options)
doctypes.forEach(doctype => {
console.log(`Doctype: ${doctype.name}`)
console.log(`Table: ${doctype.tableName}`)
console.log(`Fields: ${doctype.fields.length}`)
})Naming Utilities
Convert between different naming conventions:
import {
snakeToCamel,
camelToSnake,
snakeToLabel,
camelToLabel,
toPascalCase,
toSlug,
} from '@stonecrop/schema'
snakeToCamel('customer_name') // 'customerName'
camelToSnake('customerName') // 'customer_name'
snakeToLabel('customer_name') // 'Customer Name'
camelToLabel('customerName') // 'Customer Name'
toPascalCase('customer_name') // 'CustomerName'
toSlug('Customer Name') // 'customer-name'API
Field Type Mapping
import { TYPE_MAP, getDefaultComponent } from '@stonecrop/schema'
// Get default component for a field type
const component = getDefaultComponent('Data') // 'ATextInput'
// Access full type map
console.log(TYPE_MAP['Link']) // { component: 'ALink', fieldtype: 'Link' }Usage in Stonecrop
This package provides the type system used throughout Stonecrop:
@stonecrop/stonecrop- Registry usesDoctypeMetafor schema storage@stonecrop/aform- Renders fields based onFieldMetadefinitions@stonecrop/atable- UsesFieldMetafor column configuration- Backend APIs - Validates and stores doctypes using these schemas
Development
# Install dependencies
rush update
# Build
rushx build
# Run tests
rushx test
# Watch mode
rushx test:watch
# Generate API documentation
rushx docsTypeScript Support
This package is written in TypeScript with strict mode enabled and provides full type definitions:
import type { FieldMeta, DoctypeMeta } from '@stonecrop/schema'
// Types are inferred from Zod schemas
const field: FieldMeta = {
fieldname: 'title',
fieldtype: 'Data',
// TypeScript will catch typos and missing required fields
}
// Use Zod's infer utility for derived types
import { z } from 'zod'
import { FieldMeta as FieldMetaSchema } from '@stonecrop/schema'
type FieldMetaType = z.infer<typeof FieldMetaSchema>