@hamicek/noex-logic
v0.1.1
Published
Declarative logic layer for noex-ecosystem — expressions, computed fields, derived views, constraints
Downloads
172
Readme
@hamicek/noex-logic
Declarative logic layer for noex-ecosystem — expressions, computed fields, derived views, constraints.
Features
- Expression Evaluator — JSON-based expression trees with arithmetic, comparison, logical, string, and date operators
- Computed Fields — automatic, reactive field computation with dependency tracking and topological ordering
- Derived Views — cross-bucket queries with joins, filters, aggregations, and incremental materialization
- Integrity Constraints — record-scope and group-scope validation rules powered by expressions
- Persistence — optional save/restore of all definitions via a StorageAdapter
- Expression Validation — static analysis with depth/width limits and referenced-field extraction
Installation
npm install @hamicek/noex-logicRequires @hamicek/noex-store (dependency) and Node.js >= 20.
Quick Start
import { Store } from '@hamicek/noex-store';
import { Logic } from '@hamicek/noex-logic';
// 1. Create a store with a bucket
const store = await Store.start({ name: 'shop' });
store.defineBucket('products', {
key: 'id',
schema: {
id: { type: 'string', generated: 'uuid' },
name: { type: 'string', required: true },
price: { type: 'number', required: true },
quantity: { type: 'number', required: true },
},
});
// 2. Start the Logic layer
const logic = await Logic.start({ store });
// 3. Define computed fields — total is price * quantity
await logic.defineComputed('products', {
total: {
depends: ['price', 'quantity'],
expr: { $multiply: ['$price', '$quantity'] },
},
});
// 4. Define an integrity constraint — price must be positive
await logic.defineConstraint({
name: 'positive-price',
on: 'products',
expr: { $gt: ['$price', 0] },
message: 'Price must be greater than zero',
});
// 5. Define a derived view — expensive products
await logic.defineView({
name: 'expensive-products',
from: { p: 'products' },
where: { 'p.total': { $gt: 1000 } },
select: { name: 'p.name', total: 'p.total' },
orderBy: [{ field: 'total', direction: 'desc' }],
reactive: true,
});
// 6. Subscribe to view updates
const unsub = logic.subscribeView('expensive-products', (results) => {
console.log('Expensive products:', results);
});
// Insert data — computed fields and views update automatically
const bucket = store.bucket('products');
await bucket.insert({ name: 'Widget', price: 50, quantity: 30 });
await logic.settle();
// Clean up
unsub();
await logic.destroy();
await store.stop();API
Logic
Logic.start(config): Promise<Logic>
Creates and starts a Logic instance.
| Option | Type | Description |
|--------|------|-------------|
| store | Store | noex-store instance (required) |
| persistence | StorageAdapter | Optional adapter for persisting definitions |
Computed Fields
// Define computed fields for a bucket
await logic.defineComputed('products', {
total: {
depends: ['price', 'quantity'],
expr: { $multiply: ['$price', '$quantity'] },
},
discounted: {
depends: ['total'],
expr: { $multiply: ['$total', 0.9] },
},
});
// Drop computed fields
await logic.dropComputed('products');
// Inspect definitions
const config = logic.getComputed('products');
const all = logic.listComputed();Derived Views
// Simple view with filter and sort
await logic.defineView({
name: 'active-users',
from: { u: 'users' },
where: { 'u.active': true },
select: { name: 'u.name', email: 'u.email' },
orderBy: [{ field: 'name', direction: 'asc' }],
reactive: true,
});
// Join view — orders with user names
await logic.defineView({
name: 'order-details',
from: { o: 'orders', u: 'users' },
join: { 'o.userId': 'u.id' },
select: {
orderId: 'o.id',
userName: 'u.name',
total: 'o.total',
},
reactive: true,
});
// Aggregate view — revenue per category
await logic.defineView({
name: 'category-revenue',
from: { p: 'products' },
groupBy: 'p.category',
select: {
category: 'p.category',
totalRevenue: { $sum: '$p.total' },
count: { $count: '*' },
},
reactive: true,
});
// Query and subscribe
const results = logic.queryView('active-users');
const unsub = logic.subscribeView('active-users', (data) => { /* ... */ });
// Explain query plan
const plan = logic.explainView('order-details');
// Drop and list
await logic.dropView('active-users');
const views = logic.listViews();Constraints
// Record-scope constraint
await logic.defineConstraint({
name: 'valid-age',
on: 'users',
expr: { $and: [{ $gte: ['$age', 0] }, { $lte: ['$age', 150] }] },
message: 'Age must be between 0 and 150',
operations: ['insert', 'update'],
});
// Group-scope constraint — max 5 active orders per user
await logic.defineConstraint({
name: 'max-active-orders',
on: 'orders',
expr: { $lte: [{ $count: '*' }, 5] },
message: 'Maximum 5 active orders per user',
scope: 'group',
groupBy: 'userId',
});
// Validate manually
logic.validateRecord('users', record, 'insert');
await logic.validateGroup('orders', record, 'insert');
// Drop and inspect
await logic.dropConstraint('valid-age');
const constraint = logic.getConstraint('valid-age');
const all = logic.listConstraints();Expressions
// Evaluate an expression directly
const result = logic.evaluateExpression(
{ $add: ['$price', '$tax'] },
{ price: 100, tax: 21 },
);
// → 121
// Validate an expression
const validation = logic.validateExpression({ $add: ['$price', '$tax'] });
// → { valid: true, referencedFields: ['price', 'tax'], errors: [] }Lifecycle
// Wait for all pending recomputations
await logic.settle();
// Destroy the Logic instance
await logic.destroy();Expression Operators
| Category | Operators |
|----------|-----------|
| Arithmetic | $add, $subtract, $multiply, $divide, $mod, $abs, $ceil, $floor, $round |
| Comparison | $eq, $ne, $gt, $gte, $lt, $lte |
| Logical | $and, $or, $not, $cond |
| String | $concat, $toUpper, $toLower, $trim, $substring, $length |
| Date | $now, $dateAdd, $dateDiff |
| Aggregation (views) | $sum, $avg, $min, $max, $count |
Field references use '$fieldName' syntax (e.g. '$price', '$user.name').
Architecture
Logic
├── ExpressionEvaluator — operator registry + recursive evaluation
├── ComputedManager — per-bucket field definitions + topological sort
├── ViewManager — view definitions + materialization
│ └── ViewMaterializer — incremental simple / join / aggregate strategies
├── ConstraintManager — record + group constraint validation
└── LogicPersistence — save / restore definitions via StorageAdapterRequirements
- Node.js >= 20.0.0
- TypeScript >= 5.7.0 (for development)
License
MIT
