Skip to content

Commit b78d94a

Browse files
committed
feat: Add lifecycle, policy, events DSL & tests
Introduce higher-order, generator-oriented metadata to the schema DSL: .lifecycle(), .policy(), .events(), and .meta(). Add AST types for LifecycleDef, PolicyDef, EventDef and wire them into the builder/runtime (builders expose the new chainable methods and perform lazy validation). Add structured error codes for lifecycle/policy/events and extend the error-preview test output. Also update architecture/docs (CLAUDE.md) to describe the metadata-oriented approach and add comprehensive unit tests for meta, lifecycle, policy and events handling. Changes include AST/type updates, builder validations (lifecycle/event checks), and multiple new runtime tests.
1 parent bc113cd commit b78d94a

15 files changed

Lines changed: 1307 additions & 231 deletions

File tree

CLAUDE.md

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,20 @@ task check # Run lint + vuln + test
4646

4747
## Architecture Overview
4848

49-
AstrolaDB is a **polyglot code generator** with a Go core that embeds a JavaScript runtime (Goja) to execute user-written schemas and generators.
49+
AstrolaDB is a **documentation-oriented meta-model system** that captures higher-order system metadata through a declarative JavaScript DSL. It's fundamentally about modeling system intent across multiple dimensions — structural, behavioral, and operational — making this metadata consumable by generators and documentation tooling.
50+
51+
### What is Higher-Order System Metadata?
52+
53+
AstrolaDB schemas are **not just database schemas**. They encode:
54+
55+
- **Structural metadata** — tables, columns, types, relationships (traditional schema)
56+
- **Lifecycle metadata** — entity states, workflow transitions, state machines
57+
- **Access metadata** — role-based or attribute-based policy intents
58+
- **Event metadata** — event contracts, payloads, publish targets
59+
- **Service metadata** — bounded contexts, ownership, domain partitions
60+
- **Deployment metadata** — regions, replicas, storage, topology hints
61+
62+
**Migrations are just one built-in core** that consumes structural metadata to generate SQL. Generators consume the full metadata graph to produce application code, documentation, infrastructure templates, and more.
5063

5164
### Three-Layer Architecture
5265

@@ -66,10 +79,11 @@ AstrolaDB is a **polyglot code generator** with a Go core that embeds a JavaScri
6679
│ produces
6780
6881
┌─────────────────────────────────────────┐
69-
Engine Layer (Go) │ ← Core processing
70-
│ • internal/engine - Migration engine
82+
Core Layer (Go) │ ← Built-in metadata consumers
83+
│ • internal/engine - Migration core │ (structural → SQL)
7184
│ • internal/dialect - SQL generation │
7285
│ • internal/types - Type exports │
86+
│ • [Future: Generator registry] │ (full metadata → code/docs/IaC)
7387
└─────────────────────────────────────────┘
7488
```
7589

@@ -78,13 +92,14 @@ AstrolaDB is a **polyglot code generator** with a Go core that embeds a JavaScri
7892
- `cmd/alab/` - CLI entry point, command implementations
7993
- `internal/runtime/` - **Goja JavaScript VM and DSL bindings**
8094
- `sandbox.go` - Secure JS execution environment
81-
- `builder/` - JavaScript DSL implementation (col.\*, table())
95+
- `builder/` - JavaScript DSL implementation (col.\*, table(), metadata builders)
8296
- `jserror.go` - JavaScript error parsing and formatting
83-
- `internal/engine/` - Migration generation and planning
97+
- `internal/engine/` - **Migration core** (one of multiple built-in cores)
98+
- Consumes structural metadata to generate SQL migrations
8499
- `internal/dialect/` - SQL generation for PostgreSQL, SQLite
85100
- `internal/alerr/` - Structured error system with error codes
86101
- `internal/cli/` - CLI output formatting (Rust/Cargo-style errors)
87-
- `internal/ast/` - Schema AST representation
102+
- `internal/ast/` - Schema AST representation (structural + higher-order metadata)
88103
- `pkg/astroladb/` - Public Go API
89104

90105
## Critical Implementation Details
@@ -104,13 +119,14 @@ This applies to:
104119

105120
- Column methods: `.belongs_to()`, `.created_at`, `.updated_at`
106121
- Table methods: `.sort_by()`, `.searchable()`, `.filterable()`
122+
- Higher-order metadata methods: `.lifecycle()`, `.policy()`, `.events()`, `.meta()`, `.deploy()`
107123
- All future DSL additions
108124

109125
### JavaScript Runtime & Error Handling
110126

111-
**Unified Error Pipeline — Schemas, Migrations, and Generators:**
127+
**Unified Error Pipeline — All DSL Features:**
112128

113-
All three JS file types (schemas, migrations, generators) execute through the **same Sandbox** and must follow the **same error pipeline**:
129+
All JavaScript DSL features (table definitions with higher-order metadata, manual migrations, custom generators) execute through the **same Sandbox** and must follow the **same error pipeline**:
114130

115131
1. JS code executes in Goja VM → validation errors `panic(vm.ToValue(string))`
116132
2. Goja captures the JS call site (line:col) in the Exception stack
@@ -192,6 +208,8 @@ help: try `col.belongs_to('namespace.table')` or `col.belongs_to('.table')` for
192208

193209
**Consistency rule:** If a new DSL feature (schema, migration, or generator) can produce an error, it MUST go through the same pipeline and produce this same format. Test with `CRITICAL` prefix tests in `error_pipeline_test.go`.
194210

211+
**Error preview rule:** Every new error MUST be added to `TestPreviewAllErrors` in `internal/runtime/preview_errors_test.go`. This visual test renders every structured error with `cli.FormatError()` so developers can inspect formatting. Run with `go test -run TestPreviewAllErrors ./internal/runtime/ -v -count=1` or `task preview-errors`.
212+
195213
### Help Text Style Guide (Cargo conventions)
196214

197215
All `help:` text follows Rust/Cargo conventions for consistency:
@@ -235,13 +253,14 @@ All `help:` text follows Rust/Cargo conventions for consistency:
235253

236254
### Project Philosophy (from CONTRIBUTING.md)
237255

238-
**Four Core Principles:**
256+
**Five Core Principles:**
239257

240258
1. **Simple** - One way to do things. No config options where a default will do.
241259
2. **Boring** - No magic. Predictable behavior. Convention over configuration.
242260
- **ALL validation should happen during JavaScript execution**, not post-parse
243261
3. **Deterministic** - Same input = same output, always.
244262
4. **JS-Friendly** - All types safe in JavaScript (no int64, float64, etc.)
263+
5. **Documentation-Oriented** - Schemas are living documentation capturing system intent across structural, behavioral, and operational dimensions. All higher-order metadata is **declarative and generator-consumable only** — no runtime enforcement.
245264

246265
**Hard Constraints:**
247266

internal/alerr/errors.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ const (
3434
ErrMissingLength Code = "VAL-007" // String column requires length
3535
ErrTypeMismatchVal Code = "VAL-008" // Default value type doesn't match column type
3636
ErrMissingReference Code = "VAL-009" // belongs_to/many_to_many requires a reference
37+
ErrLifecycleInvalid Code = "VAL-020" // lifecycle() configuration is invalid
38+
ErrLifecycleColumn Code = "VAL-021" // lifecycle() column is not an enum or references unknown state
39+
ErrPolicyInvalid Code = "VAL-022" // policy() values must be non-empty arrays of role names
40+
ErrEventsInvalid Code = "VAL-023" // events() payload references unknown column
3741

3842
// Migration errors (MIG-xxx) - problems during migration operations
3943
ErrMigrationFailed Code = "MIG-001" // Migration execution failed

internal/ast/table.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ type TableDef struct {
144144
Searchable []string // Columns for fulltext search
145145
Filterable []string // Columns allowed in WHERE clauses
146146

147+
// Higher-order metadata (for generators, not migrations)
148+
Meta map[string]any // Freeform developer metadata (.meta())
149+
Lifecycle *LifecycleDef // State machine on an enum column (.lifecycle())
150+
Policy *PolicyDef // Access intent per action (.policy())
151+
Events map[string]*EventDef // Event declarations (.events())
152+
147153
// Source location (for error reporting)
148154
SourceFile string // Path to the schema file that defined this table
149155
}
@@ -608,6 +614,44 @@ func (c *CheckDef) Validate() error {
608614
return nil
609615
}
610616

617+
// -----------------------------------------------------------------------------
618+
// LifecycleDef - state machine on an enum column
619+
// -----------------------------------------------------------------------------
620+
621+
// LifecycleDef represents a state machine declared on an enum column.
622+
// States are derived from the referenced col.enum() values at export time.
623+
type LifecycleDef struct {
624+
Column string // Enum column name (states derived from it)
625+
Transitions map[string]*TransitionDef // transition_name → {from, to}
626+
}
627+
628+
// TransitionDef represents a single state transition.
629+
type TransitionDef struct {
630+
From string // Source state (must be valid enum value)
631+
To string // Target state (must be valid enum value)
632+
}
633+
634+
// -----------------------------------------------------------------------------
635+
// PolicyDef - access intent per action
636+
// -----------------------------------------------------------------------------
637+
638+
// PolicyDef represents access policy declarations: action → [roles].
639+
// Actions and roles are freeform strings — no built-in CRUD constraint.
640+
type PolicyDef struct {
641+
Rules map[string][]string // action → [roles]
642+
}
643+
644+
// -----------------------------------------------------------------------------
645+
// EventDef - event declaration
646+
// -----------------------------------------------------------------------------
647+
648+
// EventDef represents an event that this entity emits.
649+
// Payload column names are validated against the table's columns.
650+
type EventDef struct {
651+
Payload []string // Column names included in the event payload
652+
Topic string // Optional publish target (e.g., "orders.created")
653+
}
654+
611655
// -----------------------------------------------------------------------------
612656
// Reference - column reference (for belongs_to, one_to_one)
613657
// -----------------------------------------------------------------------------

internal/runtime/builder/column.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package builder
22

33
import (
44
"github.com/dop251/goja"
5+
6+
"github.com/hlop3z/astroladb/internal/ast"
57
)
68

79
// -----------------------------------------------------------------------------
@@ -117,7 +119,7 @@ func (cb *ColBuilder) ToObject() *goja.Object {
117119
col := &ColumnDef{
118120
Type: "uuid",
119121
IsRelationship: true,
120-
Reference: &RefDef{
122+
Reference: &ast.Reference{
121123
Table: ref,
122124
Column: "id",
123125
},
@@ -134,7 +136,7 @@ func (cb *ColBuilder) ToObject() *goja.Object {
134136
Type: "uuid",
135137
Unique: true,
136138
IsRelationship: true,
137-
Reference: &RefDef{
139+
Reference: &ast.Reference{
138140
Table: ref,
139141
Column: "id",
140142
},

0 commit comments

Comments
 (0)