npm package: @hamicek/noex-logic
Description: Declarative logic layer for noex-ecosystem — expressions, computed fields, derived views, constraints
Latest version: 0.1.1

# @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

```bash
npm install @hamicek/noex-logic
```

Requires `@hamicek/noex-store` (dependency) and Node.js >= 20.

## Quick Start

```typescript
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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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 StorageAdapter
```

## Requirements

- Node.js >= 20.0.0
- TypeScript >= 5.7.0 (for development)

## License

MIT