@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.
Files changed (30) hide show
  1. package/agent-loader.js +36 -25
  2. package/agent-loader.js.map +4 -4
  3. package/main.js +4 -4
  4. package/main.js.map +1 -1
  5. package/package.json +3 -3
  6. package/resources/pi-agent/extensions/docyrus-web-browser.ts +31 -0
  7. package/resources/pi-agent/shared/docyrusWebBrowserProtocol.ts +169 -0
  8. package/resources/pi-agent/skills/diffity-diff/SKILL.md +1 -1
  9. package/resources/pi-agent/skills/diffity-resolve/SKILL.md +4 -4
  10. package/resources/pi-agent/skills/diffity-review/SKILL.md +5 -4
  11. package/resources/pi-agent/skills/docyrus-api-dev/SKILL.md +197 -0
  12. package/resources/pi-agent/skills/docyrus-api-dev/references/acl-endpoints-frontend.md +295 -0
  13. package/resources/pi-agent/skills/docyrus-api-dev/references/api-client.md +349 -0
  14. package/resources/pi-agent/skills/docyrus-api-dev/references/authentication.md +298 -0
  15. package/resources/pi-agent/skills/docyrus-api-dev/references/data-source-query-guide.md +2063 -0
  16. package/resources/pi-agent/skills/docyrus-api-dev/references/formula-design-guide-llm.md +312 -0
  17. package/resources/pi-agent/skills/docyrus-api-dev/references/query-and-formulas.md +592 -0
  18. package/resources/pi-agent/skills/docyrus-app-dev-react/SKILL.md +361 -0
  19. package/resources/pi-agent/skills/docyrus-app-dev-react/references/README.md +29 -0
  20. package/resources/pi-agent/skills/docyrus-app-dev-react/references/api-client-and-auth.md +326 -0
  21. package/resources/pi-agent/skills/docyrus-app-dev-react/references/collections-and-patterns.md +353 -0
  22. package/resources/pi-agent/skills/docyrus-app-dev-react/references/component-selection-guide.md +619 -0
  23. package/resources/pi-agent/skills/docyrus-app-dev-react/references/icon-usage-guide.md +463 -0
  24. package/resources/pi-agent/skills/docyrus-app-dev-react/references/preferred-components-catalog.md +242 -0
  25. package/resources/pi-agent/skills/docyrus-platform/SKILL.md +2 -2
  26. package/resources/pi-agent/skills/docyrus-platform/references/auth-and-multi-tenancy.md +9 -1
  27. package/resources/pi-agent/skills/docyrus-platform/references/developer-tools.md +3 -2
  28. package/server-loader.js +328 -87
  29. package/server-loader.js.map +4 -4
  30. 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
+ ```
@@ -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
+ ```