@norbix.ai/react-redux 0.1.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/LICENSE +21 -0
- package/README.md +825 -0
- package/dist/index.cjs +2050 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +654 -0
- package/dist/index.d.ts +654 -0
- package/dist/index.js +2042 -0
- package/dist/index.js.map +1 -0
- package/package.json +107 -0
package/README.md
ADDED
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
<!-- BEGIN: HERO -->
|
|
2
|
+
<p align="center">
|
|
3
|
+
<a href="https://norbix.ai">
|
|
4
|
+
<img src="https://norbix.ai/brand/wordmark.svg" alt="Norbix" height="64" />
|
|
5
|
+
</a>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<div align="center">
|
|
9
|
+
<h1>Norbix React + Redux Toolkit</h1>
|
|
10
|
+
<p><strong>RTK Query helpers over the typed Norbix SDK — cache, dedup, refetch, invalidation.</strong></p>
|
|
11
|
+
|
|
12
|
+
<p>
|
|
13
|
+
<a href="https://github.com/norbix-code/react-redux/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/badge/license-MIT-blue.svg" /></a>
|
|
14
|
+
<a href="https://github.com/norbix-code/react-redux/actions"><img alt="CI" src="https://github.com/norbix-code/react-redux/actions/workflows/ci.yml/badge.svg" /></a>
|
|
15
|
+
<a href="https://www.npmjs.com/package/@norbix.ai/react-redux"><img alt="@norbix.ai/react-redux" src="https://img.shields.io/npm/v/@norbix.ai/react-redux.svg?label=@norbix.ai/react-redux&logo=npm" /></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<hr />
|
|
19
|
+
</div>
|
|
20
|
+
<!-- END: HERO -->
|
|
21
|
+
|
|
22
|
+
## What this is
|
|
23
|
+
|
|
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
|
+
|
|
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
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npm install @norbix.ai/react-redux @norbix.ai/ts @reduxjs/toolkit react-redux react
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
Three steps: create the API slice, plug it into your store, mount the provider.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// src/norbix.ts
|
|
44
|
+
import { Norbix } from '@norbix.ai/ts';
|
|
45
|
+
import { createNorbixApi } from '@norbix.ai/react-redux';
|
|
46
|
+
|
|
47
|
+
export const norbix = new Norbix(); // reads env vars, or pass { apiKey, projectId }
|
|
48
|
+
|
|
49
|
+
export const norbixApi = createNorbixApi(() => norbix);
|
|
50
|
+
|
|
51
|
+
export const {
|
|
52
|
+
useGetUsersQuery,
|
|
53
|
+
useInviteUserMutation,
|
|
54
|
+
useFindCollectionQuery,
|
|
55
|
+
useInsertOneMutation,
|
|
56
|
+
useUpdateOneMutation,
|
|
57
|
+
useDeleteOneMutation,
|
|
58
|
+
useGetAccountProfileQuery,
|
|
59
|
+
useGetDatabaseSchemasQuery,
|
|
60
|
+
useLoginMutation,
|
|
61
|
+
} = norbixApi;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// src/store.ts
|
|
66
|
+
import { configureStore } from '@reduxjs/toolkit';
|
|
67
|
+
import { norbixApi } from './norbix';
|
|
68
|
+
|
|
69
|
+
export const store = configureStore({
|
|
70
|
+
reducer: { [norbixApi.reducerPath]: norbixApi.reducer },
|
|
71
|
+
middleware: (getDefault) => getDefault().concat(norbixApi.middleware),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export type RootState = ReturnType<typeof store.getState>;
|
|
75
|
+
export type AppDispatch = typeof store.dispatch;
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
// src/main.tsx
|
|
80
|
+
import { Provider } from 'react-redux';
|
|
81
|
+
import { NorbixProvider } from '@norbix.ai/react-redux';
|
|
82
|
+
import { store } from './store';
|
|
83
|
+
import { norbix } from './norbix';
|
|
84
|
+
|
|
85
|
+
createRoot(document.getElementById('root')!).render(
|
|
86
|
+
<Provider store={store}>
|
|
87
|
+
<NorbixProvider client={norbix}>
|
|
88
|
+
<App />
|
|
89
|
+
</NorbixProvider>
|
|
90
|
+
</Provider>,
|
|
91
|
+
);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// src/UsersList.tsx
|
|
96
|
+
import { useGetUsersQuery, useInviteUserMutation } from './norbix';
|
|
97
|
+
|
|
98
|
+
export function UsersList() {
|
|
99
|
+
const { data, isLoading, error } = useGetUsersQuery({ take: 20, skip: 0 });
|
|
100
|
+
const [invite, { isLoading: inviting }] = useInviteUserMutation();
|
|
101
|
+
|
|
102
|
+
if (isLoading) return <p>Loading…</p>;
|
|
103
|
+
if (error) return <p>Error: {error.message}</p>;
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<ul>
|
|
107
|
+
{data?.users?.map((u) => <li key={u.id}>{u.email}</li>)}
|
|
108
|
+
<button
|
|
109
|
+
disabled={inviting}
|
|
110
|
+
onClick={() => invite({ email: 'maya@team.io', roleIds: [] })}
|
|
111
|
+
>
|
|
112
|
+
Invite
|
|
113
|
+
</button>
|
|
114
|
+
</ul>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
After `invite()` resolves, the `User/LIST` tag is invalidated and `useGetUsersQuery` automatically refetches — no manual cache busting needed.
|
|
120
|
+
|
|
121
|
+
## What ships in the box
|
|
122
|
+
|
|
123
|
+
A *curated* set of hooks for the most common endpoints. Every hook is end-to-end typed from the SDK's own DTOs.
|
|
124
|
+
|
|
125
|
+
### API surface — `norbix.api.*`
|
|
126
|
+
|
|
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
|
+
|
|
154
|
+
### Hub surface — `norbix.hub.*`
|
|
155
|
+
|
|
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` |
|
|
183
|
+
|
|
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.
|
|
185
|
+
|
|
186
|
+
## Public file links
|
|
187
|
+
|
|
188
|
+
A file, or a whole folder prefix, can be made readable by anyone holding its
|
|
189
|
+
link. Publishing is a Hub action and needs the signed-in client; *reading* the
|
|
190
|
+
link needs nothing at all.
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
const [publish] = useMakeFilePublicMutation();
|
|
194
|
+
const [unpublish] = useMakeFilePrivateMutation();
|
|
195
|
+
|
|
196
|
+
await publish({ filesIntegrationId: 'nbin_1', path: 'docs/invoice.pdf' });
|
|
197
|
+
|
|
198
|
+
// `name` is the file's name for a file link, or the path inside the folder
|
|
199
|
+
// for a folder link — its slashes stay slashes.
|
|
200
|
+
const { data: bytes } = useGetPublicFileQuery({
|
|
201
|
+
publicId: 'nbpf_abc',
|
|
202
|
+
name: '2026/q1/report.pdf',
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`useMakeFilePrivateMutation` is refused while a folder above the file is
|
|
207
|
+
public — switch the folder off with `useMakeFolderPrivateMutation` instead.
|
|
208
|
+
|
|
209
|
+
The four publish hooks invalidate the `Files` tag, so a listing re-reads and
|
|
210
|
+
picks up the new `isPublic` / `publicUrl` on each file and the `publicFolders`
|
|
211
|
+
on the page. `useGetPublicFileQuery` caches under its own
|
|
212
|
+
`Files/PUBLIC:<publicId>` id, so two different links never share one entry;
|
|
213
|
+
making a file private again invalidates `Files`, a different tag, so call
|
|
214
|
+
`refetch()` if a stale entry would matter to you.
|
|
215
|
+
|
|
216
|
+
## Working with terms
|
|
217
|
+
|
|
218
|
+
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:
|
|
219
|
+
|
|
220
|
+
| I want to… | Hook | `data` holds |
|
|
221
|
+
| --- | --- | --- |
|
|
222
|
+
| Get a taxonomy's terms as a flat list | `useFindTermsQuery` | a paginated `list` of terms |
|
|
223
|
+
| Get only the children of one term | `useFindTermsChildrenQuery` | a `list` of child terms (direct + multi-parent) |
|
|
224
|
+
| Get a taxonomy's terms as a ready-made tree | `useFindTermTreeQuery` | a `tree` of nested term nodes |
|
|
225
|
+
| Get the taxonomy structure (e.g. Countries → Cities) | `useFindTaxonomyTreeQuery` | a `tree` of taxonomy nodes |
|
|
226
|
+
|
|
227
|
+
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:
|
|
228
|
+
|
|
229
|
+
```text
|
|
230
|
+
Indoors
|
|
231
|
+
└─ Air conditioning
|
|
232
|
+
└─ Wall-mounted
|
|
233
|
+
Outdoors
|
|
234
|
+
└─ Solar panels
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
### List a taxonomy's terms (flat)
|
|
240
|
+
|
|
241
|
+
**Goal:** show every term of `services` in a simple list, in display order.
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
const { data, isLoading } = useFindTermsQuery({ taxonomyName: 'services' });
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
```json
|
|
248
|
+
{
|
|
249
|
+
"list": {
|
|
250
|
+
"items": [
|
|
251
|
+
{ "id": "term_indoors", "taxonomyName": "services", "parentId": null, "order": 1, "name": "Indoors" },
|
|
252
|
+
{ "id": "term_air_con", "taxonomyName": "services", "parentId": "term_indoors", "order": 1, "name": "Air conditioning" },
|
|
253
|
+
{ "id": "term_wall", "taxonomyName": "services", "parentId": "term_air_con", "order": 1, "name": "Wall-mounted" },
|
|
254
|
+
{ "id": "term_outdoors", "taxonomyName": "services", "parentId": null, "order": 2, "name": "Outdoors" },
|
|
255
|
+
{ "id": "term_solar", "taxonomyName": "services", "parentId": "term_outdoors","order": 1, "name": "Solar panels" }
|
|
256
|
+
],
|
|
257
|
+
"hasMore": false, "hasPrevious": false, "startingAfter": null, "endingBefore": null
|
|
258
|
+
},
|
|
259
|
+
"responseStatus": { "isSuccess": true }
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The list is flat — every term is one row, with its `parentId` telling you where it sits. The nesting is not built for you here (use `useFindTermTreeQuery` for that).
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### List only top-level terms (filtered)
|
|
268
|
+
|
|
269
|
+
**Goal:** show just the roots (no parent) — for the first level of a menu.
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
const { data } = useFindTermsQuery({
|
|
273
|
+
taxonomyName: 'services',
|
|
274
|
+
filter: '{ "parentId": null }',
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
{
|
|
280
|
+
"list": {
|
|
281
|
+
"items": [
|
|
282
|
+
{ "id": "term_indoors", "taxonomyName": "services", "parentId": null, "order": 1, "name": "Indoors" },
|
|
283
|
+
{ "id": "term_outdoors", "taxonomyName": "services", "parentId": null, "order": 2, "name": "Outdoors" }
|
|
284
|
+
],
|
|
285
|
+
"hasMore": false, "hasPrevious": false, "startingAfter": null, "endingBefore": null
|
|
286
|
+
},
|
|
287
|
+
"responseStatus": { "isSuccess": true }
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
`filter` is an optional MongoDB filter, ANDed with the taxonomy. Use it to fetch one level at a time (lazy tree loading) or to find terms by any field.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
### Get a term's children
|
|
296
|
+
|
|
297
|
+
**Goal:** the user expanded *Indoors* — load what is directly under it.
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
const { data } = useFindTermsChildrenQuery({
|
|
301
|
+
taxonomyName: 'services',
|
|
302
|
+
parentId: 'term_indoors',
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
```json
|
|
307
|
+
{
|
|
308
|
+
"list": {
|
|
309
|
+
"items": [
|
|
310
|
+
{
|
|
311
|
+
"id": "term_air_con",
|
|
312
|
+
"taxonomyName": "services",
|
|
313
|
+
"parentId": "term_indoors",
|
|
314
|
+
"order": 1,
|
|
315
|
+
"name": "Air conditioning",
|
|
316
|
+
"multiParents": [
|
|
317
|
+
{ "taxonomyId": "tax_service_types", "parentId": "term_indoors", "name": "Indoors" },
|
|
318
|
+
{ "taxonomyId": "tax_service_types", "parentId": "term_energy_efficient", "name": "Energy efficient" }
|
|
319
|
+
]
|
|
320
|
+
}
|
|
321
|
+
],
|
|
322
|
+
"hasMore": false, "hasPrevious": false
|
|
323
|
+
},
|
|
324
|
+
"responseStatus": { "isSuccess": true }
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
This returns **both** direct children (their `parentId` is `term_indoors`) **and** multi-parent children (terms that list `term_indoors` in `multiParents`). Parent names are already resolved, so no second lookup.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
### Multi-parent: one product in several categories
|
|
333
|
+
|
|
334
|
+
**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.
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
const { data } = useFindTermsChildrenQuery({
|
|
338
|
+
taxonomyName: 'products',
|
|
339
|
+
parentId: 'term_gift_ideas',
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
```json
|
|
344
|
+
{
|
|
345
|
+
"list": {
|
|
346
|
+
"items": [
|
|
347
|
+
{
|
|
348
|
+
"id": "term_relaxing_oil",
|
|
349
|
+
"taxonomyName": "products",
|
|
350
|
+
"name": "Relaxing massage oil",
|
|
351
|
+
"multiParents": [
|
|
352
|
+
{ "taxonomyId": "tax_categories", "parentId": "term_for_couples", "name": "For couples" },
|
|
353
|
+
{ "taxonomyId": "tax_categories", "parentId": "term_gift_ideas", "name": "Gift ideas" },
|
|
354
|
+
{ "taxonomyId": "tax_categories", "parentId": "term_body_care", "name": "Body care" }
|
|
355
|
+
]
|
|
356
|
+
}
|
|
357
|
+
],
|
|
358
|
+
"hasMore": false, "hasPrevious": false
|
|
359
|
+
},
|
|
360
|
+
"responseStatus": { "isSuccess": true }
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
One product, three category links — no duplicate listings. The same product would also come back from the children of `term_for_couples` and `term_body_care`.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
### Get the whole term tree in one call
|
|
369
|
+
|
|
370
|
+
**Goal:** render the full `services` tree at once, already nested.
|
|
371
|
+
|
|
372
|
+
```tsx
|
|
373
|
+
const { data } = useFindTermTreeQuery({ taxonomyName: 'services' });
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
```json
|
|
377
|
+
{
|
|
378
|
+
"tree": [
|
|
379
|
+
{
|
|
380
|
+
"id": "term_indoors",
|
|
381
|
+
"name": "Indoors",
|
|
382
|
+
"order": 1,
|
|
383
|
+
"children": [
|
|
384
|
+
{
|
|
385
|
+
"id": "term_air_con",
|
|
386
|
+
"name": "Air conditioning",
|
|
387
|
+
"order": 1,
|
|
388
|
+
"children": [
|
|
389
|
+
{ "id": "term_wall", "name": "Wall-mounted", "order": 1, "children": null }
|
|
390
|
+
]
|
|
391
|
+
}
|
|
392
|
+
]
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
"id": "term_outdoors",
|
|
396
|
+
"name": "Outdoors",
|
|
397
|
+
"order": 2,
|
|
398
|
+
"children": [
|
|
399
|
+
{ "id": "term_solar", "name": "Solar panels", "order": 1, "children": null }
|
|
400
|
+
]
|
|
401
|
+
}
|
|
402
|
+
],
|
|
403
|
+
"responseStatus": { "isSuccess": true }
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Roots are in `tree`; each node carries its own `children`; a leaf has `children: null`. The tree arrives ready to render — no client-side tree building.
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
### Get only a sub-tree, capped by depth
|
|
412
|
+
|
|
413
|
+
**Goal:** start from *Indoors* and go at most 2 levels deep.
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
const { data } = useFindTermTreeQuery({
|
|
417
|
+
taxonomyName: 'services',
|
|
418
|
+
rootTermId: 'term_indoors',
|
|
419
|
+
depth: 2,
|
|
420
|
+
});
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
```json
|
|
424
|
+
{
|
|
425
|
+
"tree": [
|
|
426
|
+
{
|
|
427
|
+
"id": "term_indoors",
|
|
428
|
+
"name": "Indoors",
|
|
429
|
+
"order": 1,
|
|
430
|
+
"children": [
|
|
431
|
+
{ "id": "term_air_con", "name": "Air conditioning", "order": 1, "children": null }
|
|
432
|
+
]
|
|
433
|
+
}
|
|
434
|
+
],
|
|
435
|
+
"responseStatus": { "isSuccess": true }
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
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`.
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
### Get the taxonomy structure tree — without terms
|
|
444
|
+
|
|
445
|
+
**Goal:** see how taxonomies relate to each other (e.g. a `Cities` taxonomy whose parent is `Countries`), structure only.
|
|
446
|
+
|
|
447
|
+
```tsx
|
|
448
|
+
const { data } = useFindTaxonomyTreeQuery({});
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
```json
|
|
452
|
+
{
|
|
453
|
+
"tree": [
|
|
454
|
+
{
|
|
455
|
+
"viewId": "txn_countries",
|
|
456
|
+
"taxonomyName": "Countries",
|
|
457
|
+
"taxonomySlug": "countries",
|
|
458
|
+
"parentId": null,
|
|
459
|
+
"children": [
|
|
460
|
+
{ "viewId": "txn_cities", "taxonomyName": "Cities", "taxonomySlug": "cities", "parentId": "txn_countries", "children": null, "terms": null }
|
|
461
|
+
],
|
|
462
|
+
"terms": null
|
|
463
|
+
}
|
|
464
|
+
],
|
|
465
|
+
"responseStatus": { "isSuccess": true }
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
This is the **taxonomy** tree, not the term tree: nodes are taxonomies. Every `terms` is `null` because we did not ask for terms.
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
### Get the taxonomy structure tree — with terms
|
|
474
|
+
|
|
475
|
+
**Goal:** same structure, but also pull each taxonomy's terms in the same call.
|
|
476
|
+
|
|
477
|
+
```tsx
|
|
478
|
+
const { data } = useFindTaxonomyTreeQuery({ includeTerms: true });
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
```json
|
|
482
|
+
{
|
|
483
|
+
"tree": [
|
|
484
|
+
{
|
|
485
|
+
"viewId": "txn_countries",
|
|
486
|
+
"taxonomyName": "Countries",
|
|
487
|
+
"taxonomySlug": "countries",
|
|
488
|
+
"parentId": null,
|
|
489
|
+
"terms": [
|
|
490
|
+
{ "id": "term_lt", "name": "Lithuania", "order": 1, "children": null },
|
|
491
|
+
{ "id": "term_lv", "name": "Latvia", "order": 2, "children": null }
|
|
492
|
+
],
|
|
493
|
+
"children": [
|
|
494
|
+
{
|
|
495
|
+
"viewId": "txn_cities",
|
|
496
|
+
"taxonomyName": "Cities",
|
|
497
|
+
"taxonomySlug": "cities",
|
|
498
|
+
"parentId": "txn_countries",
|
|
499
|
+
"terms": [
|
|
500
|
+
{ "id": "term_vilnius", "name": "Vilnius", "order": 1, "children": null },
|
|
501
|
+
{ "id": "term_kaunas", "name": "Kaunas", "order": 2, "children": null }
|
|
502
|
+
],
|
|
503
|
+
"children": null
|
|
504
|
+
}
|
|
505
|
+
]
|
|
506
|
+
}
|
|
507
|
+
],
|
|
508
|
+
"responseStatus": { "isSuccess": true }
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
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.
|
|
513
|
+
|
|
514
|
+
> 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.
|
|
515
|
+
|
|
516
|
+
## Common patterns
|
|
517
|
+
|
|
518
|
+
### Skip a query until ready
|
|
519
|
+
|
|
520
|
+
```tsx
|
|
521
|
+
const projectId = useSelector(selectCurrentProjectId);
|
|
522
|
+
const { data } = useGetProjectQuery({ id: projectId }, { skip: !projectId });
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Reset the entire cache after login or tenant switch
|
|
526
|
+
|
|
527
|
+
```tsx
|
|
528
|
+
const dispatch = useDispatch();
|
|
529
|
+
const [login] = useLoginMutation();
|
|
530
|
+
|
|
531
|
+
async function handleLogin(creds) {
|
|
532
|
+
await login(creds).unwrap();
|
|
533
|
+
// The login mutation already invalidates the most-affected tags.
|
|
534
|
+
// For a hard reset across every cached endpoint, also do:
|
|
535
|
+
dispatch(norbixApi.util.resetApiState());
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Use the SDK directly when RTK Query is overkill
|
|
540
|
+
|
|
541
|
+
```tsx
|
|
542
|
+
function ApiVersionBadge() {
|
|
543
|
+
const norbix = useNorbix();
|
|
544
|
+
const [v, setV] = useState<string>();
|
|
545
|
+
useEffect(() => {
|
|
546
|
+
norbix.api.echo.echo({}).then((r) => setV(r.gatewayVersion));
|
|
547
|
+
}, [norbix]);
|
|
548
|
+
return <small>API {v}</small>;
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Per-request scoping (SSR, multi-tenant)
|
|
553
|
+
|
|
554
|
+
```tsx
|
|
555
|
+
const tenantNorbix = useMemo(() => norbix.with({ projectId, accountId }), [projectId, accountId]);
|
|
556
|
+
|
|
557
|
+
return (
|
|
558
|
+
<NorbixProvider client={tenantNorbix}>
|
|
559
|
+
<Workspace />
|
|
560
|
+
</NorbixProvider>
|
|
561
|
+
);
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
For RTK Query to follow tenant changes, recreate the API slice or pass a tenant-aware `getClient` resolver to `createNorbixApi`.
|
|
565
|
+
|
|
566
|
+
### Multi-region projects
|
|
567
|
+
|
|
568
|
+
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.
|
|
569
|
+
|
|
570
|
+
**Configure the target region on the wrapped client.**
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
// 1. At construction
|
|
574
|
+
const norbix = new Norbix({ region: 'nb-eu-germany' });
|
|
575
|
+
|
|
576
|
+
// 2. Via environment — `new Norbix()` picks it up automatically
|
|
577
|
+
// NORBIX_REGION=nb-eu-germany
|
|
578
|
+
|
|
579
|
+
// 3. At runtime
|
|
580
|
+
norbix.setRegion('nb-eu-germany'); // all subsequent requests
|
|
581
|
+
norbix.setRegion(undefined); // clear — the backend uses the project's primary region
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
When a region is set, every request carries the `nb-region` header, and the client itself composes the regional base URL (`https://nb-eu-germany.api.norbix.ai`) — but only when it is using the SDK's default base URLs; a custom `baseUrl` is never rewritten. There is no default region: with nothing set, no header is sent and the backend picks the project's primary region. The underlying SDK also accepts a per-call override (`norbix.hub.regions.list({}, { region: 'nb-eu-germany' })`); the shipped hooks don't expose that option, so for a one-off cross-region call drop down to the SDK via `useNorbix()`.
|
|
585
|
+
|
|
586
|
+
**Switching regions does not refetch by itself.** Same caveat as the tenant switch above: RTK Query keys its cache by endpoint + args, and the region is part of neither. After `norbix.setRegion(...)`, data cached from the old region stays in the store until something invalidates it. Reset the cache the same way you would after a tenant switch:
|
|
587
|
+
|
|
588
|
+
```tsx
|
|
589
|
+
norbix.setRegion('nb-eu-germany');
|
|
590
|
+
dispatch(norbixApi.util.resetApiState()); // hard reset of every cached endpoint
|
|
591
|
+
// or, surgically:
|
|
592
|
+
dispatch(norbixApi.util.invalidateTags(['Regions', 'Projects']));
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
**Manage the regions a project spans.**
|
|
596
|
+
|
|
597
|
+
```tsx
|
|
598
|
+
import { useListRegionsQuery, useUpdateProjectRegionsMutation } from './norbix';
|
|
599
|
+
|
|
600
|
+
function RegionSettings({ projectId }: { projectId: string }) {
|
|
601
|
+
// GET /account/regions — the regions available to the account
|
|
602
|
+
const { data, isLoading } = useListRegionsQuery({});
|
|
603
|
+
// PATCH /account/projects/{projectId}/settings/regions
|
|
604
|
+
const [updateRegions, { isLoading: saving }] = useUpdateProjectRegionsMutation();
|
|
605
|
+
|
|
606
|
+
if (isLoading) return <p>Loading…</p>;
|
|
607
|
+
|
|
608
|
+
return (
|
|
609
|
+
<select
|
|
610
|
+
disabled={saving}
|
|
611
|
+
onChange={(e) =>
|
|
612
|
+
updateRegions({
|
|
613
|
+
projectId,
|
|
614
|
+
primaryRegion: e.target.value, // a region code, e.g. "nb-eu-germany"
|
|
615
|
+
additionalRegions: [],
|
|
616
|
+
})
|
|
617
|
+
}
|
|
618
|
+
>
|
|
619
|
+
{data?.items?.map((r) => (
|
|
620
|
+
<option key={r.id} value={r.id}>
|
|
621
|
+
{r.name ?? r.id}
|
|
622
|
+
</option>
|
|
623
|
+
))}
|
|
624
|
+
</select>
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
- `useListRegionsQuery` wraps `norbix.hub.regions.list` and **provides the `Regions` tag**. As with every query hook, a lazy variant — `useLazyListRegionsQuery` — is generated alongside it.
|
|
630
|
+
- `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.
|
|
631
|
+
|
|
632
|
+
> `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.
|
|
633
|
+
|
|
634
|
+
### Wire all the `*Integrations` modules in one line each
|
|
635
|
+
|
|
636
|
+
Almost every Hub module exposes the same integrations CRUD surface (`getXIntegrations`, `saveXIntegration`, `enableXIntegration`, ...). Instead of copy-pasting ~50 lines per module, use `buildIntegrationsEndpoints`. The package already uses it for `hub.database`; wire the other 8 in your app via `injectEndpoints`:
|
|
637
|
+
|
|
638
|
+
```ts
|
|
639
|
+
import { norbixApi, buildIntegrationsEndpoints } from '@norbix.ai/react-redux';
|
|
640
|
+
|
|
641
|
+
norbixApi.injectEndpoints({
|
|
642
|
+
endpoints: (b) => ({
|
|
643
|
+
// Email integrations live on hub.notifications (not hub.email)
|
|
644
|
+
...buildIntegrationsEndpoints(b, {
|
|
645
|
+
prefix: 'Email',
|
|
646
|
+
tag: 'EmailIntegrations',
|
|
647
|
+
namespace: (n) => n.hub.notifications,
|
|
648
|
+
include: { test: true, confirmHumanDelivery: true },
|
|
649
|
+
}),
|
|
650
|
+
// Push integrations
|
|
651
|
+
...buildIntegrationsEndpoints(b, {
|
|
652
|
+
prefix: 'Push',
|
|
653
|
+
tag: 'PushIntegrations',
|
|
654
|
+
namespace: (n) => n.hub.notifications,
|
|
655
|
+
include: { test: true, confirmHumanDelivery: true },
|
|
656
|
+
}),
|
|
657
|
+
// Sms integrations
|
|
658
|
+
...buildIntegrationsEndpoints(b, {
|
|
659
|
+
prefix: 'Sms',
|
|
660
|
+
tag: 'SmsIntegrations',
|
|
661
|
+
namespace: (n) => n.hub.notifications,
|
|
662
|
+
include: { test: true, confirmHumanDelivery: true },
|
|
663
|
+
}),
|
|
664
|
+
// Files integrations
|
|
665
|
+
...buildIntegrationsEndpoints(b, {
|
|
666
|
+
prefix: 'Files',
|
|
667
|
+
tag: 'FilesIntegrations',
|
|
668
|
+
namespace: (n) => n.hub.files,
|
|
669
|
+
include: { test: true },
|
|
670
|
+
}),
|
|
671
|
+
// Payments integrations
|
|
672
|
+
...buildIntegrationsEndpoints(b, {
|
|
673
|
+
prefix: 'Payments',
|
|
674
|
+
tag: 'PaymentIntegrations',
|
|
675
|
+
namespace: (n) => n.hub.payments,
|
|
676
|
+
include: { test: true, confirmHumanDelivery: true },
|
|
677
|
+
}),
|
|
678
|
+
// Code integrations
|
|
679
|
+
...buildIntegrationsEndpoints(b, {
|
|
680
|
+
prefix: 'Code',
|
|
681
|
+
tag: 'CodeIntegrations',
|
|
682
|
+
namespace: (n) => n.hub.code ?? (n.hub as never),
|
|
683
|
+
include: { test: true },
|
|
684
|
+
}),
|
|
685
|
+
// Membership integrations
|
|
686
|
+
...buildIntegrationsEndpoints(b, {
|
|
687
|
+
prefix: 'Membership',
|
|
688
|
+
tag: 'MembershipIntegrations',
|
|
689
|
+
namespace: (n) => n.hub.membership,
|
|
690
|
+
include: { test: false },
|
|
691
|
+
}),
|
|
692
|
+
// Logs integrations — uses the `Logging` SDK prefix, not `Logs`
|
|
693
|
+
...buildIntegrationsEndpoints(b, {
|
|
694
|
+
prefix: 'Logging',
|
|
695
|
+
tag: 'LogsIntegrations',
|
|
696
|
+
namespace: (n) => n.hub.logs,
|
|
697
|
+
include: { test: false },
|
|
698
|
+
}),
|
|
699
|
+
}),
|
|
700
|
+
overrideExisting: false,
|
|
701
|
+
});
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
This produces all hooks automatically — `useGetEmailIntegrationsQuery`, `useSaveEmailIntegrationMutation`, `useTestEmailIntegrationMutation`, `useConfirmEmailIntegrationHumanDeliveryMutation`, and the same for push/sms/files/payments/etc.
|
|
705
|
+
|
|
706
|
+
| Module | Prefix | Namespace | Tag | Notes |
|
|
707
|
+
|---|---|---|---|---|
|
|
708
|
+
| Email | `Email` | `n.hub.notifications` | `EmailIntegrations` | has `test` + `confirmHumanDelivery` |
|
|
709
|
+
| Push | `Push` | `n.hub.notifications` | `PushIntegrations` | has `test` + `confirmHumanDelivery` |
|
|
710
|
+
| Sms | `Sms` | `n.hub.notifications` | `SmsIntegrations` | has `test` + `confirmHumanDelivery` |
|
|
711
|
+
| Files | `Files` | `n.hub.files` | `FilesIntegrations` | has `test` |
|
|
712
|
+
| Payments | `Payments` | `n.hub.payments` | `PaymentIntegrations` | has `test` + `confirmHumanDelivery` |
|
|
713
|
+
| Code | `Code` | `n.hub.code` | `CodeIntegrations` | has `test` |
|
|
714
|
+
| Membership | `Membership` | `n.hub.membership` | `MembershipIntegrations` | no `test` |
|
|
715
|
+
| Logs | `Logging` *(uses `Logging`, not `Logs`)* | `n.hub.logs` | `LogsIntegrations` | no `test` |
|
|
716
|
+
| Database | `Database` | `n.hub.database` | `DatabaseIntegrations` | already wired in package |
|
|
717
|
+
|
|
718
|
+
**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`.
|
|
719
|
+
|
|
720
|
+
### Add your own endpoints with `injectEndpoints`
|
|
721
|
+
|
|
722
|
+
The package ships a curated set of hooks. When your app needs an endpoint we don't expose, you can add it without forking — `createNorbixApi` returns the standard RTK Query API object, so `injectEndpoints` works exactly as documented in the [RTK Query docs](https://redux-toolkit.js.org/rtk-query/api/created-api/code-splitting):
|
|
723
|
+
|
|
724
|
+
```ts
|
|
725
|
+
// src/services/myCampaigns.ts
|
|
726
|
+
import { norbixApi } from '../norbix';
|
|
727
|
+
import type { useNorbix } from '@norbix.ai/react-redux';
|
|
728
|
+
|
|
729
|
+
export const myCampaignsService = norbixApi.injectEndpoints({
|
|
730
|
+
endpoints: (builder) => ({
|
|
731
|
+
getEmailCampaigns: builder.query({
|
|
732
|
+
query: (args) => (norbix) => norbix.hub.notifications
|
|
733
|
+
? norbix.hub.notifications.getEmailTemplates(args) // example only
|
|
734
|
+
: Promise.resolve({}),
|
|
735
|
+
providesTags: [{ type: 'EmailCampaigns', id: 'LIST' }],
|
|
736
|
+
}),
|
|
737
|
+
// ...more app-specific endpoints
|
|
738
|
+
}),
|
|
739
|
+
overrideExisting: false,
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
export const { useGetEmailCampaignsQuery } = myCampaignsService;
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
The injected endpoints share the same `baseQuery`, the same tag taxonomy, and the same Redux slice — they integrate fully with cache invalidation across the package's hooks and your own.
|
|
746
|
+
|
|
747
|
+
### Unwrap response envelopes with `selectFromResult` or `transformResponse`
|
|
748
|
+
|
|
749
|
+
Norbix gateway responses are envelopes — `{ list: { items: [...] } }`, `{ user: {...} }`, etc. The hooks return the envelope shape (matching the SDK return type). To get just the inner array or item in a component, use `selectFromResult`:
|
|
750
|
+
|
|
751
|
+
```tsx
|
|
752
|
+
const { users, isLoading } = useGetUsersQuery(args, {
|
|
753
|
+
selectFromResult: ({ data, isLoading }) => ({
|
|
754
|
+
users: data?.list?.result ?? [],
|
|
755
|
+
isLoading,
|
|
756
|
+
}),
|
|
757
|
+
});
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
Or unwrap once at the endpoint level via `transformResponse` (use `injectEndpoints` to add a project-specific variant):
|
|
761
|
+
|
|
762
|
+
```ts
|
|
763
|
+
norbixApi.injectEndpoints({
|
|
764
|
+
endpoints: (builder) => ({
|
|
765
|
+
getUsersList: builder.query<UserDto[], Partial<GetUsersRequest>>({
|
|
766
|
+
query: (args) => (norbix) => norbix.api.membership.getUsers(args),
|
|
767
|
+
transformResponse: (res) => res.list?.result ?? [],
|
|
768
|
+
providesTags: [{ type: 'MembershipUsers', id: 'LIST' }],
|
|
769
|
+
}),
|
|
770
|
+
}),
|
|
771
|
+
overrideExisting: false,
|
|
772
|
+
});
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
### Migrating from a hand-rolled `fetchBaseQuery` setup
|
|
776
|
+
|
|
777
|
+
If your app currently calls Norbix via `fetchBaseQuery({ baseUrl })` + a custom `prepareHeaders`, you're reinventing what the SDK already does (auth precedence, retries, error mapping, env loading, MCP-aligned shape). Migration sketch:
|
|
778
|
+
|
|
779
|
+
1. **Replace** `fetchBaseQuery` setup with `createNorbixApi(() => norbix)`.
|
|
780
|
+
2. **Mount** `<NorbixProvider client={norbix}>` once at app root, alongside the existing `<Provider store={store}>`.
|
|
781
|
+
3. **Migrate per-service file**: rewrite each `api.injectEndpoints({ endpoints: builder => ({ getX: builder.query(...) }) })` from `url + method + body` to the `query: (args) => (norbix) => norbix.api.<module>.<method>(args)` shape. Keep the same `providesTags` / `invalidatesTags`.
|
|
782
|
+
4. **Remove** the custom error interceptor — `createNorbixBaseQuery` already maps `NorbixError` to `SerializedNorbixError` with `code`, `status`, `message`, `fieldErrors`. Consume those in your error UI instead.
|
|
783
|
+
5. **Keep** the same tag taxonomy — the package exports the full set (`Account`, `Projects`, `Regions`, `MembershipUsers`, `DatabaseSchemas`, ... 59 in total), so existing `providesTags` keep working.
|
|
784
|
+
|
|
785
|
+
## How it works under the hood
|
|
786
|
+
|
|
787
|
+
`createNorbixApi(getClient)` returns an RTK Query API. Its `baseQuery` is a thin wrapper that calls a closure you pass at endpoint definition time:
|
|
788
|
+
|
|
789
|
+
```ts
|
|
790
|
+
// inside @norbix.ai/react-redux
|
|
791
|
+
const baseQuery = async (call) => {
|
|
792
|
+
try {
|
|
793
|
+
return { data: await call(getClient()) };
|
|
794
|
+
} catch (err) {
|
|
795
|
+
return { error: serializeNorbixError(err) };
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
Every endpoint is shaped like:
|
|
801
|
+
|
|
802
|
+
```ts
|
|
803
|
+
getUsers: b.query({
|
|
804
|
+
query: (args) => (norbix) => norbix.api.membership.getUsers(args),
|
|
805
|
+
providesTags: [{ type: 'User', id: 'LIST' }],
|
|
806
|
+
}),
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
The closure carries the args and runs against the live SDK. Types come from the SDK's own method signatures via small `Result<F>` / `Arg<F>` helpers — no DTO imports required.
|
|
810
|
+
|
|
811
|
+
## Development
|
|
812
|
+
|
|
813
|
+
```sh
|
|
814
|
+
npm install
|
|
815
|
+
npm run lint
|
|
816
|
+
npm run typecheck
|
|
817
|
+
npm test
|
|
818
|
+
npm run build
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
Conventional commits are required. Pushes to `main` are released to npm by [semantic-release](https://github.com/semantic-release/semantic-release) with provenance enabled. `next` and `beta` branches publish prereleases.
|
|
822
|
+
|
|
823
|
+
## License
|
|
824
|
+
|
|
825
|
+
[MIT](./LICENSE) © Norbix
|