@docyrus/docyrus 0.0.30 → 0.0.32
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/agent-loader.js +36 -25
- package/agent-loader.js.map +4 -4
- package/main.js +4 -4
- package/main.js.map +1 -1
- package/package.json +3 -3
- package/resources/pi-agent/extensions/docyrus-web-browser.ts +31 -0
- package/resources/pi-agent/shared/docyrusWebBrowserProtocol.ts +169 -0
- package/resources/pi-agent/skills/diffity-diff/SKILL.md +1 -1
- package/resources/pi-agent/skills/diffity-resolve/SKILL.md +4 -4
- package/resources/pi-agent/skills/diffity-review/SKILL.md +5 -4
- package/resources/pi-agent/skills/docyrus-api-dev/SKILL.md +197 -0
- package/resources/pi-agent/skills/docyrus-api-dev/references/acl-endpoints-frontend.md +295 -0
- package/resources/pi-agent/skills/docyrus-api-dev/references/api-client.md +349 -0
- package/resources/pi-agent/skills/docyrus-api-dev/references/authentication.md +298 -0
- package/resources/pi-agent/skills/docyrus-api-dev/references/data-source-query-guide.md +2063 -0
- package/resources/pi-agent/skills/docyrus-api-dev/references/formula-design-guide-llm.md +312 -0
- package/resources/pi-agent/skills/docyrus-api-dev/references/query-and-formulas.md +592 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/SKILL.md +361 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/references/README.md +29 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/references/api-client-and-auth.md +326 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/references/collections-and-patterns.md +353 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/references/component-selection-guide.md +619 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/references/icon-usage-guide.md +463 -0
- package/resources/pi-agent/skills/docyrus-app-dev-react/references/preferred-components-catalog.md +242 -0
- package/resources/pi-agent/skills/docyrus-platform/SKILL.md +2 -2
- package/resources/pi-agent/skills/docyrus-platform/references/auth-and-multi-tenancy.md +9 -1
- package/resources/pi-agent/skills/docyrus-platform/references/developer-tools.md +3 -2
- package/server-loader.js +328 -87
- package/server-loader.js.map +4 -4
- package/resources/pi-agent/extensions/multi-edit.ts +0 -835
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# @docyrus/api-client & @docyrus/signin Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
1. [RestApiClient](#restapiclient)
|
|
6
|
+
2. [Authentication with @docyrus/signin](#authentication-with-docyrussignin)
|
|
7
|
+
3. [Authorization (Roles & Permissions)](#authorization-roles--permissions)
|
|
8
|
+
4. [API Client Access Pattern](#api-client-access-pattern)
|
|
9
|
+
5. [Interceptors](#interceptors)
|
|
10
|
+
6. [Error Handling](#error-handling)
|
|
11
|
+
7. [Advanced Features](#advanced-features)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## RestApiClient
|
|
16
|
+
|
|
17
|
+
Type-safe REST API client from `@docyrus/api-client`.
|
|
18
|
+
|
|
19
|
+
### HTTP Methods
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { RestApiClient } from '@docyrus/api-client'
|
|
23
|
+
|
|
24
|
+
client.get<T>(endpoint, params?) // GET
|
|
25
|
+
client.post<T>(endpoint, data) // POST
|
|
26
|
+
client.patch<T>(endpoint, data) // PATCH
|
|
27
|
+
client.put(endpoint, data) // PUT
|
|
28
|
+
client.delete(endpoint, data?) // DELETE
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Typed Responses
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
interface User { id: string; name: string; email: string }
|
|
35
|
+
const response = await client.get<User[]>('/v1/users')
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Config Options
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
interface ApiClientConfig {
|
|
42
|
+
baseURL?: string
|
|
43
|
+
tokenManager?: TokenManager
|
|
44
|
+
headers?: Record<string, string>
|
|
45
|
+
timeout?: number
|
|
46
|
+
fetch?: typeof fetch
|
|
47
|
+
FormData?: typeof FormData
|
|
48
|
+
AbortController?: typeof AbortController
|
|
49
|
+
storage?: Storage
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Authentication with @docyrus/signin
|
|
56
|
+
|
|
57
|
+
Package: `@docyrus/signin` (peer dep: `@docyrus/api-client >= 0.0.10`, `react >= 18`)
|
|
58
|
+
|
|
59
|
+
### DocyrusAuthProvider Setup
|
|
60
|
+
|
|
61
|
+
Wrap application root:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { DocyrusAuthProvider } from '@docyrus/signin'
|
|
65
|
+
|
|
66
|
+
<DocyrusAuthProvider
|
|
67
|
+
apiUrl={import.meta.env.VITE_API_BASE_URL}
|
|
68
|
+
clientId={import.meta.env.VITE_OAUTH2_CLIENT_ID}
|
|
69
|
+
redirectUri={import.meta.env.VITE_OAUTH2_REDIRECT_URI}
|
|
70
|
+
scopes={['offline_access', 'Read.All', 'DS.ReadWrite.All', 'Users.Read']}
|
|
71
|
+
callbackPath="/auth/callback"
|
|
72
|
+
>
|
|
73
|
+
<App />
|
|
74
|
+
</DocyrusAuthProvider>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Provider Props
|
|
78
|
+
|
|
79
|
+
| Prop | Type | Default | Description |
|
|
80
|
+
|------|------|---------|-------------|
|
|
81
|
+
| `apiUrl` | `string` | `https://alpha-api.docyrus.com` | API base URL |
|
|
82
|
+
| `clientId` | `string` | Built-in default | OAuth2 client ID |
|
|
83
|
+
| `redirectUri` | `string` | `origin + callbackPath` | OAuth2 redirect URI |
|
|
84
|
+
| `scopes` | `string[]` | `['offline_access', 'Read.All', ...]` | OAuth2 scopes |
|
|
85
|
+
| `callbackPath` | `string` | `/auth/callback` | OAuth callback route |
|
|
86
|
+
| `forceMode` | `'standalone' \| 'iframe'` | Auto-detected | Force auth mode |
|
|
87
|
+
| `storageKeyPrefix` | `string` | `docyrus_oauth2_` | localStorage prefix |
|
|
88
|
+
| `allowedHostOrigins` | `string[]` | `undefined` | Extra trusted iframe origins |
|
|
89
|
+
|
|
90
|
+
### Auth Modes
|
|
91
|
+
|
|
92
|
+
- **Standalone**: OAuth2 Authorization Code + PKCE via page redirect. Tokens stored in localStorage, auto-refreshed.
|
|
93
|
+
- **Iframe**: Receives tokens via `window.postMessage` from `*.docyrus.app` hosts. Requests refresh from host when expired.
|
|
94
|
+
|
|
95
|
+
### useDocyrusAuth() Hook
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
const {
|
|
99
|
+
status, // 'loading' | 'authenticated' | 'unauthenticated'
|
|
100
|
+
mode, // 'standalone' | 'iframe'
|
|
101
|
+
client, // RestApiClient | null
|
|
102
|
+
tokens, // { accessToken, refreshToken, ... } | null
|
|
103
|
+
user, // DocyrusUser | null — auto-fetched from /v1/users/me
|
|
104
|
+
signIn, // () => void — redirects to Docyrus login
|
|
105
|
+
signOut, // () => void — logout and clear tokens
|
|
106
|
+
hasRole, // (role: string | string[]) => boolean — check role by slug or uid
|
|
107
|
+
hasPermission, // (operation: string, dataSourceId?: string) => boolean — check ACL permission
|
|
108
|
+
refreshUser, // () => Promise<void> — re-fetch user from API
|
|
109
|
+
error, // Error | null
|
|
110
|
+
} = useDocyrusAuth()
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### useDocyrusClient() Hook
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const client = useDocyrusClient() // RestApiClient | null
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### SignInButton Component
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
// Basic
|
|
123
|
+
<SignInButton />
|
|
124
|
+
|
|
125
|
+
// Styled
|
|
126
|
+
<SignInButton className="btn" label="Log in with Docyrus" />
|
|
127
|
+
|
|
128
|
+
// Render prop
|
|
129
|
+
<SignInButton>
|
|
130
|
+
{({ signIn, isLoading }) => (
|
|
131
|
+
<button onClick={signIn} disabled={isLoading}>
|
|
132
|
+
{isLoading ? 'Redirecting...' : 'Sign in'}
|
|
133
|
+
</button>
|
|
134
|
+
)}
|
|
135
|
+
</SignInButton>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Environment Variables (.env)
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
VITE_API_BASE_URL=https://localhost:3366
|
|
142
|
+
VITE_OAUTH2_CLIENT_ID=your-client-id
|
|
143
|
+
VITE_OAUTH2_REDIRECT_URI=http://localhost:3000/auth/callback
|
|
144
|
+
VITE_OAUTH2_SCOPES=openid profile offline_access Users.Read DS.ReadWrite.All
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Authorization (Roles & Permissions)
|
|
150
|
+
|
|
151
|
+
The provider auto-fetches the current user from `/v1/users/me` after authentication. The `user`, `hasRole`, and `hasPermission` are available on the `useDocyrusAuth()` hook.
|
|
152
|
+
|
|
153
|
+
### Role-Based UI Gating
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
function Dashboard({ dataSourceId }: { dataSourceId: string }) {
|
|
157
|
+
const { user, hasRole, hasPermission } = useDocyrusAuth()
|
|
158
|
+
|
|
159
|
+
if (!user) return <Spinner />
|
|
160
|
+
|
|
161
|
+
const canEdit = hasPermission('edit', dataSourceId)
|
|
162
|
+
const canDelete = hasPermission('delete', dataSourceId)
|
|
163
|
+
const isAdmin = hasRole('super_admin')
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div>
|
|
167
|
+
{canEdit && <Button>Edit</Button>}
|
|
168
|
+
{canDelete && <Button variant="destructive">Delete</Button>}
|
|
169
|
+
{isAdmin && <AdminPanel />}
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Permission Resolution Order
|
|
176
|
+
|
|
177
|
+
1. `super_admin` role → always granted
|
|
178
|
+
2. `global_editor` role → granted for: view, create, edit, delete, create_bulk, export, import, print
|
|
179
|
+
3. `global_viewer` role → granted only for: view
|
|
180
|
+
4. Always-permitted system data sources (reports, todos, notes, etc.)
|
|
181
|
+
5. User's `aclRules` array (merged from all roles by the server)
|
|
182
|
+
|
|
183
|
+
### Pure Functions (No React)
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { hasRole, hasPermission } from '@docyrus/signin/core'
|
|
187
|
+
import type { DocyrusUser } from '@docyrus/signin/core'
|
|
188
|
+
|
|
189
|
+
hasRole(user, 'super_admin')
|
|
190
|
+
hasPermission(user, 'edit', 'some-ds-id')
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## API Client Access Pattern
|
|
196
|
+
|
|
197
|
+
Generated collections are React hooks that use `useDocyrusClient()` internally to get the authenticated `RestApiClient` from `DocyrusAuthProvider`. No manual client syncing is needed.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// Collections get the client automatically via useDocyrusClient()
|
|
201
|
+
function useBaseProjectCollection() {
|
|
202
|
+
const client = useDocyrusClient()
|
|
203
|
+
return {
|
|
204
|
+
list: (params?) => client!.get('/v1/apps/base/data-sources/project/items', params),
|
|
205
|
+
// ... other CRUD methods
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// In your component — just call the collection hook
|
|
210
|
+
function ProjectList() {
|
|
211
|
+
const { list } = useBaseProjectCollection()
|
|
212
|
+
const { data } = useQuery({
|
|
213
|
+
queryKey: ['projects'],
|
|
214
|
+
queryFn: () => list({ columns: ['name', 'status'] }),
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
For direct API access outside collections, use the `useDocyrusClient()` hook:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
const client = useDocyrusClient()
|
|
223
|
+
const data = await client!.get<MyType>('/v1/custom-endpoint')
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Interceptors
|
|
229
|
+
|
|
230
|
+
Add request/response interceptors via `client.use()`:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
client.use({
|
|
234
|
+
request: (config) => {
|
|
235
|
+
// Transform columns array to comma-separated string
|
|
236
|
+
if (config.params?.columns && Array.isArray(config.params.columns)) {
|
|
237
|
+
config.params.columns = config.params.columns.join(',')
|
|
238
|
+
}
|
|
239
|
+
return config
|
|
240
|
+
},
|
|
241
|
+
response: (response) => {
|
|
242
|
+
// Unwrap nested .data property
|
|
243
|
+
if (response.data?.data && !Array.isArray(response.data)) {
|
|
244
|
+
response.data = response.data.data
|
|
245
|
+
}
|
|
246
|
+
return response
|
|
247
|
+
},
|
|
248
|
+
error: (error, request, response) => {
|
|
249
|
+
if (error.status === 401) { /* handle auth error */ }
|
|
250
|
+
return { error, request, response }
|
|
251
|
+
},
|
|
252
|
+
})
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Error Handling
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import {
|
|
261
|
+
ApiError, NetworkError, TimeoutError,
|
|
262
|
+
AuthenticationError, AuthorizationError,
|
|
263
|
+
NotFoundError, RateLimitError, ValidationError,
|
|
264
|
+
OAuth2Error, InvalidGrantError,
|
|
265
|
+
} from '@docyrus/api-client'
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
await client.get('/resource')
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (error instanceof AuthenticationError) { /* 401 */ }
|
|
271
|
+
else if (error instanceof AuthorizationError) { /* 403 */ }
|
|
272
|
+
else if (error instanceof NotFoundError) { /* 404 */ }
|
|
273
|
+
else if (error instanceof RateLimitError) { /* 429 - error.retryAfter */ }
|
|
274
|
+
else if (error instanceof NetworkError) { /* network issue */ }
|
|
275
|
+
else if (error instanceof TimeoutError) { /* timeout */ }
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Advanced Features
|
|
282
|
+
|
|
283
|
+
### SSE (Server-Sent Events)
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
const eventSource = client.sse('/events', {
|
|
287
|
+
onMessage(data) { console.log(data) },
|
|
288
|
+
onError(error) { console.error(error) },
|
|
289
|
+
onComplete() { console.log('done') },
|
|
290
|
+
})
|
|
291
|
+
eventSource.close()
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### File Upload
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
const formData = new FormData()
|
|
298
|
+
formData.append('file', fileInput.files[0])
|
|
299
|
+
await client.post('/upload', formData)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### File Download
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
const response = await client.get('/download/file.pdf', { responseType: 'blob' })
|
|
306
|
+
const url = URL.createObjectURL(response.data)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### HTML to PDF
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
await client.html2pdf({
|
|
313
|
+
html: '<html><body>Content</body></html>',
|
|
314
|
+
options: { format: 'A4', margin: { top: 10, bottom: 10 } },
|
|
315
|
+
})
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Retry Logic
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
import { withRetry } from '@docyrus/api-client'
|
|
322
|
+
const response = await withRetry(() => client.get('/endpoint'), {
|
|
323
|
+
retries: 3, retryDelay: 1000,
|
|
324
|
+
retryCondition: (error) => error.status >= 500,
|
|
325
|
+
})
|
|
326
|
+
```
|
package/resources/pi-agent/skills/docyrus-app-dev-react/references/collections-and-patterns.md
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# Collections & App Patterns Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
1. [Collection Architecture](#collection-architecture)
|
|
6
|
+
2. [Generated Collection Structure](#generated-collection-structure)
|
|
7
|
+
3. [Collection Types](#collection-types)
|
|
8
|
+
4. [useUsersCollection](#useuserscollection)
|
|
9
|
+
5. [TanStack Query Hooks Pattern](#tanstack-query-hooks-pattern)
|
|
10
|
+
6. [Query Key Factory Pattern](#query-key-factory-pattern)
|
|
11
|
+
7. [Mutation Pattern](#mutation-pattern)
|
|
12
|
+
8. [App Bootstrap Flow](#app-bootstrap-flow)
|
|
13
|
+
9. [Routing Setup](#routing-setup)
|
|
14
|
+
10. [API Endpoints](#api-endpoints)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Collection Architecture
|
|
19
|
+
|
|
20
|
+
Collections are auto-generated from `openapi.json` using `@docyrus/tanstack-db-generator`. They provide type-safe CRUD operations for each data source.
|
|
21
|
+
|
|
22
|
+
**Generate command**: `pnpm generate-orm` (runs `@docyrus/tanstack-db-generator openapi.json`)
|
|
23
|
+
|
|
24
|
+
**Key files:**
|
|
25
|
+
- `src/collections/<app>-<entity>.collection.ts` — generated React hooks with CRUD methods + entity types
|
|
26
|
+
- `src/collections/types.ts` — shared query types (filters, calculations, formulas, etc.)
|
|
27
|
+
- `src/collections/users.collection.ts` — special system users collection hook
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Generated Collection Structure
|
|
32
|
+
|
|
33
|
+
Each collection exports an entity interface and a React hook that returns CRUD methods:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// Generated collection for base/project
|
|
37
|
+
import { useDocyrusClient } from '@docyrus/signin'
|
|
38
|
+
import type { ICollectionListParams } from './types'
|
|
39
|
+
|
|
40
|
+
export interface BaseProjectEntity {
|
|
41
|
+
id?: string
|
|
42
|
+
record_owner?: string
|
|
43
|
+
created_on?: string
|
|
44
|
+
created_by?: string
|
|
45
|
+
last_modified_on?: string
|
|
46
|
+
last_modified_by?: string
|
|
47
|
+
name: string
|
|
48
|
+
description?: Record<string, any>
|
|
49
|
+
status?: { id: string; name: string } | any
|
|
50
|
+
organization?: { id: string; name: string } | string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function useBaseProjectCollection() {
|
|
54
|
+
const client = useDocyrusClient()
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
list: (params?: ICollectionListParams): Promise<Array<BaseProjectEntity>> =>
|
|
58
|
+
client!.get('/v1/apps/base/data-sources/project/items', params as any),
|
|
59
|
+
|
|
60
|
+
get: (recordId: string, params?: { columns?: Array<string> }): Promise<BaseProjectEntity> =>
|
|
61
|
+
client!.get(`/v1/apps/base/data-sources/project/items/${recordId}`, params),
|
|
62
|
+
|
|
63
|
+
create: (data: Record<string, any>): Promise<BaseProjectEntity> =>
|
|
64
|
+
client!.post('/v1/apps/base/data-sources/project/items', data),
|
|
65
|
+
|
|
66
|
+
update: (recordId: string, data: Record<string, any>): Promise<BaseProjectEntity> =>
|
|
67
|
+
client!.patch(`/v1/apps/base/data-sources/project/items/${recordId}`, data),
|
|
68
|
+
|
|
69
|
+
delete: (recordId: string): Promise<void> =>
|
|
70
|
+
client!.delete(`/v1/apps/base/data-sources/project/items/${recordId}`),
|
|
71
|
+
|
|
72
|
+
deleteMany: (data: { recordIds: Array<string> }): Promise<void> =>
|
|
73
|
+
client!.delete('/v1/apps/base/data-sources/project/items', data),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Collections are hooks because they use `useDocyrusClient()` internally, which provides the authenticated `RestApiClient` from `DocyrusAuthProvider`. This means collections must be called inside React components.
|
|
79
|
+
|
|
80
|
+
### Default Fields (always present)
|
|
81
|
+
Every data source entity includes: `id`, `record_owner`, `created_on`, `created_by`, `last_modified_on`, `last_modified_by`, `name`
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Collection Types
|
|
86
|
+
|
|
87
|
+
Shared query parameter types in `src/collections/types.ts`:
|
|
88
|
+
|
|
89
|
+
- `ICollectionListParams` — full query payload with columns, filters, calculations, formulas, childQueries, pivot, orderBy, limit, offset, fullCount, expand
|
|
90
|
+
- `ICollectionFilterRule` — single filter rule
|
|
91
|
+
- `ICollectionFilterGroup` — nested filter group
|
|
92
|
+
- `ICollectionCalculation` — aggregation rule
|
|
93
|
+
- `ICollectionFormula` — simple formula
|
|
94
|
+
- `ICollectionBlockFormula` — block/subquery formula
|
|
95
|
+
- `ICollectionChildQuery` — child query definition
|
|
96
|
+
- `ICollectionPivot` / `ICollectionPivotMatrix` — pivot configuration
|
|
97
|
+
- `ICollectionOrderBy` — sort specification
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## useUsersCollection
|
|
102
|
+
|
|
103
|
+
System users collection hook with special methods:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
export function useUsersCollection() {
|
|
107
|
+
const client = useDocyrusClient()
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
getUsers: (): Promise<Array<UserEntity>> =>
|
|
111
|
+
client!.get('/v1/users'),
|
|
112
|
+
|
|
113
|
+
getMyInfo: (): Promise<UserEntity> =>
|
|
114
|
+
client!.get('/v1/users/me'),
|
|
115
|
+
|
|
116
|
+
createUser: (data: UserCreateParams): Promise<UserEntity> =>
|
|
117
|
+
client!.post('/v1/users', data),
|
|
118
|
+
|
|
119
|
+
updateMe: (data: UserUpdateParams): Promise<UserEntity> =>
|
|
120
|
+
client!.patch('/v1/users/me', data),
|
|
121
|
+
|
|
122
|
+
updateUser: (userId: string, data: UserUpdateParams): Promise<UserEntity> =>
|
|
123
|
+
client!.patch(`/v1/users/${userId}`, data),
|
|
124
|
+
|
|
125
|
+
changeUserStatus: (userId: string, status: number) =>
|
|
126
|
+
client!.put(`/v1/users/${userId}/status/${status}`),
|
|
127
|
+
|
|
128
|
+
saveUserDevice: (data: UserDeviceDto) =>
|
|
129
|
+
client!.post('/v1/users/device', data),
|
|
130
|
+
|
|
131
|
+
getMyTenants: () =>
|
|
132
|
+
client!.get('/v1/users/me/tenants'),
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Use `useUsersCollection().getMyInfo()` for current user profile.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## TanStack Query Hooks Pattern
|
|
142
|
+
|
|
143
|
+
Wrap collection hook methods in TanStack Query hooks. Since collections are themselves hooks, call them inside the component/hook, then pass the returned methods to TanStack Query:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { useQuery } from '@tanstack/react-query'
|
|
147
|
+
import { useBaseProjectCollection } from '@/collections/base-project.collection'
|
|
148
|
+
import { queryKeys } from '@/lib/query-keys'
|
|
149
|
+
|
|
150
|
+
const PROJECT_COLUMNS = ['name', 'status', 'description', 'record_owner(id,firstname,lastname)']
|
|
151
|
+
|
|
152
|
+
export function useProjects(params?: ICollectionListParams) {
|
|
153
|
+
const { list } = useBaseProjectCollection()
|
|
154
|
+
return useQuery({
|
|
155
|
+
queryKey: queryKeys.projects.list(params ?? {}),
|
|
156
|
+
queryFn: () =>
|
|
157
|
+
list({
|
|
158
|
+
columns: PROJECT_COLUMNS, // ALWAYS specify columns
|
|
159
|
+
...params,
|
|
160
|
+
}),
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function useProject(projectId: string) {
|
|
165
|
+
const { get } = useBaseProjectCollection()
|
|
166
|
+
return useQuery({
|
|
167
|
+
queryKey: queryKeys.projects.detail(projectId),
|
|
168
|
+
queryFn: () =>
|
|
169
|
+
get(projectId, {
|
|
170
|
+
columns: PROJECT_COLUMNS,
|
|
171
|
+
}),
|
|
172
|
+
enabled: !!projectId,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Query Key Factory Pattern
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
export const queryKeys = {
|
|
183
|
+
projects: {
|
|
184
|
+
all: ['projects'] as const,
|
|
185
|
+
lists: () => [...queryKeys.projects.all, 'list'] as const,
|
|
186
|
+
list: (params: object) => [...queryKeys.projects.lists(), params] as const,
|
|
187
|
+
detail: (id: string) => [...queryKeys.projects.all, 'detail', id] as const,
|
|
188
|
+
},
|
|
189
|
+
tasks: {
|
|
190
|
+
all: ['tasks'] as const,
|
|
191
|
+
lists: () => [...queryKeys.tasks.all, 'list'] as const,
|
|
192
|
+
list: (params: object) => [...queryKeys.tasks.lists(), params] as const,
|
|
193
|
+
detail: (id: string) => [...queryKeys.tasks.all, 'detail', id] as const,
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Mutation Pattern
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
204
|
+
import { useBaseProjectCollection } from '@/collections/base-project.collection'
|
|
205
|
+
|
|
206
|
+
export function useCreateProject() {
|
|
207
|
+
const { create } = useBaseProjectCollection()
|
|
208
|
+
const queryClient = useQueryClient()
|
|
209
|
+
return useMutation({
|
|
210
|
+
mutationFn: (data: Record<string, unknown>) => create(data),
|
|
211
|
+
onSuccess: () => {
|
|
212
|
+
void queryClient.invalidateQueries({
|
|
213
|
+
queryKey: queryKeys.projects.all,
|
|
214
|
+
})
|
|
215
|
+
},
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function useUpdateProject() {
|
|
220
|
+
const { update } = useBaseProjectCollection()
|
|
221
|
+
const queryClient = useQueryClient()
|
|
222
|
+
return useMutation({
|
|
223
|
+
mutationFn: ({ id, data }: { id: string; data: Record<string, unknown> }) =>
|
|
224
|
+
update(id, data),
|
|
225
|
+
onSuccess: (_data, { id }) => {
|
|
226
|
+
void queryClient.invalidateQueries({ queryKey: queryKeys.projects.detail(id) })
|
|
227
|
+
void queryClient.invalidateQueries({ queryKey: queryKeys.projects.lists() })
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function useDeleteProject() {
|
|
233
|
+
const { delete: deleteProject } = useBaseProjectCollection()
|
|
234
|
+
const queryClient = useQueryClient()
|
|
235
|
+
return useMutation({
|
|
236
|
+
mutationFn: (id: string) => deleteProject(id),
|
|
237
|
+
onSuccess: () => {
|
|
238
|
+
void queryClient.invalidateQueries({ queryKey: queryKeys.projects.all })
|
|
239
|
+
},
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## App Bootstrap Flow
|
|
247
|
+
|
|
248
|
+
1. `main.tsx`: Mount `DocyrusAuthProvider` → `QueryClientProvider` → `RouterProvider`
|
|
249
|
+
2. `App.tsx`: Check `useDocyrusAuth()` status — `user` is auto-fetched from `/v1/users/me`
|
|
250
|
+
3. Use `hasRole()` / `hasPermission()` from `useDocyrusAuth()` for authorization checks
|
|
251
|
+
4. Use collection hooks (e.g., `useUsersCollection()`) for data access — they get the authenticated client via `useDocyrusClient()` internally
|
|
252
|
+
5. Render protected routes
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// App.tsx
|
|
256
|
+
function App() {
|
|
257
|
+
const { status, user, hasRole, hasPermission } = useDocyrusAuth()
|
|
258
|
+
|
|
259
|
+
if (status === 'loading') return <LoadingSpinner />
|
|
260
|
+
if (status === 'unauthenticated') return <LoginPage />
|
|
261
|
+
|
|
262
|
+
// user auto-fetched, hasRole/hasPermission ready
|
|
263
|
+
return <AppLayout />
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Routing Setup
|
|
270
|
+
|
|
271
|
+
TanStack Router with code-based routes:
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { createRouter, createRoute, createRootRoute } from '@tanstack/react-router'
|
|
275
|
+
|
|
276
|
+
const rootRoute = createRootRoute({ component: () => <Outlet /> })
|
|
277
|
+
|
|
278
|
+
const layoutRoute = createRoute({
|
|
279
|
+
getParentRoute: () => rootRoute,
|
|
280
|
+
id: 'layout',
|
|
281
|
+
component: AppLayout,
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
const indexRoute = createRoute({
|
|
285
|
+
getParentRoute: () => layoutRoute,
|
|
286
|
+
path: '/',
|
|
287
|
+
component: DashboardPage,
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
const projectsRoute = createRoute({
|
|
291
|
+
getParentRoute: () => layoutRoute,
|
|
292
|
+
path: '/projects',
|
|
293
|
+
component: ProjectsPage,
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
const projectDetailRoute = createRoute({
|
|
297
|
+
getParentRoute: () => layoutRoute,
|
|
298
|
+
path: '/projects/$projectId',
|
|
299
|
+
component: ProjectDetailPage,
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
// Auth routes (public)
|
|
303
|
+
const authCallbackRoute = createRoute({
|
|
304
|
+
getParentRoute: () => rootRoute,
|
|
305
|
+
path: '/auth/callback',
|
|
306
|
+
component: () => <div>Processing login...</div>,
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
const routeTree = rootRoute.addChildren([
|
|
310
|
+
layoutRoute.addChildren([indexRoute, projectsRoute, projectDetailRoute]),
|
|
311
|
+
authCallbackRoute,
|
|
312
|
+
])
|
|
313
|
+
|
|
314
|
+
const router = createRouter({
|
|
315
|
+
routeTree,
|
|
316
|
+
defaultPreload: 'intent',
|
|
317
|
+
scrollRestoration: true,
|
|
318
|
+
})
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## API Endpoints
|
|
324
|
+
|
|
325
|
+
### Data Source Items (Dynamic)
|
|
326
|
+
```
|
|
327
|
+
GET /v1/apps/{appSlug}/data-sources/{slug}/items — List (with query payload)
|
|
328
|
+
GET /v1/apps/{appSlug}/data-sources/{slug}/items/{id} — Get one
|
|
329
|
+
POST /v1/apps/{appSlug}/data-sources/{slug}/items — Create
|
|
330
|
+
PATCH /v1/apps/{appSlug}/data-sources/{slug}/items/{id} — Update
|
|
331
|
+
DELETE /v1/apps/{appSlug}/data-sources/{slug}/items/{id} — Delete one
|
|
332
|
+
DELETE /v1/apps/{appSlug}/data-sources/{slug}/items — Delete many
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Endpoints are **dynamic** — they exist only if a data source is defined in the tenant. The `openapi.json` spec enumerates all available data sources.
|
|
336
|
+
|
|
337
|
+
### System Endpoints (Always Available)
|
|
338
|
+
```
|
|
339
|
+
GET /v1/users — List users
|
|
340
|
+
POST /v1/users — Create user
|
|
341
|
+
GET /v1/users/me — Current user profile
|
|
342
|
+
PATCH /v1/users/me — Update current user
|
|
343
|
+
PATCH /v1/users/{userId} — Update user
|
|
344
|
+
PUT /v1/users/{userId}/status/{s} — Change user status
|
|
345
|
+
POST /v1/users/device — Save push notification device
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Other Standard Endpoints
|
|
349
|
+
```
|
|
350
|
+
GET /v1/api/openapi.json — Generate OpenAPI spec
|
|
351
|
+
HEAD /v1/oauth2 — Check rate limits
|
|
352
|
+
PUT reports/runCustomQuery/{id} — Run custom query/report
|
|
353
|
+
```
|