dzql 0.5.33 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.sample +28 -0
- package/compose.yml +28 -0
- package/dist/client/index.ts +1 -0
- package/dist/client/stores/useMyProfileStore.ts +114 -0
- package/dist/client/stores/useOrgDashboardStore.ts +131 -0
- package/dist/client/stores/useVenueDetailStore.ts +117 -0
- package/dist/client/ws.ts +716 -0
- package/dist/db/migrations/000_core.sql +92 -0
- package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
- package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
- package/dist/runtime/manifest.json +1562 -0
- package/docs/README.md +293 -36
- package/docs/feature-requests/applyPatch-bug-report.md +85 -0
- package/docs/feature-requests/connection-ready-profile.md +57 -0
- package/docs/feature-requests/hidden-bug-report.md +111 -0
- package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
- package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
- package/docs/feature-requests/todo.md +146 -0
- package/docs/for_ai.md +641 -0
- package/docs/project-setup.md +432 -0
- package/examples/blog.ts +50 -0
- package/examples/invalid.ts +18 -0
- package/examples/venues.js +485 -0
- package/package.json +23 -60
- package/src/cli/codegen/client.ts +99 -0
- package/src/cli/codegen/manifest.ts +95 -0
- package/src/cli/codegen/pinia.ts +174 -0
- package/src/cli/codegen/realtime.ts +58 -0
- package/src/cli/codegen/sql.ts +698 -0
- package/src/cli/codegen/subscribable_sql.ts +547 -0
- package/src/cli/codegen/subscribable_store.ts +184 -0
- package/src/cli/codegen/types.ts +142 -0
- package/src/cli/compiler/analyzer.ts +52 -0
- package/src/cli/compiler/graph_rules.ts +251 -0
- package/src/cli/compiler/ir.ts +233 -0
- package/src/cli/compiler/loader.ts +132 -0
- package/src/cli/compiler/permissions.ts +227 -0
- package/src/cli/index.ts +164 -0
- package/src/client/index.ts +1 -0
- package/src/client/ws.ts +286 -0
- package/src/create/.env.example +8 -0
- package/src/create/README.md +101 -0
- package/src/create/compose.yml +14 -0
- package/src/create/domain.ts +153 -0
- package/src/create/package.json +24 -0
- package/src/create/server.ts +18 -0
- package/src/create/setup.sh +11 -0
- package/src/create/tsconfig.json +15 -0
- package/src/runtime/auth.ts +39 -0
- package/src/runtime/db.ts +33 -0
- package/src/runtime/errors.ts +51 -0
- package/src/runtime/index.ts +98 -0
- package/src/runtime/js_functions.ts +63 -0
- package/src/runtime/manifest_loader.ts +29 -0
- package/src/runtime/namespace.ts +483 -0
- package/src/runtime/server.ts +87 -0
- package/src/runtime/ws.ts +197 -0
- package/src/shared/ir.ts +197 -0
- package/tests/client.test.ts +38 -0
- package/tests/codegen.test.ts +71 -0
- package/tests/compiler.test.ts +45 -0
- package/tests/graph_rules.test.ts +173 -0
- package/tests/integration/db.test.ts +174 -0
- package/tests/integration/e2e.test.ts +65 -0
- package/tests/integration/features.test.ts +922 -0
- package/tests/integration/full_stack.test.ts +262 -0
- package/tests/integration/setup.ts +45 -0
- package/tests/ir.test.ts +32 -0
- package/tests/namespace.test.ts +395 -0
- package/tests/permissions.test.ts +55 -0
- package/tests/pinia.test.ts +48 -0
- package/tests/realtime.test.ts +22 -0
- package/tests/runtime.test.ts +80 -0
- package/tests/subscribable_gen.test.ts +72 -0
- package/tests/subscribable_reactivity.test.ts +258 -0
- package/tests/venues_gen.test.ts +25 -0
- package/tsconfig.json +20 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/README.md +0 -90
- package/bin/cli.js +0 -727
- package/docs/compiler/ADVANCED_FILTERS.md +0 -183
- package/docs/compiler/CODING_STANDARDS.md +0 -415
- package/docs/compiler/COMPARISON.md +0 -673
- package/docs/compiler/QUICKSTART.md +0 -326
- package/docs/compiler/README.md +0 -134
- package/docs/examples/README.md +0 -38
- package/docs/examples/blog.sql +0 -160
- package/docs/examples/venue-detail-simple.sql +0 -8
- package/docs/examples/venue-detail-subscribable.sql +0 -45
- package/docs/for-ai/claude-guide.md +0 -1210
- package/docs/getting-started/quickstart.md +0 -125
- package/docs/getting-started/subscriptions-quick-start.md +0 -203
- package/docs/getting-started/tutorial.md +0 -1104
- package/docs/guides/atomic-updates.md +0 -299
- package/docs/guides/client-stores.md +0 -730
- package/docs/guides/composite-primary-keys.md +0 -158
- package/docs/guides/custom-functions.md +0 -362
- package/docs/guides/drop-semantics.md +0 -554
- package/docs/guides/field-defaults.md +0 -240
- package/docs/guides/interpreter-vs-compiler.md +0 -237
- package/docs/guides/many-to-many.md +0 -929
- package/docs/guides/subscriptions.md +0 -537
- package/docs/reference/api.md +0 -1373
- package/docs/reference/client.md +0 -224
- package/src/client/stores/index.js +0 -8
- package/src/client/stores/useAppStore.js +0 -285
- package/src/client/stores/useWsStore.js +0 -289
- package/src/client/ws.js +0 -762
- package/src/compiler/cli/compile-example.js +0 -33
- package/src/compiler/cli/compile-subscribable.js +0 -43
- package/src/compiler/cli/debug-compile.js +0 -44
- package/src/compiler/cli/debug-parse.js +0 -26
- package/src/compiler/cli/debug-path-parser.js +0 -18
- package/src/compiler/cli/debug-subscribable-parser.js +0 -21
- package/src/compiler/cli/index.js +0 -174
- package/src/compiler/codegen/auth-codegen.js +0 -153
- package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
- package/src/compiler/codegen/graph-rules-codegen.js +0 -450
- package/src/compiler/codegen/notification-codegen.js +0 -232
- package/src/compiler/codegen/operation-codegen.js +0 -1382
- package/src/compiler/codegen/permission-codegen.js +0 -318
- package/src/compiler/codegen/subscribable-codegen.js +0 -827
- package/src/compiler/compiler.js +0 -371
- package/src/compiler/index.js +0 -11
- package/src/compiler/parser/entity-parser.js +0 -440
- package/src/compiler/parser/path-parser.js +0 -290
- package/src/compiler/parser/subscribable-parser.js +0 -244
- package/src/database/dzql-core.sql +0 -161
- package/src/database/migrations/001_schema.sql +0 -60
- package/src/database/migrations/002_functions.sql +0 -890
- package/src/database/migrations/003_operations.sql +0 -1135
- package/src/database/migrations/004_search.sql +0 -581
- package/src/database/migrations/005_entities.sql +0 -730
- package/src/database/migrations/006_auth.sql +0 -94
- package/src/database/migrations/007_events.sql +0 -133
- package/src/database/migrations/008_hello.sql +0 -18
- package/src/database/migrations/008a_meta.sql +0 -172
- package/src/database/migrations/009_subscriptions.sql +0 -240
- package/src/database/migrations/010_atomic_updates.sql +0 -157
- package/src/database/migrations/010_fix_m2m_events.sql +0 -94
- package/src/index.js +0 -40
- package/src/server/api.js +0 -9
- package/src/server/db.js +0 -442
- package/src/server/index.js +0 -317
- package/src/server/logger.js +0 -259
- package/src/server/mcp.js +0 -594
- package/src/server/meta-route.js +0 -251
- package/src/server/namespace.js +0 -292
- package/src/server/subscriptions.js +0 -351
- package/src/server/ws.js +0 -573
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
# DZQL Quick Start
|
|
2
|
-
|
|
3
|
-
Get a real-time API with automatic CRUD in 5 minutes.
|
|
4
|
-
|
|
5
|
-
## Prerequisites
|
|
6
|
-
|
|
7
|
-
- PostgreSQL (local or Docker)
|
|
8
|
-
- Bun or Node.js 18+
|
|
9
|
-
|
|
10
|
-
## 1. Install
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
mkdir my-app && cd my-app
|
|
14
|
-
bun init -y
|
|
15
|
-
bun add dzql
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## 2. Start PostgreSQL
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
docker run -d --name dzql-db \
|
|
22
|
-
-e POSTGRES_USER=dzql \
|
|
23
|
-
-e POSTGRES_PASSWORD=dzql \
|
|
24
|
-
-e POSTGRES_DB=dzql \
|
|
25
|
-
-p 5432:5432 \
|
|
26
|
-
postgres:latest
|
|
27
|
-
|
|
28
|
-
export DATABASE_URL="postgresql://dzql:dzql@localhost:5432/dzql"
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## 3. Initialize Database
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
bunx dzql db:init
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## 4. Define Entities
|
|
38
|
-
|
|
39
|
-
Create `entities.sql`:
|
|
40
|
-
|
|
41
|
-
```sql
|
|
42
|
-
-- Schema
|
|
43
|
-
CREATE TABLE users (
|
|
44
|
-
id SERIAL PRIMARY KEY,
|
|
45
|
-
email TEXT UNIQUE NOT NULL,
|
|
46
|
-
name TEXT,
|
|
47
|
-
created_at TIMESTAMPTZ DEFAULT now()
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
CREATE TABLE todos (
|
|
51
|
-
id SERIAL PRIMARY KEY,
|
|
52
|
-
title TEXT NOT NULL,
|
|
53
|
-
completed BOOLEAN DEFAULT false,
|
|
54
|
-
user_id INT REFERENCES users(id),
|
|
55
|
-
created_at TIMESTAMPTZ DEFAULT now()
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
-- Register with DZQL
|
|
59
|
-
SELECT dzql.register_entity('users', 'name', ARRAY['name', 'email']);
|
|
60
|
-
SELECT dzql.register_entity('todos', 'title', ARRAY['title']);
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## 5. Compile
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
bunx dzql compile entities.sql -o init_db/
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## 6. Apply
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
psql $DATABASE_URL -f init_db/001_schema.sql
|
|
73
|
-
psql $DATABASE_URL -f init_db/users.sql
|
|
74
|
-
psql $DATABASE_URL -f init_db/todos.sql
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## 7. Create Server
|
|
78
|
-
|
|
79
|
-
Create `index.js`:
|
|
80
|
-
|
|
81
|
-
```javascript
|
|
82
|
-
import { createServer } from 'dzql/server';
|
|
83
|
-
|
|
84
|
-
createServer({ port: 3000 });
|
|
85
|
-
console.log('Server running at http://localhost:3000');
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## 8. Use
|
|
89
|
-
|
|
90
|
-
```javascript
|
|
91
|
-
import { WebSocketManager } from 'dzql/client';
|
|
92
|
-
|
|
93
|
-
const ws = new WebSocketManager();
|
|
94
|
-
await ws.connect();
|
|
95
|
-
|
|
96
|
-
// Auto-generated CRUD
|
|
97
|
-
const todo = await ws.api.save.todos({ title: 'Buy milk' });
|
|
98
|
-
const todos = await ws.api.search.todos({});
|
|
99
|
-
await ws.api.save.todos({ id: todo.id, completed: true });
|
|
100
|
-
await ws.api.delete.todos({ id: todo.id });
|
|
101
|
-
|
|
102
|
-
// Real-time updates
|
|
103
|
-
ws.onBroadcast((method, params) => {
|
|
104
|
-
console.log('Change:', method, params);
|
|
105
|
-
});
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
## What You Get
|
|
109
|
-
|
|
110
|
-
For each entity:
|
|
111
|
-
- `get_<entity>(user_id, id)` - Get by ID
|
|
112
|
-
- `save_<entity>(user_id, data)` - Create or update
|
|
113
|
-
- `delete_<entity>(user_id, id)` - Delete
|
|
114
|
-
- `search_<entity>(user_id, filters, search, sort, page, limit)` - Search
|
|
115
|
-
|
|
116
|
-
Plus:
|
|
117
|
-
- Real-time updates via WebSocket
|
|
118
|
-
- Permission checks in SQL
|
|
119
|
-
- Audit trail in `dzql.events`
|
|
120
|
-
|
|
121
|
-
## Next Steps
|
|
122
|
-
|
|
123
|
-
- [Full Tutorial](./tutorial.md) - Complete walkthrough
|
|
124
|
-
- [Subscriptions](./subscriptions-quick-start.md) - Real-time denormalized documents
|
|
125
|
-
- [API Reference](../reference/api.md) - All operations
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
# Live Query Subscriptions - Quick Start
|
|
2
|
-
|
|
3
|
-
Get up and running with live query subscriptions in 5 minutes.
|
|
4
|
-
|
|
5
|
-
## Step 1: Create a Subscribable (2 min)
|
|
6
|
-
|
|
7
|
-
Create `my_subscribable.sql`:
|
|
8
|
-
|
|
9
|
-
```sql
|
|
10
|
-
SELECT dzql.register_subscribable(
|
|
11
|
-
'venue_detail', -- Name (use in API)
|
|
12
|
-
'{"subscribe": ["@org_id->acts_for[org_id=$]{active}.user_id"]}'::jsonb, -- Who can subscribe
|
|
13
|
-
'{"venue_id": "int"}'::jsonb, -- Subscription parameters
|
|
14
|
-
'venues', -- Root table
|
|
15
|
-
'{"org": "organisations", "sites": {"entity": "sites", "filter": "venue_id=$venue_id"}}'::jsonb -- Related data
|
|
16
|
-
);
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Step 2: Compile and Deploy (1 min)
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
# Compile to PostgreSQL functions
|
|
23
|
-
bun packages/dzql/src/compiler/cli/compile-subscribable.js my_subscribable.sql | psql $DATABASE_URL
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
This creates 3 functions:
|
|
27
|
-
- `venue_detail_can_subscribe(user_id, params)` - permission check
|
|
28
|
-
- `get_venue_detail(params, user_id)` - query builder
|
|
29
|
-
- `venue_detail_affected_documents(table, op, old, new)` - change detector
|
|
30
|
-
|
|
31
|
-
## Step 3: Subscribe from Client (2 min)
|
|
32
|
-
|
|
33
|
-
```javascript
|
|
34
|
-
import { WebSocketManager } from '@dzql/client';
|
|
35
|
-
|
|
36
|
-
const ws = new WebSocketManager('ws://localhost:3000/ws');
|
|
37
|
-
await ws.connect();
|
|
38
|
-
|
|
39
|
-
// Subscribe - get initial data + live updates
|
|
40
|
-
const { data, unsubscribe } = await ws.api.subscribe_venue_detail(
|
|
41
|
-
{ venue_id: 123 },
|
|
42
|
-
(updatedData) => {
|
|
43
|
-
console.log('Venue changed!', updatedData);
|
|
44
|
-
}
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
console.log('Initial data:', data);
|
|
48
|
-
|
|
49
|
-
// Later: cleanup
|
|
50
|
-
await unsubscribe();
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## That's It!
|
|
54
|
-
|
|
55
|
-
Your client now receives real-time updates whenever:
|
|
56
|
-
- The venue record changes
|
|
57
|
-
- Related organisation changes
|
|
58
|
-
- Related sites change
|
|
59
|
-
|
|
60
|
-
All change detection happens in PostgreSQL - zero configuration needed on the server!
|
|
61
|
-
|
|
62
|
-
## Next Steps
|
|
63
|
-
|
|
64
|
-
- [Full Documentation](../guides/subscriptions.md)
|
|
65
|
-
- [Permission Paths Guide](../../../../docs/architecture/PERMISSIONS.md)
|
|
66
|
-
- [API Reference](../reference/api.md)
|
|
67
|
-
|
|
68
|
-
## Common Patterns
|
|
69
|
-
|
|
70
|
-
### Simple Document (Single Table)
|
|
71
|
-
|
|
72
|
-
```sql
|
|
73
|
-
SELECT dzql.register_subscribable(
|
|
74
|
-
'user_settings',
|
|
75
|
-
'{"subscribe": ["@user_id"]}'::jsonb, -- Only owner
|
|
76
|
-
'{"user_id": "int"}'::jsonb,
|
|
77
|
-
'user_settings',
|
|
78
|
-
'{}'::jsonb -- No relations
|
|
79
|
-
);
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### With One Relation
|
|
83
|
-
|
|
84
|
-
```sql
|
|
85
|
-
SELECT dzql.register_subscribable(
|
|
86
|
-
'booking_summary',
|
|
87
|
-
'{"subscribe": ["@user_id"]}'::jsonb,
|
|
88
|
-
'{"booking_id": "int"}'::jsonb,
|
|
89
|
-
'bookings',
|
|
90
|
-
'{"venue": "venues"}'::jsonb -- Include venue
|
|
91
|
-
);
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### With Filtered Relations
|
|
95
|
-
|
|
96
|
-
```sql
|
|
97
|
-
SELECT dzql.register_subscribable(
|
|
98
|
-
'organisation_dashboard',
|
|
99
|
-
'{"subscribe": ["@id->acts_for[org_id=$]{active}.user_id"]}'::jsonb,
|
|
100
|
-
'{"org_id": "int"}'::jsonb,
|
|
101
|
-
'organisations',
|
|
102
|
-
'{
|
|
103
|
-
"members": {
|
|
104
|
-
"entity": "acts_for",
|
|
105
|
-
"filter": "org_id=$org_id AND valid_to IS NULL"
|
|
106
|
-
},
|
|
107
|
-
"venues": {
|
|
108
|
-
"entity": "venues",
|
|
109
|
-
"filter": "org_id=$org_id"
|
|
110
|
-
}
|
|
111
|
-
}'::jsonb
|
|
112
|
-
);
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### Multiple Permission Paths (OR logic)
|
|
116
|
-
|
|
117
|
-
```sql
|
|
118
|
-
SELECT dzql.register_subscribable(
|
|
119
|
-
'venue_admin',
|
|
120
|
-
'{
|
|
121
|
-
"subscribe": [
|
|
122
|
-
"@owner_id", -- Direct owner
|
|
123
|
-
"@org_id->acts_for[org_id=$]{active}.user_id" -- OR org member
|
|
124
|
-
]
|
|
125
|
-
}'::jsonb,
|
|
126
|
-
'{"venue_id": "int"}'::jsonb,
|
|
127
|
-
'venues',
|
|
128
|
-
'{"sites": {"entity": "sites", "filter": "venue_id=$venue_id"}}'::jsonb
|
|
129
|
-
);
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
## Debugging Tips
|
|
133
|
-
|
|
134
|
-
### Test the functions manually:
|
|
135
|
-
|
|
136
|
-
```sql
|
|
137
|
-
-- Check permission
|
|
138
|
-
SELECT venue_detail_can_subscribe(1, '{"venue_id": 123}'::jsonb);
|
|
139
|
-
|
|
140
|
-
-- Get data
|
|
141
|
-
SELECT get_venue_detail('{"venue_id": 123}'::jsonb, 1);
|
|
142
|
-
|
|
143
|
-
-- Test change detection
|
|
144
|
-
SELECT venue_detail_affected_documents(
|
|
145
|
-
'venues',
|
|
146
|
-
'update',
|
|
147
|
-
'{"id": 123}'::jsonb,
|
|
148
|
-
'{"id": 123, "name": "New"}'::jsonb
|
|
149
|
-
);
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
### Check active subscriptions:
|
|
153
|
-
|
|
154
|
-
```javascript
|
|
155
|
-
// Client-side
|
|
156
|
-
console.log('My subscriptions:', ws.subscriptions.size);
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
## FAQ
|
|
160
|
-
|
|
161
|
-
**Q: When should I use subscriptions vs. simple queries?**
|
|
162
|
-
A: Use subscriptions when data changes frequently and client needs to stay in sync. Use simple queries for one-time lookups.
|
|
163
|
-
|
|
164
|
-
**Q: What happens when client disconnects?**
|
|
165
|
-
A: Server automatically cleans up all subscriptions for that connection.
|
|
166
|
-
|
|
167
|
-
**Q: Can multiple clients subscribe to the same data?**
|
|
168
|
-
A: Yes! Each subscription is independent. All will receive updates.
|
|
169
|
-
|
|
170
|
-
**Q: How do I update the subscribable definition?**
|
|
171
|
-
A: Re-compile and deploy. The `register_subscribable()` call uses `ON CONFLICT UPDATE`, so it's safe to run repeatedly.
|
|
172
|
-
|
|
173
|
-
**Q: What if the underlying data is deleted?**
|
|
174
|
-
A: The `get_<name>()` function returns `null`. Handle this in your callback:
|
|
175
|
-
```javascript
|
|
176
|
-
(data) => {
|
|
177
|
-
if (!data) {
|
|
178
|
-
console.log('Record was deleted');
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
updateUI(data);
|
|
182
|
-
}
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
**Q: How do I subscribe to a list of items?**
|
|
186
|
-
A: Create a subscribable with array parameters or use multiple subscriptions. For dashboard-style views, consider a single subscribable that returns an array.
|
|
187
|
-
|
|
188
|
-
## Performance Tips
|
|
189
|
-
|
|
190
|
-
1. **Index your joins**: Make sure foreign keys are indexed
|
|
191
|
-
2. **Keep _affected_documents() simple**: Early return for unrelated tables
|
|
192
|
-
3. **Limit relation depth**: Avoid deeply nested relations (max 2-3 levels)
|
|
193
|
-
4. **Use specific subscription keys**: `venue_id` is better than `org_id` (fewer false positives)
|
|
194
|
-
5. **Unsubscribe when done**: Always cleanup to free server resources
|
|
195
|
-
|
|
196
|
-
## Architecture Benefits
|
|
197
|
-
|
|
198
|
-
- ✅ **PostgreSQL-First**: All logic in database, not application code
|
|
199
|
-
- ✅ **Zero Configuration**: No server changes needed for new subscribables
|
|
200
|
-
- ✅ **Type Safe**: Compiled functions validated at deploy time
|
|
201
|
-
- ✅ **Efficient**: In-memory registry, PostgreSQL does matching
|
|
202
|
-
- ✅ **Secure**: Permission paths enforced at database level
|
|
203
|
-
- ✅ **Scalable**: Stateless server, can add instances freely
|