Skip to content

Commit 750dc46

Browse files
committed
chore(release): bump all versions to 1.4.1 and add lifecycle hooks docs
Bump Python packages (pyproject.toml, __init__.py) and all five Rust crates from 1.4.0/1.4.0-rc0 to 1.4.1. Docs: - CHANGELOG: add [1.4.1] section covering lifecycle hooks (Python ORM, Rust ORM, Rust Server), Put clause, and server API simplification - docs/guide/crud.md: add Lifecycle Hooks section with full API reference, examples (audit, validation, filtering, composition) - docs/guide/index.md: add hooks to Quick Reference - README.md: add Lifecycle Hooks to feature list - CLAUDE.md: add hooks.py to project structure tree Fix: - Export missing CrudHook protocol from type_bridge top-level __all__
1 parent 4d38e3b commit 750dc46

14 files changed

Lines changed: 159 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,33 @@ All notable changes to TypeBridge will be documented in this file.
44

55
## [Unreleased]
66

7+
## [1.4.1] - 2026-03-03
8+
79
### New Features
810

11+
#### Lifecycle Hook System (PR #118, closes #116)
12+
13+
Three-layer hook system for reacting to CRUD lifecycle events (audit logging, validation, cache invalidation, async notifications).
14+
15+
##### Python ORM — `type_bridge.crud.hooks`
16+
- **`CrudEvent` enum**`PRE_INSERT`, `POST_INSERT`, `PRE_UPDATE`, `POST_UPDATE`, `PRE_DELETE`, `POST_DELETE`, `PRE_PUT`, `POST_PUT`
17+
- **`LifecycleHook` protocol** — implement only the methods you need (`pre_insert`, `post_delete`, etc.)
18+
- **`HookCancelled` exception** — raise in a pre-hook to abort the operation
19+
- **Per-manager registration**`manager.add_hook(hook)` / `manager.remove_hook(hook)`, chainable
20+
- **`should_run(event, sender)`** filtering by event type or model class
21+
- Pre-hooks run in registration order; post-hooks run in reverse order (middleware unwinding)
22+
- Zero overhead when no hooks are registered
23+
24+
##### Rust ORM — `type-bridge-orm`
25+
- **`LifecycleHook` trait** with `HookContext`, `PreHookResult` (Continue/Reject), and `HookRunner`
26+
- Integrated into `EntityManager` and `RelationManager` via `add_hook()`
27+
- Same semantics as the Python layer (registration-order pre-hooks, reverse-order post-hooks)
28+
29+
##### Rust Server — `type-bridge-server`
30+
- **`CrudInfo`** on `RequestContext` — operation, type_name, type_kind, attribute_names, iid
31+
- **`CrudInterceptor` trait** with `on_crud_request` / `on_crud_response` and `should_intercept`
32+
- **`CrudInterceptorAdapter`** bridges `CrudInterceptor` into the existing `Interceptor` chain
33+
934
#### Put (Upsert) Clause — `type-bridge-core-lib`
1035
- **Added `Clause::Put(Vec<Statement>)` variant** for idempotent insert (upsert) operations
1136
- Parser: `parse_put_clause` with keyword lookahead in both `parse_patterns` and `parse_statements`
@@ -16,11 +41,7 @@ All notable changes to TypeBridge will be documented in this file.
1641
### Refactoring
1742

1843
#### Server API Simplification — `type-bridge-server`
19-
- **Removed CRUD convenience endpoints** (`/entities/*`, `/relations/*`) — the `/query` endpoint handles all AST-based queries
20-
- **Removed `/query/raw` endpoint** — raw TypeQL bypasses AST interception, defeating the middleware purpose
21-
- **Removed `CrudInfo` and `CrudInterceptor`** — built exclusively for CRUD handlers, dead weight without them
22-
- **Removed `execute_raw()` and `RawQueryInput`** from pipeline
23-
- **Removed `RawQueryRequest`** from transport types
44+
- **Removed standalone CRUD module** (`/entities/*`, `/relations/*` endpoints, `CrudQueryBuilder`, raw query support) — superseded by the interceptor-based design
2445
- **Removed `crud_builder` benchmark** and `criterion` dev-dependency
2546
- **Server now has 4 endpoints**: `POST /query`, `POST /query/validate`, `GET /health`, `GET /schema`
2647

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ type_bridge/
8484
│ ├── base.py # Type variables (E, R)
8585
│ ├── utils.py # Shared utilities (format_value, is_multi_value_attribute)
8686
│ ├── exceptions.py # CRUD exceptions
87+
│ ├── hooks.py # Lifecycle hooks (CrudEvent, HookCancelled, CrudHook, HookRunner)
8788
│ ├── entity/ # Entity CRUD operations
8889
│ │ ├── __init__.py # Entity module exports
8990
│ │ ├── manager.py # EntityManager class

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ A modern, Pythonic ORM for [TypeDB](https://github.com/typedb/typedb) with an At
2525
- **Data Validation**: Automatic type checking and coercion via Pydantic, including keyword validation
2626
- **JSON Support**: Seamless JSON serialization/deserialization
2727
- **CRUD Operations**: Full CRUD with fetching API (get, filter, all, update) for entities and relations
28+
- **Lifecycle Hooks**: Pre/post-operation hooks for audit logging, validation, cache invalidation, and async notifications
2829
- **Chainable Operations**: Filter, delete, and bulk update with method chaining and lambda functions
2930
- **Query Builder**: Pythonic interface for building TypeQL queries
3031
- **Multi-player Roles**: A single role can accept multiple entity types via `Role.multi(...)`

docs/guide/crud.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,6 +1532,113 @@ def test_database_operations():
15321532
mock_driver.databases.contains.assert_called_with("test_db")
15331533
```
15341534

1535+
## Lifecycle Hooks
1536+
1537+
Hooks let you react to CRUD events for cross-cutting concerns — audit logging, input validation, cache invalidation, auto-populating fields, and more.
1538+
1539+
### Quick Example
1540+
1541+
```python
1542+
from type_bridge import CrudEvent, HookCancelled
1543+
1544+
class AuditHook:
1545+
"""Log every write operation."""
1546+
1547+
def post_insert(self, sender, instance):
1548+
print(f"[insert] {sender.__name__} iid={instance.iid}")
1549+
1550+
def post_update(self, sender, instance):
1551+
print(f"[update] {sender.__name__} iid={instance.iid}")
1552+
1553+
def post_delete(self, sender, instance):
1554+
print(f"[delete] {sender.__name__} iid={instance.iid}")
1555+
1556+
1557+
manager = Person.manager(db)
1558+
manager.add_hook(AuditHook()) # chainable — returns self
1559+
```
1560+
1561+
### Events
1562+
1563+
The `CrudEvent` enum covers all eight lifecycle points:
1564+
1565+
| Event | When it fires |
1566+
|-------|---------------|
1567+
| `PRE_INSERT` | Before inserting an entity/relation |
1568+
| `POST_INSERT` | After a successful insert |
1569+
| `PRE_UPDATE` | Before updating |
1570+
| `POST_UPDATE` | After a successful update |
1571+
| `PRE_DELETE` | Before deleting |
1572+
| `POST_DELETE` | After a successful delete |
1573+
| `PRE_PUT` | Before an idempotent put (upsert) |
1574+
| `POST_PUT` | After a successful put |
1575+
1576+
### Writing a Hook
1577+
1578+
Hooks are **duck-typed** — implement only the methods you need. No base class required.
1579+
1580+
```python
1581+
class TimestampHook:
1582+
"""Auto-populate created_at on insert."""
1583+
1584+
def pre_insert(self, sender, instance):
1585+
if hasattr(instance, "created_at") and instance.created_at is None:
1586+
instance.created_at = CreatedAt(datetime.now(timezone.utc))
1587+
```
1588+
1589+
### Cancelling Operations
1590+
1591+
Raise `HookCancelled` in any pre-hook to abort the operation:
1592+
1593+
```python
1594+
class EmailDomainValidator:
1595+
def __init__(self, domain: str):
1596+
self.domain = domain
1597+
1598+
def pre_insert(self, sender, instance):
1599+
if hasattr(instance, "email"):
1600+
if not instance.email.value.endswith(f"@{self.domain}"):
1601+
raise HookCancelled(f"Email must end with @{self.domain}")
1602+
1603+
def pre_update(self, sender, instance):
1604+
self.pre_insert(sender, instance) # same logic
1605+
```
1606+
1607+
### Filtering with `should_run`
1608+
1609+
Implement `should_run(event, sender)` to restrict when a hook fires:
1610+
1611+
```python
1612+
class PersonOnlyHook:
1613+
def should_run(self, event, sender):
1614+
return sender.__name__ == "Person"
1615+
1616+
def post_insert(self, sender, instance):
1617+
print(f"New person: {instance}")
1618+
```
1619+
1620+
Without `should_run`, hooks run for every event on every model.
1621+
1622+
### Registration and Composition
1623+
1624+
```python
1625+
manager = (
1626+
Person.manager(db)
1627+
.add_hook(TimestampHook())
1628+
.add_hook(EmailDomainValidator("company.com"))
1629+
.add_hook(AuditHook())
1630+
)
1631+
1632+
# Remove a hook later
1633+
manager.remove_hook(audit_hook)
1634+
```
1635+
1636+
### Execution Order
1637+
1638+
- **Pre-hooks** run in registration order. If any raises `HookCancelled`, the operation is aborted.
1639+
- **Post-hooks** run in **reverse** registration order (middleware unwinding). Post-hook errors are logged but do not propagate.
1640+
- **Zero overhead** when no hooks are registered — the runner short-circuits on an empty hook list.
1641+
15351642
## See Also
15361643

15371644
- [Entities](entities.md) - Entity definition

docs/guide/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ alice = Person(name=Name("Alice"), age=Age(30))
5858
person_manager = Person.manager(db)
5959
person_manager.insert(alice)
6060
persons = person_manager.all()
61+
62+
# 5. Add lifecycle hooks (optional)
63+
person_manager.add_hook(my_audit_hook) # chainable
6164
```
6265

6366
## Key Principles

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "type-bridge"
7-
version = "1.4.0"
7+
version = "1.4.1"
88
description = "A modern, Pythonic ORM for TypeDB with an Attribute-based API"
99
readme = "README.md"
1010
requires-python = ">=3.13"
@@ -29,7 +29,7 @@ dependencies = [
2929
"lark>=1.1.9",
3030
"jinja2>=3.1.0",
3131
"typer>=0.15.0",
32-
"type-bridge-core>=1.4.0",
32+
"type-bridge-core>=1.4.1",
3333
]
3434

3535
[project.urls]

type-bridge-core/Cargo.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

type-bridge-core/crates/core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "type-bridge-core-lib"
3-
version = "1.4.0"
3+
version = "1.4.1"
44
edition = "2024"
55
description = "TypeQL AST, schema parser, query compiler, and validation engine for type-bridge"
66
license.workspace = true

type-bridge-core/crates/orm-derive/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "type-bridge-orm-derive"
3-
version = "1.4.0"
3+
version = "1.4.1"
44
edition = "2024"
55
description = "Derive macros for type-bridge-orm: TypeBridgeEntity, TypeBridgeAttribute, TypeBridgeRelation"
66
license.workspace = true
@@ -14,7 +14,7 @@ proc-macro = true
1414
proc-macro2 = "1"
1515
quote = "1"
1616
syn = { version = "2", features = ["full", "extra-traits"] }
17-
type-bridge-core-lib = { path = "../core", version = "1.4.0" }
17+
type-bridge-core-lib = { path = "../core", version = "1.4.1" }
1818

1919
[lints]
2020
workspace = true

type-bridge-core/crates/orm/Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "type-bridge-orm"
3-
version = "1.4.0"
3+
version = "1.4.1"
44
edition = "2024"
55
description = "Async ORM for TypeDB built on type-bridge-core-lib"
66
license.workspace = true
@@ -21,13 +21,13 @@ typedb = ["dep:typedb-driver", "dep:futures"]
2121
derive = ["dep:type-bridge-orm-derive"]
2222

2323
[dependencies]
24-
type-bridge-core-lib = { path = "../core", version = "1.4.0" }
24+
type-bridge-core-lib = { path = "../core", version = "1.4.1" }
2525
tokio = { version = "1", features = ["full"] }
2626
serde = { version = "1.0", features = ["derive"] }
2727
serde_json = "1.0"
2828
thiserror = "2"
2929
tracing = "0.1"
30-
type-bridge-orm-derive = { path = "../orm-derive", version = "1.4.0", optional = true }
30+
type-bridge-orm-derive = { path = "../orm-derive", version = "1.4.1", optional = true }
3131

3232
# Optional: real TypeDB backend
3333
typedb-driver = { version = "3", optional = true }

0 commit comments

Comments
 (0)