|
@@ -242,6 +242,98 @@ if (!db.updateIfVersion("users", "user-123", *doc, version)) {
|
|
|
}
|
|
}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
|
|
+### Views
|
|
|
|
|
+
|
|
|
|
|
+Views are named, read-only projections over collections. They filter which fields are returned and optionally enforce baked-in filters and default sort order. Views are ideal for:
|
|
|
|
|
+
|
|
|
|
|
+- **Schema evolution** — add fields to a collection without affecting consumers that query through a view
|
|
|
|
|
+- **Security boundary** — hide sensitive fields (passwords, tokens) and enforce row-level constraints (status=active, tenant=X)
|
|
|
|
|
+- **API clarity** — name and document a stable contract ("this view returns these fields with these filters")
|
|
|
|
|
+
|
|
|
|
|
+#### Creating a view
|
|
|
|
|
+
|
|
|
|
|
+```cpp
|
|
|
|
|
+using Op = smartbotic::database::Client::FilterOp;
|
|
|
|
|
+
|
|
|
|
|
+// Simple: just project fields
|
|
|
|
|
+db.createView("users_public",
|
|
|
|
|
+ /*collection=*/"users",
|
|
|
|
|
+ /*include=*/{"id", "name", "avatar_url", "profile.public_bio"});
|
|
|
|
|
+
|
|
|
|
|
+// Full-featured: fields + filters + default sort
|
|
|
|
|
+db.createView("active_admins",
|
|
|
|
|
+ /*collection=*/"users",
|
|
|
|
|
+ /*include=*/{"id", "name", "role"},
|
|
|
|
|
+ /*exclude=*/{},
|
|
|
|
|
+ /*where=*/{
|
|
|
|
|
+ {"status", Op::EQ, "active"},
|
|
|
|
|
+ {"role", Op::EQ, "admin"}
|
|
|
|
|
+ },
|
|
|
|
|
+ /*defaultSort=*/smartbotic::database::Client::Sort{"name", false});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### Querying a view
|
|
|
|
|
+
|
|
|
|
|
+Views work like collections — use the view name in `get()`, `find()`, `count()`:
|
|
|
|
|
+
|
|
|
|
|
+```cpp
|
|
|
|
|
+auto user = db.get("users_public", "u1"); // projection applied
|
|
|
|
|
+auto results = db.find("active_admins", {
|
|
|
|
|
+ .filters = {{"department", Op::EQ, "eng"}}, // AND-merged with view's where
|
|
|
|
|
+ .limit = 50
|
|
|
|
|
+});
|
|
|
|
|
+auto total = db.count("active_admins", {{"department", Op::EQ, "eng"}});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### Inspecting and dropping
|
|
|
|
|
+
|
|
|
|
|
+```cpp
|
|
|
|
|
+auto views = db.listViews();
|
|
|
|
|
+auto info = db.getViewInfo("users_public");
|
|
|
|
|
+db.dropView("users_public");
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### Semantics
|
|
|
|
|
+
|
|
|
|
|
+| Dimension | Behavior |
|
|
|
|
|
+|-----------|----------|
|
|
|
|
|
+| **Filters** | **View AND caller** — view's `where` is always applied. Caller's filters are merged with AND. Consumers cannot escape the view's constraints. |
|
|
|
|
|
+| **Sort** | **Caller overrides view** — if the caller specifies a sort, it wins. Otherwise the view's `defaultSort` is used. |
|
|
|
|
|
+| **Include** | Whitelist — when non-empty, only those paths are returned. |
|
|
|
|
|
+| **Exclude** | Blacklist — ignored if include is non-empty; otherwise removes listed paths. |
|
|
|
|
|
+| **Metadata** | `_id`, `_version`, `_created_at`, `_updated_at`, `_created_by`, `_updated_by` are always returned regardless of include/exclude. |
|
|
|
|
|
+| **Nested paths** | Dot-notation supported: `user.profile.name`. Stops at arrays. |
|
|
|
|
|
+| **Writes** | Rejected — `insert()`, `update()`, `patch()`, `upsert()`, `remove()` on a view name return an error. |
|
|
|
|
|
+| **View-of-view** | Not allowed — views must target real collections. |
|
|
|
|
|
+
|
|
|
|
|
+#### Via migrations (recommended)
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "version": "020",
|
|
|
|
|
+ "name": "create_active_admins_view",
|
|
|
|
|
+ "operations": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "type": "create_view",
|
|
|
|
|
+ "name": "active_admins",
|
|
|
|
|
+ "collection": "users",
|
|
|
|
|
+ "include": ["id", "name", "role"],
|
|
|
|
|
+ "where": [
|
|
|
|
|
+ {"field": "status", "op": "EQ", "value": "active"},
|
|
|
|
|
+ {"field": "role", "op": "EQ", "value": "admin"}
|
|
|
|
|
+ ],
|
|
|
|
|
+ "default_sort": {"field": "name", "descending": false}
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+The `op` field in `where` filters accepts either a string name (`EQ`, `NE`, `GT`, `GTE`, `LT`, `LTE`, `IN`, `CONTAINS`, `EXISTS`, `REGEX`, `SEARCH`) or the matching integer. Migrations are idempotent — re-running a migration that creates an existing view is a no-op.
|
|
|
|
|
+
|
|
|
|
|
+#### Why AND-merge filters?
|
|
|
|
|
+
|
|
|
|
|
+The view's filters act as a **security enforcement boundary**. A consumer querying `active_admins` literally cannot see inactive users — the filter is applied server-side, after authentication, before projection. This is how SQL views with `WHERE` clauses work, and how row-level security works in most databases. Any consumer filter is ANDed on top, narrowing further.
|
|
|
|
|
+
|
|
|
### Querying
|
|
### Querying
|
|
|
|
|
|
|
|
```cpp
|
|
```cpp
|