create-lego-one 2.0.12 → 2.0.14
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/dist/index.cjs +150 -15
- package/dist/index.cjs.map +1 -1
- package/package.json +1 -1
- package/template/.cursor/rules/rules.mdc +639 -0
- package/template/.dockerignore +58 -0
- package/template/.env.example +18 -0
- package/template/.eslintignore +5 -0
- package/template/.eslintrc.js +28 -0
- package/template/.prettierignore +6 -0
- package/template/.prettierrc +11 -0
- package/template/CLAUDE.md +634 -0
- package/template/Dockerfile +67 -0
- package/template/PROMPT.md +457 -0
- package/template/README.md +325 -0
- package/template/docker-compose.yml +48 -0
- package/template/docker-entrypoint.sh +23 -0
- package/template/docs/checkpoints/.template.md +64 -0
- package/template/docs/checkpoints/framework/01-infrastructure-setup.md +132 -0
- package/template/docs/checkpoints/framework/02-pocketbase-setup.md +155 -0
- package/template/docs/checkpoints/framework/03-host-kernel.md +170 -0
- package/template/docs/checkpoints/framework/04-auth-system.md +163 -0
- package/template/docs/checkpoints/framework/phase-05-multitenancy-rbac.md +223 -0
- package/template/docs/checkpoints/framework/phase-06-ui-components.md +260 -0
- package/template/docs/checkpoints/framework/phase-07-communication-system.md +276 -0
- package/template/docs/checkpoints/framework/phase-08-plugin-system.md +91 -0
- package/template/docs/checkpoints/framework/phase-09-dashboard-plugin.md +111 -0
- package/template/docs/checkpoints/framework/phase-10-todo-plugin.md +169 -0
- package/template/docs/checkpoints/framework/phase-11-testing.md +264 -0
- package/template/docs/checkpoints/framework/phase-12-deployment.md +294 -0
- package/template/docs/checkpoints/framework/phase-13-documentation.md +312 -0
- package/template/docs/framework/plans/00-index.md +164 -0
- package/template/docs/framework/plans/01-infrastructure-setup.md +855 -0
- package/template/docs/framework/plans/02-pocketbase-setup.md +1374 -0
- package/template/docs/framework/plans/03-host-kernel.md +1518 -0
- package/template/docs/framework/plans/04-auth-system.md +1466 -0
- package/template/docs/framework/plans/05-multitenancy-rbac.md +1527 -0
- package/template/docs/framework/plans/06-ui-components.md +1478 -0
- package/template/docs/framework/plans/07-communication-system.md +1106 -0
- package/template/docs/framework/plans/08-plugin-system.md +1179 -0
- package/template/docs/framework/plans/09-dashboard-plugin.md +1137 -0
- package/template/docs/framework/plans/10-todo-plugin.md +1343 -0
- package/template/docs/framework/plans/11-testing.md +935 -0
- package/template/docs/framework/plans/12-deployment.md +896 -0
- package/template/docs/framework/prompts/0-boilerplate-modernjs.md +151 -0
- package/template/docs/framework/research/00-modernjs-audit.md +488 -0
- package/template/docs/framework/research/01-system-blueprint.md +721 -0
- package/template/docs/framework/research/02-data-migration-protocol.md +699 -0
- package/template/docs/framework/research/03-host-setup.md +714 -0
- package/template/docs/framework/research/04-plugin-architecture.md +645 -0
- package/template/docs/framework/research/05-slot-injection-pattern.md +671 -0
- package/template/docs/framework/research/06-cli-strategy.md +615 -0
- package/template/docs/framework/research/07-deployment.md +629 -0
- package/template/docs/framework/research/README.md +282 -0
- package/template/docs/framework/setup/00-index.md +210 -0
- package/template/docs/framework/setup/01-framework-structure.md +308 -0
- package/template/docs/framework/setup/02-development-workflow.md +405 -0
- package/template/docs/framework/setup/03-environment-setup.md +215 -0
- package/template/docs/framework/setup/04-kernel-architecture.md +499 -0
- package/template/docs/framework/setup/05-plugin-system.md +620 -0
- package/template/docs/framework/setup/06-communication-patterns.md +451 -0
- package/template/docs/framework/setup/07-plugin-development.md +582 -0
- package/template/docs/framework/setup/08-component-library.md +658 -0
- package/template/docs/framework/setup/09-data-integration.md +609 -0
- package/template/docs/framework/setup/10-auth-rbac.md +497 -0
- package/template/docs/framework/setup/11-hooks-api.md +393 -0
- package/template/docs/framework/setup/12-components-api.md +665 -0
- package/template/docs/framework/setup/13-deployment-guide.md +566 -0
- package/template/docs/framework/setup/README.md +548 -0
- package/template/host/package.json +1 -1
- package/template/nginx.conf +72 -0
- package/template/package.json +1 -1
- package/template/packages/plugins/@lego/plugin-dashboard/package.json +1 -1
- package/template/packages/plugins/@lego/plugin-todo/package.json +1 -1
- package/template/pocketbase/CHANGELOG.md +911 -0
- package/template/pocketbase/LICENSE.md +17 -0
- package/template/scripts/create-plugin.js +221 -0
- package/template/scripts/deploy.sh +56 -0
- package/template/tsconfig.base.json +26 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
# Data Integration
|
|
2
|
+
|
|
3
|
+
**PocketBase, Multi-Tenancy, and Data Fetching Patterns**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Lego-One uses **PocketBase** as the backend, providing an embedded database with auto-generated APIs. All data is scoped to organizations for multi-tenancy, and accessed through TanStack Query for optimal caching.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## PocketBase Setup
|
|
14
|
+
|
|
15
|
+
### Collections
|
|
16
|
+
|
|
17
|
+
**Standard Collections:**
|
|
18
|
+
|
|
19
|
+
| Collection | Purpose | Multi-Tenancy |
|
|
20
|
+
|-----------|---------|---------------|
|
|
21
|
+
| `users` | User accounts | Via memberships |
|
|
22
|
+
| `organizations` | Tenant organizations | N/A |
|
|
23
|
+
| `roles` | System and custom roles | Per org |
|
|
24
|
+
| `permissions` | Permission definitions | N/A |
|
|
25
|
+
| `user_roles` | User-role assignments | Required |
|
|
26
|
+
| `audit_logs` | Activity tracking | Required |
|
|
27
|
+
|
|
28
|
+
**Plugin Collections:**
|
|
29
|
+
Plugins create their own collections, e.g.:
|
|
30
|
+
- `todos` - Todo plugin
|
|
31
|
+
- `dashboard_widgets` - Dashboard plugin
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
### Collection Schema Pattern
|
|
36
|
+
|
|
37
|
+
All plugin collections MUST follow this pattern:
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
{
|
|
41
|
+
name: 'my_plugin_items',
|
|
42
|
+
type: 'base',
|
|
43
|
+
|
|
44
|
+
// Multi-tenancy required fields
|
|
45
|
+
fields: [
|
|
46
|
+
{
|
|
47
|
+
name: 'organizationId',
|
|
48
|
+
type: 'relation',
|
|
49
|
+
required: true,
|
|
50
|
+
options: {
|
|
51
|
+
collectionId: 'organizations',
|
|
52
|
+
displayFields: ['name']
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'ownerId',
|
|
57
|
+
type: 'relation',
|
|
58
|
+
required: true,
|
|
59
|
+
options: {
|
|
60
|
+
collectionId: 'users',
|
|
61
|
+
displayFields: ['name', 'email']
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Your plugin-specific fields
|
|
66
|
+
{ name: 'title', type: 'text', required: true },
|
|
67
|
+
{ name: 'description', type: 'text' },
|
|
68
|
+
{ name: 'status', type: 'select', options: { values: ['active', 'inactive'] } },
|
|
69
|
+
{ name: 'priority', type: 'select', options: { values: ['low', 'medium', 'high'] } },
|
|
70
|
+
],
|
|
71
|
+
|
|
72
|
+
// Multi-tenancy API rules
|
|
73
|
+
listRule: '@request.auth.id != "" && organizationId = @request.auth.membership.organizations',
|
|
74
|
+
viewRule: '@request.auth.id != "" && organizationId = @request.auth.membership.organizations',
|
|
75
|
+
createRule: '@request.auth.id != "" && organizationId = @request.auth.membership.organizations',
|
|
76
|
+
updateRule: '@request.auth.id != "" && organizationId = @request.auth.membership.organizations && ownerId = @request.auth.id',
|
|
77
|
+
deleteRule: '@request.auth.id != "" && organizationId = @request.auth.membership.organizations && ownerId = @request.auth.id',
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## PocketBase Client
|
|
84
|
+
|
|
85
|
+
### Accessing PocketBase
|
|
86
|
+
|
|
87
|
+
**In Host Components:**
|
|
88
|
+
```typescript
|
|
89
|
+
import { usePocketBase } from '@lego/kernel/providers';
|
|
90
|
+
|
|
91
|
+
function MyComponent() {
|
|
92
|
+
const pb = usePocketBase();
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!pb) return;
|
|
96
|
+
|
|
97
|
+
async function fetchData() {
|
|
98
|
+
const result = await pb.collection('todos').getList(1, 20);
|
|
99
|
+
console.log(result.items);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fetchData();
|
|
103
|
+
}, [pb]);
|
|
104
|
+
|
|
105
|
+
return <div>...</div>;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**In Plugins:**
|
|
110
|
+
```typescript
|
|
111
|
+
// Create usePocketBase hook for plugin
|
|
112
|
+
import { useEffect, useState } from 'react';
|
|
113
|
+
|
|
114
|
+
export function usePocketBase() {
|
|
115
|
+
const [pb, setPb] = useState(null);
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
// Get kernel state from window bridge
|
|
119
|
+
const kernelState = window.__LEGO_KERNEL_STATE__;
|
|
120
|
+
|
|
121
|
+
// Get PocketBase URL from environment
|
|
122
|
+
const pbUrl = import.meta.env.VITE_POCKETBASE_URL || 'http://127.0.0.1:8090';
|
|
123
|
+
|
|
124
|
+
// Create PocketBase client
|
|
125
|
+
const PocketBase = require('pocketbase').default;
|
|
126
|
+
const client = new PocketBase(pbUrl);
|
|
127
|
+
|
|
128
|
+
// Restore auth token from kernel state
|
|
129
|
+
const state = kernelState?.useGlobalKernelState?.getState();
|
|
130
|
+
if (state?.token) {
|
|
131
|
+
client.authStore.save(state.token);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
setPb(client);
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
return pb;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## TanStack Query Integration
|
|
144
|
+
|
|
145
|
+
### Creating Data Hooks
|
|
146
|
+
|
|
147
|
+
**Use TanStack Query for all server state:**
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { useQuery } from '@tanstack/react-query';
|
|
151
|
+
import { usePocketBase } from './usePocketBase';
|
|
152
|
+
|
|
153
|
+
export function useTodos() {
|
|
154
|
+
const pb = usePocketBase();
|
|
155
|
+
|
|
156
|
+
return useQuery({
|
|
157
|
+
queryKey: ['todos'],
|
|
158
|
+
queryFn: async () => {
|
|
159
|
+
if (!pb) throw new Error('PB not initialized');
|
|
160
|
+
|
|
161
|
+
const kernelState = window.__LEGO_KERNEL_STATE__;
|
|
162
|
+
const state = kernelState?.useGlobalKernelState?.getState();
|
|
163
|
+
const orgId = state?.organization?.id;
|
|
164
|
+
|
|
165
|
+
const result = await pb.collection('todos').getList(1, 50, {
|
|
166
|
+
filter: `organizationId = "${orgId}"`,
|
|
167
|
+
sort: '-created',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return result.items;
|
|
171
|
+
},
|
|
172
|
+
enabled: !!pb && !!orgId,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Creating Mutation Hooks
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
181
|
+
import { usePocketBase } from './usePocketBase';
|
|
182
|
+
import { useToastChannel } from './useToastChannel';
|
|
183
|
+
|
|
184
|
+
export function useCreateTodo() {
|
|
185
|
+
const pb = usePocketBase();
|
|
186
|
+
const queryClient = useQueryClient();
|
|
187
|
+
const showToast = useToastChannel();
|
|
188
|
+
|
|
189
|
+
return useMutation({
|
|
190
|
+
mutationFn: async (data: { title: string; description?: string }) => {
|
|
191
|
+
if (!pb) throw new Error('PB not initialized');
|
|
192
|
+
|
|
193
|
+
const kernelState = window.__LEGO_KERNEL_STATE__;
|
|
194
|
+
const state = kernelState?.useGlobalKernelState?.getState();
|
|
195
|
+
|
|
196
|
+
return await pb.collection('todos').create({
|
|
197
|
+
...data,
|
|
198
|
+
organizationId: state?.organization?.id,
|
|
199
|
+
ownerId: state?.user?.id,
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
onSuccess: () => {
|
|
203
|
+
// Invalidate and refetch
|
|
204
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
|
205
|
+
|
|
206
|
+
// Show success toast
|
|
207
|
+
showToast({
|
|
208
|
+
type: 'success',
|
|
209
|
+
title: 'Todo Created',
|
|
210
|
+
description: 'Your todo was created successfully',
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
onError: (error) => {
|
|
214
|
+
showToast({
|
|
215
|
+
type: 'error',
|
|
216
|
+
title: 'Error',
|
|
217
|
+
description: error.message,
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Multi-Tenancy Patterns
|
|
227
|
+
|
|
228
|
+
### Data Isolation
|
|
229
|
+
|
|
230
|
+
**CRITICAL:** All data queries MUST be scoped to organization:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
// ✅ CORRECT - Filter by organization
|
|
234
|
+
const result = await pb.collection('items').getList(1, 50, {
|
|
235
|
+
filter: `organizationId = "${orgId}"`,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ❌ WRONG - No organization filter
|
|
239
|
+
const result = await pb.collection('items').getList(1, 50);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Getting Current Organization
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { useEffect, useState } from 'react';
|
|
246
|
+
|
|
247
|
+
export function useCurrentOrganization() {
|
|
248
|
+
const [org, setOrg] = useState(null);
|
|
249
|
+
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
// Get from kernel state
|
|
252
|
+
const kernelState = window.__LEGO_KERNEL_STATE__;
|
|
253
|
+
const state = kernelState?.useGlobalKernelState?.getState();
|
|
254
|
+
|
|
255
|
+
setOrg(state?.organization || null);
|
|
256
|
+
|
|
257
|
+
// Subscribe to organization changes
|
|
258
|
+
const channelBus = window.__LEGO_CHANNEL_BUS__;
|
|
259
|
+
if (!channelBus) return;
|
|
260
|
+
|
|
261
|
+
const unsubscribe = channelBus.subscribe('lego:organization:change', () => {
|
|
262
|
+
const newState = kernelState?.useGlobalKernelState?.getState();
|
|
263
|
+
setOrg(newState?.organization || null);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return unsubscribe;
|
|
267
|
+
}, []);
|
|
268
|
+
|
|
269
|
+
return org;
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## CRUD Patterns
|
|
276
|
+
|
|
277
|
+
### Create
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
export function useCreateItem() {
|
|
281
|
+
const pb = usePocketBase();
|
|
282
|
+
const queryClient = useQueryClient();
|
|
283
|
+
|
|
284
|
+
return useMutation({
|
|
285
|
+
mutationFn: async (data: CreateItemData) => {
|
|
286
|
+
if (!pb) throw new Error('PB not initialized');
|
|
287
|
+
|
|
288
|
+
const state = window.__LEGO_KERNEL_STATE__?.useGlobalKernelState?.getState();
|
|
289
|
+
|
|
290
|
+
return await pb.collection('items').create({
|
|
291
|
+
...data,
|
|
292
|
+
organizationId: state?.organization?.id,
|
|
293
|
+
ownerId: state?.user?.id,
|
|
294
|
+
});
|
|
295
|
+
},
|
|
296
|
+
onSuccess: () => {
|
|
297
|
+
queryClient.invalidateQueries({ queryKey: ['items'] });
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Read
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
export function useItems() {
|
|
307
|
+
const pb = usePocketBase();
|
|
308
|
+
|
|
309
|
+
return useQuery({
|
|
310
|
+
queryKey: ['items'],
|
|
311
|
+
queryFn: async () => {
|
|
312
|
+
if (!pb) throw new Error('PB not initialized');
|
|
313
|
+
|
|
314
|
+
const state = window.__LEGO_KERNEL_STATE__?.useGlobalKernelState?.getState();
|
|
315
|
+
const orgId = state?.organization?.id;
|
|
316
|
+
|
|
317
|
+
const result = await pb.collection('items').getList(1, 50, {
|
|
318
|
+
filter: `organizationId = "${orgId}"`,
|
|
319
|
+
sort: '-created',
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return result.items;
|
|
323
|
+
},
|
|
324
|
+
enabled: !!pb && !!orgId,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Update
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
export function useUpdateItem() {
|
|
333
|
+
const pb = usePocketBase();
|
|
334
|
+
const queryClient = useQueryClient();
|
|
335
|
+
|
|
336
|
+
return useMutation({
|
|
337
|
+
mutationFn: async ({ id, data }: { id: string; data: UpdateItemData }) => {
|
|
338
|
+
if (!pb) throw new Error('PB not initialized');
|
|
339
|
+
|
|
340
|
+
return await pb.collection('items').update(id, data);
|
|
341
|
+
},
|
|
342
|
+
onSuccess: (_, { id }) => {
|
|
343
|
+
queryClient.invalidateQueries({ queryKey: ['items'] });
|
|
344
|
+
queryClient.invalidateQueries({ queryKey: ['item', id] });
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Delete
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
export function useDeleteItem() {
|
|
354
|
+
const pb = usePocketBase();
|
|
355
|
+
const queryClient = useQueryClient();
|
|
356
|
+
|
|
357
|
+
return useMutation({
|
|
358
|
+
mutationFn: async (id: string) => {
|
|
359
|
+
if (!pb) throw new Error('PB not initialized');
|
|
360
|
+
|
|
361
|
+
return await pb.collection('items').delete(id);
|
|
362
|
+
},
|
|
363
|
+
onSuccess: () => {
|
|
364
|
+
queryClient.invalidateQueries({ queryKey: ['items'] });
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Advanced Queries
|
|
373
|
+
|
|
374
|
+
### Filtering
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
export function useTodos(filter: 'all' | 'active' | 'completed') {
|
|
378
|
+
const pb = usePocketBase();
|
|
379
|
+
|
|
380
|
+
return useQuery({
|
|
381
|
+
queryKey: ['todos', filter],
|
|
382
|
+
queryFn: async () => {
|
|
383
|
+
if (!pb) throw new Error('PB not initialized');
|
|
384
|
+
|
|
385
|
+
const state = window.__LEGO_KERNEL_STATE__?.useGlobalKernelState?.getState();
|
|
386
|
+
const orgId = state?.organization?.id;
|
|
387
|
+
|
|
388
|
+
let filterStr = `organizationId = "${orgId}"`;
|
|
389
|
+
|
|
390
|
+
if (filter === 'active') {
|
|
391
|
+
filterStr += ' && status = "active"';
|
|
392
|
+
} else if (filter === 'completed') {
|
|
393
|
+
filterStr += ' && status = "completed"';
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const result = await pb.collection('todos').getList(1, 50, {
|
|
397
|
+
filter: filterStr,
|
|
398
|
+
sort: '-created',
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return result.items;
|
|
402
|
+
},
|
|
403
|
+
enabled: !!pb && !!orgId,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Searching
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
export function useSearchTodos(search: string) {
|
|
412
|
+
const pb = usePocketBase();
|
|
413
|
+
|
|
414
|
+
return useQuery({
|
|
415
|
+
queryKey: ['todos', 'search', search],
|
|
416
|
+
queryFn: async () => {
|
|
417
|
+
if (!pb || !search) return [];
|
|
418
|
+
|
|
419
|
+
const state = window.__LEGO_KERNEL_STATE__?.useGlobalKernelState?.getState();
|
|
420
|
+
const orgId = state?.organization?.id;
|
|
421
|
+
|
|
422
|
+
const result = await pb.collection('todos').getList(1, 50, {
|
|
423
|
+
filter: `organizationId = "${orgId}" && title ~ "${search}"`,
|
|
424
|
+
sort: '-created',
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
return result.items;
|
|
428
|
+
},
|
|
429
|
+
enabled: !!pb && !!orgId && search.length > 0,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Pagination
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
export function useTodosPaginated(page: number, perPage: number = 20) {
|
|
438
|
+
const pb = usePocketBase();
|
|
439
|
+
|
|
440
|
+
return useQuery({
|
|
441
|
+
queryKey: ['todos', 'paginated', page, perPage],
|
|
442
|
+
queryFn: async () => {
|
|
443
|
+
if (!pb) throw new Error('PB not initialized');
|
|
444
|
+
|
|
445
|
+
const state = window.__LEGO_KERNEL_STATE__?.useGlobalKernelState?.getState();
|
|
446
|
+
const orgId = state?.organization?.id;
|
|
447
|
+
|
|
448
|
+
const result = await pb.collection('todos').getList(page, perPage, {
|
|
449
|
+
filter: `organizationId = "${orgId}"`,
|
|
450
|
+
sort: '-created',
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
items: result.items,
|
|
455
|
+
totalPages: result.totalPages,
|
|
456
|
+
totalItems: result.totalItems,
|
|
457
|
+
};
|
|
458
|
+
},
|
|
459
|
+
enabled: !!pb && !!orgId,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Real-time Subscriptions
|
|
467
|
+
|
|
468
|
+
PocketBase supports real-time subscriptions:
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
useEffect(() => {
|
|
472
|
+
const pb = usePocketBase();
|
|
473
|
+
if (!pb) return;
|
|
474
|
+
|
|
475
|
+
// Subscribe to todo changes
|
|
476
|
+
const unsubscribe = pb.collection('todos').subscribe('*', (e) => {
|
|
477
|
+
console.log('Todo changed:', e);
|
|
478
|
+
|
|
479
|
+
// Refresh data on changes
|
|
480
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
return () => {
|
|
484
|
+
unsubscribe(); // Cleanup
|
|
485
|
+
};
|
|
486
|
+
}, [pb]);
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## Validation with Zod
|
|
492
|
+
|
|
493
|
+
### Define Schema
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
import { z } from 'zod';
|
|
497
|
+
|
|
498
|
+
export const todoFormSchema = z.object({
|
|
499
|
+
title: z.string().min(1, 'Title is required').max(200, 'Title too long'),
|
|
500
|
+
description: z.string().max(1000).optional(),
|
|
501
|
+
priority: z.enum(['low', 'medium', 'high']).default('medium'),
|
|
502
|
+
dueDate: z.string().optional(),
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
export type TodoFormData = z.infer<typeof todoFormSchema>;
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Use with Form Library
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
import { useForm } from 'react-hook-form';
|
|
512
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
513
|
+
import { todoFormSchema } from './schemas';
|
|
514
|
+
|
|
515
|
+
function TodoForm() {
|
|
516
|
+
const { register, handleSubmit, formState: { errors } } = useForm({
|
|
517
|
+
resolver: zodResolver(todoFormSchema),
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const { mutate: createTodo } = useCreateTodo();
|
|
521
|
+
|
|
522
|
+
const onSubmit = (data: TodoFormData) => {
|
|
523
|
+
createTodo(data);
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
528
|
+
<input {...register('title')} />
|
|
529
|
+
{errors.title && <span>{errors.title.message}</span>}
|
|
530
|
+
|
|
531
|
+
<textarea {...register('description')} />
|
|
532
|
+
|
|
533
|
+
<select {...register('priority')}>
|
|
534
|
+
<option value="low">Low</option>
|
|
535
|
+
<option value="medium">Medium</option>
|
|
536
|
+
<option value="high">High</option>
|
|
537
|
+
</select>
|
|
538
|
+
|
|
539
|
+
<button type="submit">Create</button>
|
|
540
|
+
</form>
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
## Error Handling
|
|
548
|
+
|
|
549
|
+
### Handling PocketBase Errors
|
|
550
|
+
|
|
551
|
+
```typescript
|
|
552
|
+
try {
|
|
553
|
+
const result = await pb.collection('todos').create(data);
|
|
554
|
+
} catch (error) {
|
|
555
|
+
// PocketBase errors
|
|
556
|
+
if (error?.data) {
|
|
557
|
+
// Validation error
|
|
558
|
+
console.error('Validation failed:', error.data);
|
|
559
|
+
} else if (error?.status === 404) {
|
|
560
|
+
console.error('Collection not found');
|
|
561
|
+
} else if (error?.status === 403) {
|
|
562
|
+
console.error('Permission denied');
|
|
563
|
+
} else {
|
|
564
|
+
console.error('Unknown error:', error);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### User-Friendly Error Messages
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
function getErrorMessage(error: any): string {
|
|
573
|
+
if (error?.data?.title) {
|
|
574
|
+
return error.data.title; // PocketBase validation error
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (error?.message) {
|
|
578
|
+
return error.message;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return 'An unexpected error occurred';
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## Best Practices
|
|
588
|
+
|
|
589
|
+
### DO ✅
|
|
590
|
+
|
|
591
|
+
1. **Scope all queries to organization** - Always filter by `organizationId`
|
|
592
|
+
2. **Use TanStack Query** - Automatic caching, refetching, loading states
|
|
593
|
+
3. **Show loading states** - Use Skeleton, Spinners during fetch
|
|
594
|
+
4. **Handle errors gracefully** - Show user-friendly error messages
|
|
595
|
+
5. **Invalidate queries** - After mutations, invalidate related queries
|
|
596
|
+
6. **Use toast notifications** - For user feedback on actions
|
|
597
|
+
7. **Validate with Zod** - Type-safe validation
|
|
598
|
+
|
|
599
|
+
### DON'T ❌
|
|
600
|
+
|
|
601
|
+
1. **Don't skip organization filtering** - Data isolation is critical
|
|
602
|
+
2. **Don't fetch directly in components** - Use custom hooks
|
|
603
|
+
3. **Don't ignore loading/error states** - Always handle these
|
|
604
|
+
4. **Don't forget cleanup** - Unsubscribe from events
|
|
605
|
+
5. **Don't hardcode org IDs** - Always get from kernel state
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
**Next:** Read [`10-auth-rbac.md`](./10-auth-rbac.md) for authentication and authorization patterns.
|