@norbix.ai/react-redux 1.2.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();
@@ -239,16 +279,52 @@ the storage, and a failed delete step leaves its file behind — and
239
279
  `FilesIntegrations`, because the gateway records the last test result on the
240
280
  integration.
241
281
 
282
+ ## SMS
283
+
284
+ Every SMS Hub endpoint (35) has a hook, 1:1 with `norbix.hub.notifications`.
285
+ Queries provide a tag, mutations invalidate it, so a list re-reads after a
286
+ write without any manual refetch:
287
+
288
+ | Area | Hooks | Cache tag |
289
+ |---|---|---|
290
+ | Module switch | `useEnableSmsMutation`, `useDisableSmsMutation`, `useGetSmsDisableDependenciesQuery` (what a switch-off would affect — read it before disabling), `useGetSmsSettingsQuery` | `Sms`, `SmsSettings` |
291
+ | Integrations (providers) | `useGetSmsIntegrationsQuery`, `useGetSmsIntegrationQuery`, `useSaveSmsIntegrationMutation`, `useTestSmsIntegrationMutation`, `useConfirmSmsIntegrationHumanDeliveryMutation`, `useDeleteSmsIntegrationMutation`, `useSetSmsIntegrationAsDefaultMutation`, `useEnableSmsIntegrationMutation`, `useDisableSmsIntegrationMutation` | `SmsIntegrations` |
292
+ | Templates | `useGetSmsTemplatesQuery`, `useGetSmsTemplateQuery`, `useCreateSmsTemplateMutation`, `useUpdateSmsTemplateMutation`, `useDeleteSmsTemplateMutation`, `useArchiveSmsTemplateMutation`, `useUnArchiveSmsTemplateMutation`, `useCloneSmsTemplateMutation`, `useGetSmsMessageContentTokensQuery`, `useRenderSmsMutation` | `SmsTemplates`, `Sms` |
293
+ | Campaigns | `useGetSmsCampaignsQuery`, `useCreateSmsCampaignMutation`, `useGetSmsCampaignQuery`, `useDeleteSmsCampaignMutation`, `useStopSmsCampaignMutation`, `useGetSmsCampaignStatisticsQuery`, `useGetSmsCampaignBatchesQuery`, `useGetSmsCampaignBatchNotificationsQuery`, `useGetSmsCampaignBatchNotificationQuery`, `useGetSmsCampaignMessagesQuery`, `useGetSmsCampaignMessageQuery` | `SmsCampaigns` |
294
+ | Preview | `usePreviewSmsNotificationQuery` (opens with the signed link `hash`, no sign-in — see above) | `Sms` |
295
+
296
+ ```tsx
297
+ const [stop, { isLoading }] = useStopSmsCampaignMutation();
298
+ const [render] = useRenderSmsMutation();
299
+
300
+ // Stop a running campaign — no further messages go out; this cannot be undone.
301
+ await stop({ id: 'cmp_1' });
302
+
303
+ // Render a template's Razor code with token values before saving it.
304
+ const { data } = await render({
305
+ code: 'Hi @Model.FirstName, your code is @Model.Code',
306
+ tokens: [
307
+ { name: 'FirstName', value: 'Ada' },
308
+ { name: 'Code', value: '4242' },
309
+ ],
310
+ });
311
+ ```
312
+
313
+ `useRenderSmsMutation` saves nothing on the server, so it invalidates nothing;
314
+ it is a mutation only so a render is never served from the cache. The
315
+ integration hooks can also be produced by `buildIntegrationsEndpoints` (next
316
+ section) — the shipped ones above are the typed variants.
317
+
242
318
  ## Working with terms
243
319
 
244
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:
245
321
 
246
- | I want to… | Hook | `data` holds |
247
- | --- | --- | --- |
248
- | Get a taxonomy's terms as a flat list | `useFindTermsQuery` | a paginated `list` of terms |
249
- | Get only the children of one term | `useFindTermsChildrenQuery` | a `list` of child terms (direct + multi-parent) |
250
- | Get a taxonomy's terms as a ready-made tree | `useFindTermTreeQuery` | a `tree` of nested term nodes |
251
- | 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 |
252
328
 
253
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:
254
330
 
@@ -274,13 +350,46 @@ const { data, isLoading } = useFindTermsQuery({ taxonomyName: 'services' });
274
350
  {
275
351
  "list": {
276
352
  "items": [
277
- { "id": "term_indoors", "taxonomyName": "services", "parentId": null, "order": 1, "name": "Indoors" },
278
- { "id": "term_air_con", "taxonomyName": "services", "parentId": "term_indoors", "order": 1, "name": "Air conditioning" },
279
- { "id": "term_wall", "taxonomyName": "services", "parentId": "term_air_con", "order": 1, "name": "Wall-mounted" },
280
- { "id": "term_outdoors", "taxonomyName": "services", "parentId": null, "order": 2, "name": "Outdoors" },
281
- { "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
+ }
282
388
  ],
283
- "hasMore": false, "hasPrevious": false, "startingAfter": null, "endingBefore": null
389
+ "hasMore": false,
390
+ "hasPrevious": false,
391
+ "startingAfter": null,
392
+ "endingBefore": null
284
393
  },
285
394
  "responseStatus": { "isSuccess": true }
286
395
  }
@@ -305,10 +414,25 @@ const { data } = useFindTermsQuery({
305
414
  {
306
415
  "list": {
307
416
  "items": [
308
- { "id": "term_indoors", "taxonomyName": "services", "parentId": null, "order": 1, "name": "Indoors" },
309
- { "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
+ }
310
431
  ],
311
- "hasMore": false, "hasPrevious": false, "startingAfter": null, "endingBefore": null
432
+ "hasMore": false,
433
+ "hasPrevious": false,
434
+ "startingAfter": null,
435
+ "endingBefore": null
312
436
  },
313
437
  "responseStatus": { "isSuccess": true }
314
438
  }
@@ -320,7 +444,7 @@ const { data } = useFindTermsQuery({
320
444
 
321
445
  ### Get a term's children
322
446
 
323
- **Goal:** the user expanded *Indoors* — load what is directly under it.
447
+ **Goal:** the user expanded _Indoors_ — load what is directly under it.
324
448
 
325
449
  ```tsx
326
450
  const { data } = useFindTermsChildrenQuery({
@@ -340,12 +464,17 @@ const { data } = useFindTermsChildrenQuery({
340
464
  "order": 1,
341
465
  "name": "Air conditioning",
342
466
  "multiParents": [
343
- { "taxonomyId": "tax_service_types", "parentId": "term_indoors", "name": "Indoors" },
344
- { "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
+ }
345
473
  ]
346
474
  }
347
475
  ],
348
- "hasMore": false, "hasPrevious": false
476
+ "hasMore": false,
477
+ "hasPrevious": false
349
478
  },
350
479
  "responseStatus": { "isSuccess": true }
351
480
  }
@@ -357,7 +486,7 @@ This returns **both** direct children (their `parentId` is `term_indoors`) **and
357
486
 
358
487
  ### Multi-parent: one product in several categories
359
488
 
360
- **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.
361
490
 
362
491
  ```tsx
363
492
  const { data } = useFindTermsChildrenQuery({
@@ -376,12 +505,13 @@ const { data } = useFindTermsChildrenQuery({
376
505
  "name": "Relaxing massage oil",
377
506
  "multiParents": [
378
507
  { "taxonomyId": "tax_categories", "parentId": "term_for_couples", "name": "For couples" },
379
- { "taxonomyId": "tax_categories", "parentId": "term_gift_ideas", "name": "Gift ideas" },
380
- { "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" }
381
510
  ]
382
511
  }
383
512
  ],
384
- "hasMore": false, "hasPrevious": false
513
+ "hasMore": false,
514
+ "hasPrevious": false
385
515
  },
386
516
  "responseStatus": { "isSuccess": true }
387
517
  }
@@ -411,9 +541,7 @@ const { data } = useFindTermTreeQuery({ taxonomyName: 'services' });
411
541
  "id": "term_air_con",
412
542
  "name": "Air conditioning",
413
543
  "order": 1,
414
- "children": [
415
- { "id": "term_wall", "name": "Wall-mounted", "order": 1, "children": null }
416
- ]
544
+ "children": [{ "id": "term_wall", "name": "Wall-mounted", "order": 1, "children": null }]
417
545
  }
418
546
  ]
419
547
  },
@@ -421,9 +549,7 @@ const { data } = useFindTermTreeQuery({ taxonomyName: 'services' });
421
549
  "id": "term_outdoors",
422
550
  "name": "Outdoors",
423
551
  "order": 2,
424
- "children": [
425
- { "id": "term_solar", "name": "Solar panels", "order": 1, "children": null }
426
- ]
552
+ "children": [{ "id": "term_solar", "name": "Solar panels", "order": 1, "children": null }]
427
553
  }
428
554
  ],
429
555
  "responseStatus": { "isSuccess": true }
@@ -436,7 +562,7 @@ Roots are in `tree`; each node carries its own `children`; a leaf has `children:
436
562
 
437
563
  ### Get only a sub-tree, capped by depth
438
564
 
439
- **Goal:** start from *Indoors* and go at most 2 levels deep.
565
+ **Goal:** start from _Indoors_ and go at most 2 levels deep.
440
566
 
441
567
  ```tsx
442
568
  const { data } = useFindTermTreeQuery({
@@ -462,7 +588,7 @@ const { data } = useFindTermTreeQuery({
462
588
  }
463
589
  ```
464
590
 
465
- 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`.
466
592
 
467
593
  ---
468
594
 
@@ -483,7 +609,14 @@ const { data } = useFindTaxonomyTreeQuery({});
483
609
  "taxonomySlug": "countries",
484
610
  "parentId": null,
485
611
  "children": [
486
- { "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
+ }
487
620
  ],
488
621
  "terms": null
489
622
  }
@@ -514,7 +647,7 @@ const { data } = useFindTaxonomyTreeQuery({ includeTerms: true });
514
647
  "parentId": null,
515
648
  "terms": [
516
649
  { "id": "term_lt", "name": "Lithuania", "order": 1, "children": null },
517
- { "id": "term_lv", "name": "Latvia", "order": 2, "children": null }
650
+ { "id": "term_lv", "name": "Latvia", "order": 2, "children": null }
518
651
  ],
519
652
  "children": [
520
653
  {
@@ -524,7 +657,7 @@ const { data } = useFindTaxonomyTreeQuery({ includeTerms: true });
524
657
  "parentId": "txn_countries",
525
658
  "terms": [
526
659
  { "id": "term_vilnius", "name": "Vilnius", "order": 1, "children": null },
527
- { "id": "term_kaunas", "name": "Kaunas", "order": 2, "children": null }
660
+ { "id": "term_kaunas", "name": "Kaunas", "order": 2, "children": null }
528
661
  ],
529
662
  "children": null
530
663
  }
@@ -535,7 +668,7 @@ const { data } = useFindTaxonomyTreeQuery({ includeTerms: true });
535
668
  }
536
669
  ```
537
670
 
538
- 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.
539
672
 
540
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.
541
674
 
@@ -591,7 +724,7 @@ For RTK Query to follow tenant changes, recreate the API slice or pass a tenant-
591
724
 
592
725
  ### Multi-region projects
593
726
 
594
- 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.
595
728
 
596
729
  **Configure the target region on the wrapped client.**
597
730
 
@@ -653,7 +786,7 @@ function RegionSettings({ projectId }: { projectId: string }) {
653
786
  ```
654
787
 
655
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.
656
- - `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.
657
790
 
658
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.
659
792
 
@@ -729,17 +862,17 @@ norbixApi.injectEndpoints({
729
862
 
730
863
  This produces all hooks automatically — `useGetEmailIntegrationsQuery`, `useSaveEmailIntegrationMutation`, `useTestEmailIntegrationMutation`, `useConfirmEmailIntegrationHumanDeliveryMutation`, and the same for push/sms/files/payments/etc.
731
864
 
732
- | Module | Prefix | Namespace | Tag | Notes |
733
- |---|---|---|---|---|
734
- | Email | `Email` | `n.hub.notifications` | `EmailIntegrations` | has `test` + `confirmHumanDelivery` |
735
- | Push | `Push` | `n.hub.notifications` | `PushIntegrations` | has `test` + `confirmHumanDelivery` |
736
- | Sms | `Sms` | `n.hub.notifications` | `SmsIntegrations` | has `test` + `confirmHumanDelivery` |
737
- | Files | `Files` | `n.hub.files` | `FilesIntegrations` | has `test` |
738
- | Payments | `Payments` | `n.hub.payments` | `PaymentIntegrations` | has `test` + `confirmHumanDelivery` |
739
- | Code | `Code` | `n.hub.code` | `CodeIntegrations` | has `test` |
740
- | Membership | `Membership` | `n.hub.membership` | `MembershipIntegrations` | no `test` |
741
- | Logs | `Logging` *(uses `Logging`, not `Logs`)* | `n.hub.logs` | `LogsIntegrations` | no `test` |
742
- | 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 |
743
876
 
744
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`.
745
878
 
@@ -755,9 +888,10 @@ import type { useNorbix } from '@norbix.ai/react-redux';
755
888
  export const myCampaignsService = norbixApi.injectEndpoints({
756
889
  endpoints: (builder) => ({
757
890
  getEmailCampaigns: builder.query({
758
- query: (args) => (norbix) => norbix.hub.notifications
759
- ? norbix.hub.notifications.getEmailTemplates(args) // example only
760
- : Promise.resolve({}),
891
+ query: (args) => (norbix) =>
892
+ norbix.hub.notifications
893
+ ? norbix.hub.notifications.getEmailTemplates(args) // example only
894
+ : Promise.resolve({}),
761
895
  providesTags: [{ type: 'EmailCampaigns', id: 'LIST' }],
762
896
  }),
763
897
  // ...more app-specific endpoints