@norbix.ai/react-redux 1.3.0 → 1.4.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/README.md CHANGED
@@ -23,11 +23,11 @@
23
23
 
24
24
  `@norbix.ai/react-redux` is a thin, opinionated layer that wires the Norbix TypeScript SDK into a Redux Toolkit + RTK Query app. You get React hooks for the most-used Norbix endpoints with full caching, request dedup, and tag-based invalidation built in. Under the hood every hook calls the same typed `Norbix` SDK — so DTOs, auth, errors, and base URLs all stay consistent with the rest of your stack.
25
25
 
26
- | You get | Provided by |
27
- |---|---|
28
- | `useGetUsersQuery`, `useFindCollectionQuery`, `useInsertOneMutation`, ... | this package |
29
- | Cache, dedup, polling, refetch on focus, optimistic updates | RTK Query (`@reduxjs/toolkit/query`) |
30
- | Tree-shakeable typed methods, JWT auth, error mapping | `@norbix.ai/ts` SDK |
26
+ | You get | Provided by |
27
+ | ------------------------------------------------------------------------- | ------------------------------------ |
28
+ | `useGetUsersQuery`, `useFindCollectionQuery`, `useInsertOneMutation`, ... | this package |
29
+ | Cache, dedup, polling, refetch on focus, optimistic updates | RTK Query (`@reduxjs/toolkit/query`) |
30
+ | Tree-shakeable typed methods, JWT auth, error mapping | `@norbix.ai/ts` SDK |
31
31
 
32
32
  ## Install
33
33
 
@@ -104,11 +104,10 @@ export function UsersList() {
104
104
 
105
105
  return (
106
106
  <ul>
107
- {data?.list?.items.map((u) => <li key={u.id}>{u.email}</li>)}
108
- <button
109
- disabled={inviting}
110
- onClick={() => invite({ email: 'maya@team.io' })}
111
- >
107
+ {data?.list?.items.map((u) => (
108
+ <li key={u.id}>{u.email}</li>
109
+ ))}
110
+ <button disabled={inviting} onClick={() => invite({ email: 'maya@team.io' })}>
112
111
  Invite
113
112
  </button>
114
113
  </ul>
@@ -120,74 +119,115 @@ After `invite()` resolves, the `User/LIST` tag is invalidated and `useGetUsersQu
120
119
 
121
120
  ## What ships in the box
122
121
 
123
- A *curated* set of hooks for the most common endpoints. Every hook is end-to-end typed from the SDK's own DTOs.
122
+ A _curated_ set of hooks for the most common endpoints. Every hook is end-to-end typed from the SDK's own DTOs.
124
123
 
125
124
  ### API surface — `norbix.api.*`
126
125
 
127
- | Hook | Wraps | Cache tags |
128
- |---|---|---|
129
- | `useLoginMutation` | `client.login(...)` | invalidates `User/LIST`, `AccountProfile/CURRENT` |
130
- | `useLogoutMutation` | `client.logout()` | invalidates `User`, `AccountProfile`, `Collection`, `Schema` |
131
- | `useAuthenticateMutation` | `api.auth.authenticate` | — |
132
- | `useGetUsersQuery` | `api.membership.getUsers` | provides `User/LIST` |
133
- | `useGetUserQuery` | `api.membership.getUser` | provides `User/<id>` |
134
- | `useInviteUserMutation` | `api.membership.inviteUser` | invalidates `User/LIST` |
135
- | `useUpdateUserMutation` | `api.membership.updateUser` | invalidates `User/<id>` + `User/LIST` |
136
- | `useBlockUserMutation` | `api.membership.blockUser` | invalidates `User/LIST` |
137
- | `useUnblockUserMutation` | `api.membership.unblockUser` | invalidates `User/LIST` |
138
- | `useDeleteUserMutation` | `api.membership.deleteUser` | invalidates `User/LIST` |
139
- | `useFindCollectionQuery` | `api.database.find` | provides `Collection/<name>` |
140
- | `useFindOneQuery` | `api.database.findOne` | provides `Collection/<name>` |
141
- | `useCountCollectionQuery` | `api.database.count` | provides `Collection/<name>` |
142
- | `useInsertOneMutation` | `api.database.insertOne` | invalidates `Collection/<name>` |
143
- | `useUpdateOneMutation` | `api.database.updateOne` | invalidates `Collection/<name>` |
144
- | `useReplaceOneMutation` | `api.database.replaceOne` | invalidates `Collection/<name>` |
145
- | `useDeleteOneMutation` | `api.database.deleteOne` | invalidates `Collection/<name>` |
146
- | `useFindTermsQuery` | `api.database.findTerms` | provides `DatabaseTaxonomyTerms/<taxonomyName>` |
147
- | `useFindTermsChildrenQuery` | `api.database.findTermsChildren` | provides `DatabaseTaxonomyTerms/<taxonomyName>` |
148
- | `useFindTermTreeQuery` | `api.database.findTermTree` | provides `DatabaseTaxonomyTerms/<taxonomyName>` |
149
- | `useFindTaxonomyTreeQuery` | `api.database.findTaxonomyTree` | provides `DatabaseTaxonomyTerms/ANY` |
150
- | `useGetApiKeysQuery` | `api.apikeys.getApiKeys` | provides `ApiKey/LIST` |
151
- | `useRegenerateApiKeysMutation` | `api.apikeys.regenerateApiKeys` | invalidates `ApiKey/LIST` |
152
- | `useGetPublicFileQuery` | `api.files.getPublicFile` | provides `Files/PUBLIC:<publicId>` |
153
- | `useTestFilesIntegrationApiMutation` | `api.files.testFilesIntegration` | invalidates `Files` + `FilesIntegrations` |
126
+ | Hook | Wraps | Cache tags |
127
+ | ------------------------------------ | -------------------------------- | ------------------------------------------------------------ |
128
+ | `useLoginMutation` | `client.login(...)` | invalidates `User/LIST`, `AccountProfile/CURRENT` |
129
+ | `useLogoutMutation` | `client.logout()` | invalidates `User`, `AccountProfile`, `Collection`, `Schema` |
130
+ | `useAuthenticateMutation` | `api.auth.authenticate` | — |
131
+ | `useGetUsersQuery` | `api.membership.getUsers` | provides `User/LIST` |
132
+ | `useGetUserQuery` | `api.membership.getUser` | provides `User/<id>` |
133
+ | `useInviteUserMutation` | `api.membership.inviteUser` | invalidates `User/LIST` |
134
+ | `useUpdateUserMutation` | `api.membership.updateUser` | invalidates `User/<id>` + `User/LIST` |
135
+ | `useBlockUserMutation` | `api.membership.blockUser` | invalidates `User/LIST` |
136
+ | `useUnblockUserMutation` | `api.membership.unblockUser` | invalidates `User/LIST` |
137
+ | `useDeleteUserMutation` | `api.membership.deleteUser` | invalidates `User/LIST` |
138
+ | `useFindCollectionQuery` | `api.database.find` | provides `Collection/<name>` |
139
+ | `useFindOneQuery` | `api.database.findOne` | provides `Collection/<name>` |
140
+ | `useCountCollectionQuery` | `api.database.count` | provides `Collection/<name>` |
141
+ | `useInsertOneMutation` | `api.database.insertOne` | invalidates `Collection/<name>` |
142
+ | `useUpdateOneMutation` | `api.database.updateOne` | invalidates `Collection/<name>` |
143
+ | `useReplaceOneMutation` | `api.database.replaceOne` | invalidates `Collection/<name>` |
144
+ | `useDeleteOneMutation` | `api.database.deleteOne` | invalidates `Collection/<name>` |
145
+ | `useFindTermsQuery` | `api.database.findTerms` | provides `DatabaseTaxonomyTerms/<taxonomyName>` |
146
+ | `useFindTermsChildrenQuery` | `api.database.findTermsChildren` | provides `DatabaseTaxonomyTerms/<taxonomyName>` |
147
+ | `useFindTermTreeQuery` | `api.database.findTermTree` | provides `DatabaseTaxonomyTerms/<taxonomyName>` |
148
+ | `useFindTaxonomyTreeQuery` | `api.database.findTaxonomyTree` | provides `DatabaseTaxonomyTerms/ANY` |
149
+ | `useGetApiKeysQuery` | `api.apikeys.getApiKeys` | provides `ApiKey/LIST` |
150
+ | `useRegenerateApiKeysMutation` | `api.apikeys.regenerateApiKeys` | invalidates `ApiKey/LIST` |
151
+ | `useGetPublicFileQuery` | `api.files.getPublicFile` | provides `Files/PUBLIC:<publicId>` |
152
+ | `useTestFilesIntegrationApiMutation` | `api.files.testFilesIntegration` | invalidates `Files` + `FilesIntegrations` |
154
153
 
155
154
  ### Hub surface — `norbix.hub.*`
156
155
 
157
- | Hook | Wraps | Cache tags |
158
- |---|---|---|
159
- | `useGetAccountProfileQuery` | `hub.account.getAccountProfile` | provides `AccountProfile/CURRENT` |
160
- | `useGetAccountStatusQuery` | `hub.account.getAccountStatus` | provides `AccountProfile/STATUS` |
161
- | `useUpdateAccountProfileMutation` | `hub.account.updateAccountProfile` | invalidates `AccountProfile/CURRENT` |
162
- | `useGetProjectsQuery` | `hub.account.getProjects` | provides `AccountProfile/PROJECTS` |
163
- | `useGetProjectQuery` | `hub.account.getProject` | provides `AccountProfile/PROJECT/<id>` |
164
- | `useCreateProjectMutation` | `hub.account.createProject` | invalidates `AccountProfile/PROJECTS` |
165
- | `useDeleteProjectMutation` | `hub.account.deleteProject` | invalidates `AccountProfile/PROJECTS` |
166
- | `useListRegionsQuery` | `hub.regions.list` | provides `Regions` |
167
- | `useUpdateProjectRegionsMutation` | `hub.regions.updateProjectRegions` | invalidates `Regions` + `Projects` |
168
- | `useGetDatabaseSchemasQuery` | `hub.database.getDatabaseSchemas` | provides `Schema/LIST` |
169
- | `useGetDatabaseSchemaQuery` | `hub.database.getDatabaseSchema` | provides `Schema/<id>` |
170
- | `useSaveDatabaseSchemaMutation` | `hub.database.saveDatabaseSchema` | invalidates `Schema/LIST` |
171
- | `useRenameDatabaseSchemaMutation` | `hub.database.renameDatabaseSchema` | invalidates `Schema/LIST` + `Schema/<id>` |
172
- | `usePublishDatabaseSchemaMutation` | `hub.database.publishDatabaseSchema` | invalidates `Schema` + `Collection` |
173
- | `useDeleteDatabaseSchemaMutation` | `hub.database.deleteDatabaseSchema` | invalidates `Schema/LIST` + `Collection` |
174
- | `useGetEmailTemplatesQuery` | `hub.notifications.getEmailTemplates` | provides `Notification/EMAIL_TEMPLATE_LIST` |
175
- | `useGetEmailTemplateQuery` | `hub.notifications.getEmailTemplate` | provides `Notification/EMAIL_TEMPLATE/<id>` |
176
- | `useCreateEmailTemplateMutation` | `hub.notifications.createEmailTemplate` | invalidates `Notification/EMAIL_TEMPLATE_LIST` |
177
- | `useUpdateEmailTemplateMutation` | `hub.notifications.updateEmailTemplate` | invalidates list + per-id |
178
- | `useDeleteEmailTemplateMutation` | `hub.notifications.deleteEmailTemplate` | invalidates list |
179
- | `useTestFilesIntegrationMutation` | `hub.files.testFilesIntegration` | — |
180
- | `useMakeFilePublicMutation` | `hub.files.makeFilePublic` | invalidates `Files` |
181
- | `useMakeFilePrivateMutation` | `hub.files.makeFilePrivate` | invalidates `Files` |
182
- | `useMakeFolderPublicMutation` | `hub.files.makeFolderPublic` | invalidates `Files` |
183
- | `useMakeFolderPrivateMutation` | `hub.files.makeFolderPrivate` | invalidates `Files` |
156
+ | Hook | Wraps | Cache tags |
157
+ | ---------------------------------- | --------------------------------------- | ---------------------------------------------- |
158
+ | `useGetAccountProfileQuery` | `hub.account.getAccountProfile` | provides `AccountProfile/CURRENT` |
159
+ | `useGetAccountStatusQuery` | `hub.account.getAccountStatus` | provides `AccountProfile/STATUS` |
160
+ | `useUpdateAccountProfileMutation` | `hub.account.updateAccountProfile` | invalidates `AccountProfile/CURRENT` |
161
+ | `useGetProjectsQuery` | `hub.account.getProjects` | provides `AccountProfile/PROJECTS` |
162
+ | `useGetProjectQuery` | `hub.account.getProject` | provides `AccountProfile/PROJECT/<id>` |
163
+ | `useCreateProjectMutation` | `hub.account.createProject` | invalidates `AccountProfile/PROJECTS` |
164
+ | `useDeleteProjectMutation` | `hub.account.deleteProject` | invalidates `AccountProfile/PROJECTS` |
165
+ | `useListRegionsQuery` | `hub.regions.list` | provides `Regions` |
166
+ | `useUpdateProjectRegionsMutation` | `hub.regions.updateProjectRegions` | invalidates `Regions` + `Projects` |
167
+ | `useGetDatabaseSchemasQuery` | `hub.database.getDatabaseSchemas` | provides `Schema/LIST` |
168
+ | `useGetDatabaseSchemaQuery` | `hub.database.getDatabaseSchema` | provides `Schema/<id>` |
169
+ | `useSaveDatabaseSchemaMutation` | `hub.database.saveDatabaseSchema` | invalidates `Schema/LIST` |
170
+ | `useRenameDatabaseSchemaMutation` | `hub.database.renameDatabaseSchema` | invalidates `Schema/LIST` + `Schema/<id>` |
171
+ | `usePublishDatabaseSchemaMutation` | `hub.database.publishDatabaseSchema` | invalidates `Schema` + `Collection` |
172
+ | `useDeleteDatabaseSchemaMutation` | `hub.database.deleteDatabaseSchema` | invalidates `Schema/LIST` + `Collection` |
173
+ | `useGetEmailTemplatesQuery` | `hub.notifications.getEmailTemplates` | provides `Notification/EMAIL_TEMPLATE_LIST` |
174
+ | `useGetEmailTemplateQuery` | `hub.notifications.getEmailTemplate` | provides `Notification/EMAIL_TEMPLATE/<id>` |
175
+ | `useCreateEmailTemplateMutation` | `hub.notifications.createEmailTemplate` | invalidates `Notification/EMAIL_TEMPLATE_LIST` |
176
+ | `useUpdateEmailTemplateMutation` | `hub.notifications.updateEmailTemplate` | invalidates list + per-id |
177
+ | `useDeleteEmailTemplateMutation` | `hub.notifications.deleteEmailTemplate` | invalidates list |
178
+ | `useTestFilesIntegrationMutation` | `hub.files.testFilesIntegration` | — |
179
+ | `useMakeFilePublicMutation` | `hub.files.makeFilePublic` | invalidates `Files` |
180
+ | `useMakeFilePrivateMutation` | `hub.files.makeFilePrivate` | invalidates `Files` |
181
+ | `useMakeFolderPublicMutation` | `hub.files.makeFolderPublic` | invalidates `Files` |
182
+ | `useMakeFolderPrivateMutation` | `hub.files.makeFolderPrivate` | invalidates `Files` |
184
183
 
185
184
  > **Need a hook we don't ship?** Two options. (1) Drop down to the SDK with `useNorbix()` for a one-off call. (2) Add a new file under `src/hooks/api/` or `src/hooks/hub/`, follow the pattern of the others, and open a PR.
186
185
 
186
+ ### End-user AI chat — `norbix.api.ai`
187
+
188
+ For a signed-in project user. `useStartEndUserChatTurnMutation` answers at once with a `turnId`; the answer streams on the user's SSE channel `ai-chat:{projectId}:{authId}` — open it with `norbix.aiChat({ authId })` from `@norbix.ai/ts` (a foreign channel is refused with 403 `AiChatChannelRefused`, no retry). Needs `@norbix.ai/ts` >= 4.3.0.
189
+
190
+ | Hook | SDK method | Cache |
191
+ | ---------------------------------------- | ------------------------------------ | -------------------- |
192
+ | `useGetEndUserChatAvailabilityQuery` | `api.ai.getEndUserChatAvailability` | provides `AiChat` |
193
+ | `useListEndUserChatSessionsQuery` | `api.ai.listEndUserChatSessions` | provides `AiChat` |
194
+ | `useCreateEndUserChatSessionMutation` | `api.ai.createEndUserChatSession` | invalidates `AiChat` |
195
+ | `useGetEndUserChatSessionQuery` | `api.ai.getEndUserChatSession` | provides `AiChat` |
196
+ | `useRenameEndUserChatSessionMutation` | `api.ai.renameEndUserChatSession` | invalidates `AiChat` |
197
+ | `useDeleteEndUserChatSessionMutation` | `api.ai.deleteEndUserChatSession` | invalidates `AiChat` |
198
+ | `usePinEndUserChatSessionMutation` | `api.ai.pinEndUserChatSession` | invalidates `AiChat` |
199
+ | `useArchiveEndUserChatSessionMutation` | `api.ai.archiveEndUserChatSession` | invalidates `AiChat` |
200
+ | `useGetEndUserChatEntriesQuery` | `api.ai.getEndUserChatEntries` | provides `AiChat` |
201
+ | `useSetEndUserChatEntryFeedbackMutation` | `api.ai.setEndUserChatEntryFeedback` | invalidates `AiChat` |
202
+ | `useListEndUserChatAttachmentsQuery` | `api.ai.listEndUserChatAttachments` | provides `AiChat` |
203
+ | `useUploadEndUserChatAttachmentMutation` | `api.ai.uploadEndUserChatAttachment` | invalidates `AiChat` |
204
+ | `useDeleteEndUserChatAttachmentMutation` | `api.ai.deleteEndUserChatAttachment` | invalidates `AiChat` |
205
+ | `useListEndUserChatMemoryQuery` | `api.ai.listEndUserChatMemory` | provides `AiChat` |
206
+ | `useForgetEndUserChatMemoryMutation` | `api.ai.forgetEndUserChatMemory` | invalidates `AiChat` |
207
+ | `useStartEndUserChatTurnMutation` | `api.ai.startEndUserChatTurn` | invalidates `AiChat` |
208
+
209
+ ### Project AI settings, assistants, usage, Admin Portal — `norbix.hub.account` / embeddings — `norbix.hub.ai`
210
+
211
+ | Hook | SDK method | Cache |
212
+ | --------------------------------------- | -------------------------------------- | ---------------------- |
213
+ | `useGetProjectAiSettingsQuery` | `hub.account.getProjectAiSettings` | provides `Projects` |
214
+ | `useUpdateProjectAiSettingsMutation` | `hub.account.updateProjectAiSettings` | invalidates `Projects` |
215
+ | `useCreateProjectAiAssistantMutation` | `hub.account.createProjectAiAssistant` | invalidates `Projects` |
216
+ | `useUpdateProjectAiAssistantMutation` | `hub.account.updateProjectAiAssistant` | invalidates `Projects` |
217
+ | `useDeleteProjectAiAssistantMutation` | `hub.account.deleteProjectAiAssistant` | invalidates `Projects` |
218
+ | `useGetProjectAiUsageQuery` | `hub.account.getProjectAiUsage` | provides `Projects` |
219
+ | `useSetAdminPortalEnabledMutation` | `hub.account.setAdminPortalEnabled` | invalidates `Projects` |
220
+ | `useGetEmbeddingIntegrationsQuery` | `hub.ai.getEmbeddingIntegrations` | provides `Ai` |
221
+ | `useSaveEmbeddingIntegrationMutation` | `hub.ai.saveEmbeddingIntegration` | invalidates `Ai` |
222
+ | `useGetEmbeddingIntegrationQuery` | `hub.ai.getEmbeddingIntegration` | provides `Ai` |
223
+ | `useDeleteEmbeddingIntegrationMutation` | `hub.ai.deleteEmbeddingIntegration` | invalidates `Ai` |
224
+ | `useTestEmbeddingIntegrationMutation` | `hub.ai.testEmbeddingIntegration` | invalidates `Ai` |
225
+ | `useSetLlmIntegrationAsDefaultMutation` | `hub.ai.setLlmIntegrationAsDefault` | invalidates `Ai` |
226
+
187
227
  ## Public file links
188
228
 
189
229
  A file, or a whole folder prefix, can be made readable by anyone holding its
190
- link. Publishing is a Hub action and needs the signed-in client; *reading* the
230
+ link. Publishing is a Hub action and needs the signed-in client; _reading_ the
191
231
  link needs nothing at all.
192
232
 
193
233
  ```tsx
@@ -221,10 +261,10 @@ small file, read it, list the folder, delete the file — and answer one item
221
261
  per step (`UploadFile`, `GetFile`, `GetAllFiles`, `DeleteFile`) with `result`
222
262
  `OK`, `FAILED` or `NOT_TESTED`:
223
263
 
224
- | Hook | Wraps | Route | Use it from |
225
- |---|---|---|---|
264
+ | Hook | Wraps | Route | Use it from |
265
+ | ------------------------------------ | -------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------ |
226
266
  | `useTestFilesIntegrationApiMutation` | `api.files.testFilesIntegration` | `POST /{version}/files/{filesIntegrationId}/test` | your app, with an API key or a user session (needs `files:create`) |
227
- | `useTestFilesIntegrationMutation` | `hub.files.testFilesIntegration` | `POST /{version}/files/integrations/test` | a dashboard, signed in to the Hub |
267
+ | `useTestFilesIntegrationMutation` | `hub.files.testFilesIntegration` | `POST /{version}/files/integrations/test` | a dashboard, signed in to the Hub |
228
268
 
229
269
  ```tsx
230
270
  const [testIntegration, { data, isLoading }] = useTestFilesIntegrationApiMutation();
@@ -279,12 +319,12 @@ section) — the shipped ones above are the typed variants.
279
319
 
280
320
  A **taxonomy** is a named tree of **terms** (labels). A term can have one parent (a clean hierarchy) or several parents (the same item under many categories). There is one read hook per scenario — pick the one that matches what you want:
281
321
 
282
- | I want to… | Hook | `data` holds |
283
- | --- | --- | --- |
284
- | Get a taxonomy's terms as a flat list | `useFindTermsQuery` | a paginated `list` of terms |
285
- | Get only the children of one term | `useFindTermsChildrenQuery` | a `list` of child terms (direct + multi-parent) |
286
- | Get a taxonomy's terms as a ready-made tree | `useFindTermTreeQuery` | a `tree` of nested term nodes |
287
- | Get the taxonomy structure (e.g. Countries → Cities) | `useFindTaxonomyTreeQuery` | a `tree` of taxonomy nodes |
322
+ | I want to… | Hook | `data` holds |
323
+ | ---------------------------------------------------- | --------------------------- | ----------------------------------------------- |
324
+ | Get a taxonomy's terms as a flat list | `useFindTermsQuery` | a paginated `list` of terms |
325
+ | Get only the children of one term | `useFindTermsChildrenQuery` | a `list` of child terms (direct + multi-parent) |
326
+ | Get a taxonomy's terms as a ready-made tree | `useFindTermTreeQuery` | a `tree` of nested term nodes |
327
+ | Get the taxonomy structure (e.g. Countries → Cities) | `useFindTaxonomyTreeQuery` | a `tree` of taxonomy nodes |
288
328
 
289
329
  All four share the same cache tag, so a write to a taxonomy refreshes every term view automatically. The examples below all use one example `services` taxonomy shaped like this:
290
330
 
@@ -310,13 +350,46 @@ const { data, isLoading } = useFindTermsQuery({ taxonomyName: 'services' });
310
350
  {
311
351
  "list": {
312
352
  "items": [
313
- { "id": "term_indoors", "taxonomyName": "services", "parentId": null, "order": 1, "name": "Indoors" },
314
- { "id": "term_air_con", "taxonomyName": "services", "parentId": "term_indoors", "order": 1, "name": "Air conditioning" },
315
- { "id": "term_wall", "taxonomyName": "services", "parentId": "term_air_con", "order": 1, "name": "Wall-mounted" },
316
- { "id": "term_outdoors", "taxonomyName": "services", "parentId": null, "order": 2, "name": "Outdoors" },
317
- { "id": "term_solar", "taxonomyName": "services", "parentId": "term_outdoors","order": 1, "name": "Solar panels" }
353
+ {
354
+ "id": "term_indoors",
355
+ "taxonomyName": "services",
356
+ "parentId": null,
357
+ "order": 1,
358
+ "name": "Indoors"
359
+ },
360
+ {
361
+ "id": "term_air_con",
362
+ "taxonomyName": "services",
363
+ "parentId": "term_indoors",
364
+ "order": 1,
365
+ "name": "Air conditioning"
366
+ },
367
+ {
368
+ "id": "term_wall",
369
+ "taxonomyName": "services",
370
+ "parentId": "term_air_con",
371
+ "order": 1,
372
+ "name": "Wall-mounted"
373
+ },
374
+ {
375
+ "id": "term_outdoors",
376
+ "taxonomyName": "services",
377
+ "parentId": null,
378
+ "order": 2,
379
+ "name": "Outdoors"
380
+ },
381
+ {
382
+ "id": "term_solar",
383
+ "taxonomyName": "services",
384
+ "parentId": "term_outdoors",
385
+ "order": 1,
386
+ "name": "Solar panels"
387
+ }
318
388
  ],
319
- "hasMore": false, "hasPrevious": false, "startingAfter": null, "endingBefore": null
389
+ "hasMore": false,
390
+ "hasPrevious": false,
391
+ "startingAfter": null,
392
+ "endingBefore": null
320
393
  },
321
394
  "responseStatus": { "isSuccess": true }
322
395
  }
@@ -341,10 +414,25 @@ const { data } = useFindTermsQuery({
341
414
  {
342
415
  "list": {
343
416
  "items": [
344
- { "id": "term_indoors", "taxonomyName": "services", "parentId": null, "order": 1, "name": "Indoors" },
345
- { "id": "term_outdoors", "taxonomyName": "services", "parentId": null, "order": 2, "name": "Outdoors" }
417
+ {
418
+ "id": "term_indoors",
419
+ "taxonomyName": "services",
420
+ "parentId": null,
421
+ "order": 1,
422
+ "name": "Indoors"
423
+ },
424
+ {
425
+ "id": "term_outdoors",
426
+ "taxonomyName": "services",
427
+ "parentId": null,
428
+ "order": 2,
429
+ "name": "Outdoors"
430
+ }
346
431
  ],
347
- "hasMore": false, "hasPrevious": false, "startingAfter": null, "endingBefore": null
432
+ "hasMore": false,
433
+ "hasPrevious": false,
434
+ "startingAfter": null,
435
+ "endingBefore": null
348
436
  },
349
437
  "responseStatus": { "isSuccess": true }
350
438
  }
@@ -356,7 +444,7 @@ const { data } = useFindTermsQuery({
356
444
 
357
445
  ### Get a term's children
358
446
 
359
- **Goal:** the user expanded *Indoors* — load what is directly under it.
447
+ **Goal:** the user expanded _Indoors_ — load what is directly under it.
360
448
 
361
449
  ```tsx
362
450
  const { data } = useFindTermsChildrenQuery({
@@ -376,12 +464,17 @@ const { data } = useFindTermsChildrenQuery({
376
464
  "order": 1,
377
465
  "name": "Air conditioning",
378
466
  "multiParents": [
379
- { "taxonomyId": "tax_service_types", "parentId": "term_indoors", "name": "Indoors" },
380
- { "taxonomyId": "tax_service_types", "parentId": "term_energy_efficient", "name": "Energy efficient" }
467
+ { "taxonomyId": "tax_service_types", "parentId": "term_indoors", "name": "Indoors" },
468
+ {
469
+ "taxonomyId": "tax_service_types",
470
+ "parentId": "term_energy_efficient",
471
+ "name": "Energy efficient"
472
+ }
381
473
  ]
382
474
  }
383
475
  ],
384
- "hasMore": false, "hasPrevious": false
476
+ "hasMore": false,
477
+ "hasPrevious": false
385
478
  },
386
479
  "responseStatus": { "isSuccess": true }
387
480
  }
@@ -393,7 +486,7 @@ This returns **both** direct children (their `parentId` is `term_indoors`) **and
393
486
 
394
487
  ### Multi-parent: one product in several categories
395
488
 
396
- **Goal:** in a `products` taxonomy, a *Relaxing massage oil* belongs to *For couples*, *Gift ideas*, **and** *Body care*. Listing the children of **any** of those categories returns it.
489
+ **Goal:** in a `products` taxonomy, a _Relaxing massage oil_ belongs to _For couples_, _Gift ideas_, **and** _Body care_. Listing the children of **any** of those categories returns it.
397
490
 
398
491
  ```tsx
399
492
  const { data } = useFindTermsChildrenQuery({
@@ -412,12 +505,13 @@ const { data } = useFindTermsChildrenQuery({
412
505
  "name": "Relaxing massage oil",
413
506
  "multiParents": [
414
507
  { "taxonomyId": "tax_categories", "parentId": "term_for_couples", "name": "For couples" },
415
- { "taxonomyId": "tax_categories", "parentId": "term_gift_ideas", "name": "Gift ideas" },
416
- { "taxonomyId": "tax_categories", "parentId": "term_body_care", "name": "Body care" }
508
+ { "taxonomyId": "tax_categories", "parentId": "term_gift_ideas", "name": "Gift ideas" },
509
+ { "taxonomyId": "tax_categories", "parentId": "term_body_care", "name": "Body care" }
417
510
  ]
418
511
  }
419
512
  ],
420
- "hasMore": false, "hasPrevious": false
513
+ "hasMore": false,
514
+ "hasPrevious": false
421
515
  },
422
516
  "responseStatus": { "isSuccess": true }
423
517
  }
@@ -447,9 +541,7 @@ const { data } = useFindTermTreeQuery({ taxonomyName: 'services' });
447
541
  "id": "term_air_con",
448
542
  "name": "Air conditioning",
449
543
  "order": 1,
450
- "children": [
451
- { "id": "term_wall", "name": "Wall-mounted", "order": 1, "children": null }
452
- ]
544
+ "children": [{ "id": "term_wall", "name": "Wall-mounted", "order": 1, "children": null }]
453
545
  }
454
546
  ]
455
547
  },
@@ -457,9 +549,7 @@ const { data } = useFindTermTreeQuery({ taxonomyName: 'services' });
457
549
  "id": "term_outdoors",
458
550
  "name": "Outdoors",
459
551
  "order": 2,
460
- "children": [
461
- { "id": "term_solar", "name": "Solar panels", "order": 1, "children": null }
462
- ]
552
+ "children": [{ "id": "term_solar", "name": "Solar panels", "order": 1, "children": null }]
463
553
  }
464
554
  ],
465
555
  "responseStatus": { "isSuccess": true }
@@ -472,7 +562,7 @@ Roots are in `tree`; each node carries its own `children`; a leaf has `children:
472
562
 
473
563
  ### Get only a sub-tree, capped by depth
474
564
 
475
- **Goal:** start from *Indoors* and go at most 2 levels deep.
565
+ **Goal:** start from _Indoors_ and go at most 2 levels deep.
476
566
 
477
567
  ```tsx
478
568
  const { data } = useFindTermTreeQuery({
@@ -498,7 +588,7 @@ const { data } = useFindTermTreeQuery({
498
588
  }
499
589
  ```
500
590
 
501
- With `depth: 2` you get *Indoors* (level 1) and *Air conditioning* (level 2); *Wall-mounted* (level 3) is cut off, so *Air conditioning* shows `children: null`.
591
+ With `depth: 2` you get _Indoors_ (level 1) and _Air conditioning_ (level 2); _Wall-mounted_ (level 3) is cut off, so _Air conditioning_ shows `children: null`.
502
592
 
503
593
  ---
504
594
 
@@ -519,7 +609,14 @@ const { data } = useFindTaxonomyTreeQuery({});
519
609
  "taxonomySlug": "countries",
520
610
  "parentId": null,
521
611
  "children": [
522
- { "viewId": "txn_cities", "taxonomyName": "Cities", "taxonomySlug": "cities", "parentId": "txn_countries", "children": null, "terms": null }
612
+ {
613
+ "viewId": "txn_cities",
614
+ "taxonomyName": "Cities",
615
+ "taxonomySlug": "cities",
616
+ "parentId": "txn_countries",
617
+ "children": null,
618
+ "terms": null
619
+ }
523
620
  ],
524
621
  "terms": null
525
622
  }
@@ -550,7 +647,7 @@ const { data } = useFindTaxonomyTreeQuery({ includeTerms: true });
550
647
  "parentId": null,
551
648
  "terms": [
552
649
  { "id": "term_lt", "name": "Lithuania", "order": 1, "children": null },
553
- { "id": "term_lv", "name": "Latvia", "order": 2, "children": null }
650
+ { "id": "term_lv", "name": "Latvia", "order": 2, "children": null }
554
651
  ],
555
652
  "children": [
556
653
  {
@@ -560,7 +657,7 @@ const { data } = useFindTaxonomyTreeQuery({ includeTerms: true });
560
657
  "parentId": "txn_countries",
561
658
  "terms": [
562
659
  { "id": "term_vilnius", "name": "Vilnius", "order": 1, "children": null },
563
- { "id": "term_kaunas", "name": "Kaunas", "order": 2, "children": null }
660
+ { "id": "term_kaunas", "name": "Kaunas", "order": 2, "children": null }
564
661
  ],
565
662
  "children": null
566
663
  }
@@ -571,7 +668,7 @@ const { data } = useFindTaxonomyTreeQuery({ includeTerms: true });
571
668
  }
572
669
  ```
573
670
 
574
- Now each taxonomy node's `terms` holds that taxonomy's full term tree (same shape as `useFindTermTreeQuery`) — *Countries* carries its countries, *Cities* carries its cities.
671
+ Now each taxonomy node's `terms` holds that taxonomy's full term tree (same shape as `useFindTermTreeQuery`) — _Countries_ carries its countries, _Cities_ carries its cities.
575
672
 
576
673
  > All four hooks also accept the usual RTK Query options (e.g. `{ skip: !ready }`), and an optional `databaseIntegrationId` in the argument to target a non-default database.
577
674
 
@@ -627,7 +724,7 @@ For RTK Query to follow tenant changes, recreate the API slice or pass a tenant-
627
724
 
628
725
  ### Multi-region projects
629
726
 
630
- A Norbix project spans one **primary region** plus any number of **additional regions**. Two concerns, two homes: *which region your requests target* is configured on the wrapped `norbix` client (this package inherits it — every hook just calls the client you passed to `createNorbixApi`), while *which regions a project spans* is managed through the two hooks below.
727
+ A Norbix project spans one **primary region** plus any number of **additional regions**. Two concerns, two homes: _which region your requests target_ is configured on the wrapped `norbix` client (this package inherits it — every hook just calls the client you passed to `createNorbixApi`), while _which regions a project spans_ is managed through the two hooks below.
631
728
 
632
729
  **Configure the target region on the wrapped client.**
633
730
 
@@ -689,7 +786,7 @@ function RegionSettings({ projectId }: { projectId: string }) {
689
786
  ```
690
787
 
691
788
  - `useListRegionsQuery` wraps `norbix.hub.regions.list` and **provides the `Regions` tag**. As with every query hook, a lazy variant — `useLazyListRegionsQuery` — is generated alongside it.
692
- - `useUpdateProjectRegionsMutation` wraps `norbix.hub.regions.updateProjectRegions({ projectId, primaryRegion?, additionalRegions? })` and **invalidates `Regions` and `Projects`** — changing the regions a project spans updates the project DTO (`primaryRegion` / `additionalRegions`), so cached region lists *and* project queries refetch automatically once the mutation resolves.
789
+ - `useUpdateProjectRegionsMutation` wraps `norbix.hub.regions.updateProjectRegions({ projectId, primaryRegion?, additionalRegions? })` and **invalidates `Regions` and `Projects`** — changing the regions a project spans updates the project DTO (`primaryRegion` / `additionalRegions`), so cached region lists _and_ project queries refetch automatically once the mutation resolves.
693
790
 
694
791
  > `hub.account` carries `getAccountRegions` / `updateProjectRegions` aliases for the same wire endpoints (`useGetAccountRegionsQuery` tags `Account`). The `hub.regions` hooks above are the canonical pair — they are the ones wired to the `Regions` tag.
695
792
 
@@ -765,17 +862,17 @@ norbixApi.injectEndpoints({
765
862
 
766
863
  This produces all hooks automatically — `useGetEmailIntegrationsQuery`, `useSaveEmailIntegrationMutation`, `useTestEmailIntegrationMutation`, `useConfirmEmailIntegrationHumanDeliveryMutation`, and the same for push/sms/files/payments/etc.
767
864
 
768
- | Module | Prefix | Namespace | Tag | Notes |
769
- |---|---|---|---|---|
770
- | Email | `Email` | `n.hub.notifications` | `EmailIntegrations` | has `test` + `confirmHumanDelivery` |
771
- | Push | `Push` | `n.hub.notifications` | `PushIntegrations` | has `test` + `confirmHumanDelivery` |
772
- | Sms | `Sms` | `n.hub.notifications` | `SmsIntegrations` | has `test` + `confirmHumanDelivery` |
773
- | Files | `Files` | `n.hub.files` | `FilesIntegrations` | has `test` |
774
- | Payments | `Payments` | `n.hub.payments` | `PaymentIntegrations` | has `test` + `confirmHumanDelivery` |
775
- | Code | `Code` | `n.hub.code` | `CodeIntegrations` | has `test` |
776
- | Membership | `Membership` | `n.hub.membership` | `MembershipIntegrations` | no `test` |
777
- | Logs | `Logging` *(uses `Logging`, not `Logs`)* | `n.hub.logs` | `LogsIntegrations` | no `test` |
778
- | Database | `Database` | `n.hub.database` | `DatabaseIntegrations` | already wired in package |
865
+ | Module | Prefix | Namespace | Tag | Notes |
866
+ | ---------- | ---------------------------------------- | --------------------- | ------------------------ | ----------------------------------- |
867
+ | Email | `Email` | `n.hub.notifications` | `EmailIntegrations` | has `test` + `confirmHumanDelivery` |
868
+ | Push | `Push` | `n.hub.notifications` | `PushIntegrations` | has `test` + `confirmHumanDelivery` |
869
+ | Sms | `Sms` | `n.hub.notifications` | `SmsIntegrations` | has `test` + `confirmHumanDelivery` |
870
+ | Files | `Files` | `n.hub.files` | `FilesIntegrations` | has `test` |
871
+ | Payments | `Payments` | `n.hub.payments` | `PaymentIntegrations` | has `test` + `confirmHumanDelivery` |
872
+ | Code | `Code` | `n.hub.code` | `CodeIntegrations` | has `test` |
873
+ | Membership | `Membership` | `n.hub.membership` | `MembershipIntegrations` | no `test` |
874
+ | Logs | `Logging` _(uses `Logging`, not `Logs`)_ | `n.hub.logs` | `LogsIntegrations` | no `test` |
875
+ | Database | `Database` | `n.hub.database` | `DatabaseIntegrations` | already wired in package |
779
876
 
780
877
  **Trade-off.** The helper is terse but loses static request/response typing — generated hooks return `any` data, because endpoint keys are computed at runtime. If you need full types on a specific integration surface (autocompletion in your IDE), define those endpoints manually instead, following the canonical shape used by `hub.database` integrations in `src/hooks/hub/database.ts`.
781
878
 
@@ -791,9 +888,10 @@ import type { useNorbix } from '@norbix.ai/react-redux';
791
888
  export const myCampaignsService = norbixApi.injectEndpoints({
792
889
  endpoints: (builder) => ({
793
890
  getEmailCampaigns: builder.query({
794
- query: (args) => (norbix) => norbix.hub.notifications
795
- ? norbix.hub.notifications.getEmailTemplates(args) // example only
796
- : Promise.resolve({}),
891
+ query: (args) => (norbix) =>
892
+ norbix.hub.notifications
893
+ ? norbix.hub.notifications.getEmailTemplates(args) // example only
894
+ : Promise.resolve({}),
797
895
  providesTags: [{ type: 'EmailCampaigns', id: 'LIST' }],
798
896
  }),
799
897
  // ...more app-specific endpoints