norbix 2.0.0 copy "norbix: ^2.0.0" to clipboard
norbix: ^2.0.0 copied to clipboard

Official Dart SDK for Norbix. Project-scoped data plane (api.norbix.ai) and account-scoped configuration plane (hub.norbix.ai), one package, two importable libraries.

Norbix Dart SDK #

One package, two importable libraries:

Import What it talks to Default URL
package:norbix/norbix_api.dart Project-scoped Norbix API https://api.norbix.ai
package:norbix/norbix_hub.dart Account-scoped Norbix Hub https://hub.norbix.ai

A single shared core (HTTP transport, config, typed errors) lives under lib/src/core/ and is re-exported from both entry-point libraries — you never need to import it directly.

Install #

dart pub add norbix

Import the gateway you use:

import 'package:norbix/norbix_api.dart';   // project-scoped API
import 'package:norbix/norbix_hub.dart';   // account-scoped Hub

If you need both in the same file, import one with a prefix to avoid name collisions on resources that exist in both gateways (e.g. auth, apiKeys):

import 'package:norbix/norbix_api.dart';
import 'package:norbix/norbix_hub.dart' as hub;

final api = NorbixApi();
final h   = hub.NorbixHub();

Resource-style API (no namespace nesting) #

Resources are direct properties on the client:

final api = NorbixApi();
await api.users.getUsers();
await api.auth.authenticate();
await api.apiKeys.getApiKeys();

final hub = NorbixHub();
await hub.projects.getProjects();
await hub.database.getDatabaseSchemas();
await hub.emailNotifications.createEmailTemplate(body: {...});

There is no client.api.xxx or client.hub.xxx middle layer.

Configuration #

Defaults to the public *.norbix.ai hosts. Override the URL when you self-host:

// Self-hosted at your company domain
final api = NorbixApi(
  config: NorbixConfig(
    baseUrl: 'https://api.norbix.isidos.lt',
    apiKey: 'nbx_...',
  ),
);

// Local development
final dev = NorbixHub(
  config: NorbixConfig(baseUrl: 'http://localhost:5000'),
);

Or load everything from environment variables — no boilerplate, no String.fromEnvironment compile-time defines:

final api = NorbixApi.fromEnv();   // reads NORBIX_API_*
final hub = NorbixHub.fromEnv();   // reads NORBIX_HUB_*

Regions #

Pin a client to a Norbix region (a region code such as nb-eu-germany). There is no default region: when none is set, no region header is sent and requests go to the plain host — existing and self-hosted setups are unaffected.

// 1) At construction
final hub = NorbixHub(region: 'nb-eu-germany');
final api = NorbixApi(region: 'nb-eu-germany');

// 2) From environment variables
final hub = NorbixHub.fromEnv();   // reads NORBIX_HUB_REGION
final api = NorbixApi.fromEnv();   // reads NORBIX_API_REGION

// 3) At runtime
hub.setRegion('nb-us-east');       // subsequent requests target nb-us-east
print(hub.region);                 // 'nb-us-east'
hub.setRegion(null);               // clear — no header, default host

(NorbixConfig.fromEnv also takes a regionVar: name, default NORBIX_REGION; the per-host factories above set it for you.)

Every request with a resolved region carries the nb-region header. When the client's base URL is one of the SDK defaults (https://api.norbix.ai, https://hub.norbix.aikNorbixRegionalDefaultBaseUrls), the client region also routes the request to the regional host:

https://hub.norbix.ai  +  region nb-eu-germany  →  https://nb-eu-germany.hub.norbix.ai

A custom base URL (self-hosted, localhost) is never rewritten — the region then only adds the nb-region header.

The two region-aware endpoints also accept a per-call region: argument that overrides the client region for that single request. A per-call override sets the header only; the URL is not recomposed:

await hub.accounts.getAccountRegions(region: 'nb-us-east');

Regions endpoints #

Regions live on the existing accounts and projects resources — there is no separate hub.regions module:

// GET /{version}/account/regions — regions available to the account.
// Response: {'items': [{'id': 'nb-eu-germany', 'continent': ..., 'name': ...}, ...]}
// 'id' is the region code; 'continent' and 'name' are optional.
final regions = await hub.accounts.getAccountRegions();

// PATCH /{version}/account/projects/{projectId}/settings/regions
// Body takes 'primaryRegion' (a region code) and/or 'additionalRegions'
// (a list of region codes). The response is empty.
await hub.projects.updateProjectRegions(
  projectId: 'p1',
  body: {
    'primaryRegion': 'nb-eu-germany',
    'additionalRegions': ['nb-us-east'],
  },
);

hub.projects.createProject also accepts optional primaryRegion / additionalRegions keys in its body to place a new project in specific regions at creation time.

Errors #

try {
  await api.files.getFileInfo(integrationId, path: 'a/b.txt');
} on NorbixError catch (e) {
  // httpStatus / errorCode are the names every Norbix SDK uses.
  // status / code are the same values, kept for older code.
  print('${e.httpStatus} ${e.errorCode}: ${e.message}');
  for (final item in e.errors) {
    print('${item.errorCode} ${item.fieldName}: ${item.message}');
  }
  print(e.body); // the answer exactly as it arrived
}

message and errorCode are the gateway's own. The gateway puts them inside responseStatus.errors[], so the SDK reads that list first, takes the first entry for the message and the code, and keeps every entry in errors. Only when the body has no responseStatus are the top-level message and errorCode read. Request failed (HTTP N) with the code NORBIX_HTTP_ERROR is the last fallback, used when the body says nothing — a 500 page that is not JSON, say.

Breaking change — a refused call now throws #

The gateway answers a business refusal (an unknown id, a rule that says no) with HTTP 200 and responseStatus.isSuccess = false. The SDK used to hand that answer back as a normal value, so code carried on as if the call had worked. It now throws a NorbixError with httpStatus 200 and the gateway's message and error code.

If your code checked result['responseStatus']['isSuccess'] itself, move that check into a try / catch. Endpoints that answer with raw bytes rather than a document (sendBytes — file download, the public file link) are not JSON and are unchanged.

Working with terms #

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). Pick the call that matches what you want:

I want to… Call Returns
Get a taxonomy's terms as a flat list findTerms a paginated list of terms
Get only the children of one term findTermsChildren a list of child terms (direct + multi-parent)
Get a taxonomy's terms as a ready-made tree findTermTree a tree of nested term nodes
Get the taxonomy structure (e.g. Countries → Cities) findTaxonomyTree a tree of taxonomy nodes

The examples below all use one example services taxonomy shaped like this:

Indoors
  └─ Air conditioning
       └─ Wall-mounted
Outdoors
  └─ Solar panels

List a taxonomy's terms (flat) #

Goal: show every term of services in a simple list, in display order.

final res = await hub.database.findTerms(taxonomyName: 'services');
{
  "list": {
    "items": [
      { "id": "term_indoors",   "taxonomyName": "services", "parentId": null,           "order": 1, "name": "Indoors" },
      { "id": "term_air_con",   "taxonomyName": "services", "parentId": "term_indoors", "order": 1, "name": "Air conditioning" },
      { "id": "term_wall",      "taxonomyName": "services", "parentId": "term_air_con", "order": 1, "name": "Wall-mounted" },
      { "id": "term_outdoors",  "taxonomyName": "services", "parentId": null,           "order": 2, "name": "Outdoors" },
      { "id": "term_solar",     "taxonomyName": "services", "parentId": "term_outdoors","order": 1, "name": "Solar panels" }
    ],
    "hasMore": false, "hasPrevious": false, "startingAfter": null, "endingBefore": null
  },
  "responseStatus": { "isSuccess": true }
}

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 findTermTree for that).


List only top-level terms (filtered) #

Goal: show just the roots (no parent) — for the first level of a menu.

final res = await hub.database.findTerms(
  taxonomyName: 'services',
  query: {'filter': '{ "parentId": null }'},
);
{
  "list": {
    "items": [
      { "id": "term_indoors",  "taxonomyName": "services", "parentId": null, "order": 1, "name": "Indoors" },
      { "id": "term_outdoors", "taxonomyName": "services", "parentId": null, "order": 2, "name": "Outdoors" }
    ],
    "hasMore": false, "hasPrevious": false, "startingAfter": null, "endingBefore": null
  },
  "responseStatus": { "isSuccess": true }
}

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.


Get a term's children #

Goal: the user expanded Indoors — load what is directly under it.

final res = await hub.database.findTermsChildren(
  taxonomyName: 'services',
  parentId: 'term_indoors',
);
{
  "list": {
    "items": [
      {
        "id": "term_air_con",
        "taxonomyName": "services",
        "parentId": "term_indoors",
        "order": 1,
        "name": "Air conditioning",
        "multiParents": [
          { "taxonomyId": "tax_service_types", "parentId": "term_indoors",          "name": "Indoors" },
          { "taxonomyId": "tax_service_types", "parentId": "term_energy_efficient", "name": "Energy efficient" }
        ]
      }
    ],
    "hasMore": false, "hasPrevious": false
  },
  "responseStatus": { "isSuccess": true }
}

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.


Multi-parent: one product in several categories #

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.

final res = await hub.database.findTermsChildren(
  taxonomyName: 'products',
  parentId: 'term_gift_ideas',
);
{
  "list": {
    "items": [
      {
        "id": "term_relaxing_oil",
        "taxonomyName": "products",
        "name": "Relaxing massage oil",
        "multiParents": [
          { "taxonomyId": "tax_categories", "parentId": "term_for_couples", "name": "For couples" },
          { "taxonomyId": "tax_categories", "parentId": "term_gift_ideas",  "name": "Gift ideas" },
          { "taxonomyId": "tax_categories", "parentId": "term_body_care",   "name": "Body care" }
        ]
      }
    ],
    "hasMore": false, "hasPrevious": false
  },
  "responseStatus": { "isSuccess": true }
}

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.


Get the whole term tree in one call #

Goal: render the full services tree at once, already nested.

final res = await hub.database.findTermTree(taxonomyName: 'services');
{
  "tree": [
    {
      "id": "term_indoors",
      "name": "Indoors",
      "order": 1,
      "children": [
        {
          "id": "term_air_con",
          "name": "Air conditioning",
          "order": 1,
          "children": [
            { "id": "term_wall", "name": "Wall-mounted", "order": 1, "children": null }
          ]
        }
      ]
    },
    {
      "id": "term_outdoors",
      "name": "Outdoors",
      "order": 2,
      "children": [
        { "id": "term_solar", "name": "Solar panels", "order": 1, "children": null }
      ]
    }
  ],
  "responseStatus": { "isSuccess": true }
}

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.


Get only a sub-tree, capped by depth #

Goal: start from Indoors and go at most 2 levels deep.

final res = await hub.database.findTermTree(
  taxonomyName: 'services',
  query: {'rootTermId': 'term_indoors', 'depth': 2},
);
{
  "tree": [
    {
      "id": "term_indoors",
      "name": "Indoors",
      "order": 1,
      "children": [
        { "id": "term_air_con", "name": "Air conditioning", "order": 1, "children": null }
      ]
    }
  ],
  "responseStatus": { "isSuccess": true }
}

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.


Get the taxonomy structure tree — without terms #

Goal: see how taxonomies relate to each other (e.g. a Cities taxonomy whose parent is Countries), structure only.

final res = await hub.database.findTaxonomyTree();
{
  "tree": [
    {
      "viewId": "txn_countries",
      "taxonomyName": "Countries",
      "taxonomySlug": "countries",
      "parentId": null,
      "children": [
        { "viewId": "txn_cities", "taxonomyName": "Cities", "taxonomySlug": "cities", "parentId": "txn_countries", "children": null, "terms": null }
      ],
      "terms": null
    }
  ],
  "responseStatus": { "isSuccess": true }
}

This is the taxonomy tree, not the term tree: nodes are taxonomies. Every terms is null because we did not ask for terms.


Get the taxonomy structure tree — with terms #

Goal: same structure, but also pull each taxonomy's terms in the same call.

final res = await hub.database.findTaxonomyTree(
  query: {'includeTerms': true},
);
{
  "tree": [
    {
      "viewId": "txn_countries",
      "taxonomyName": "Countries",
      "taxonomySlug": "countries",
      "parentId": null,
      "terms": [
        { "id": "term_lt", "name": "Lithuania", "order": 1, "children": null },
        { "id": "term_lv", "name": "Latvia",    "order": 2, "children": null }
      ],
      "children": [
        {
          "viewId": "txn_cities",
          "taxonomyName": "Cities",
          "taxonomySlug": "cities",
          "parentId": "txn_countries",
          "terms": [
            { "id": "term_vilnius", "name": "Vilnius", "order": 1, "children": null },
            { "id": "term_kaunas",  "name": "Kaunas",  "order": 2, "children": null }
          ],
          "children": null
        }
      ]
    }
  ],
  "responseStatus": { "isSuccess": true }
}

Now each taxonomy node's terms holds that taxonomy's full term tree (same shape as findTermTree) — Countries carries its countries, Cities carries its cities.

Every term-reading call also accepts an optional databaseIntegrationId to target a non-default database.

Files #

A file or a whole folder can be made readable by anyone holding its link. Publishing is a Hub action and needs your key; reading the link needs nothing at all.

final hub = NorbixHub(config: NorbixConfig(baseUrl: 'https://hub.norbix.ai', apiKey: 'k'));

// Publish one file, or a whole folder prefix (one record, however many
// files sit under it, at any depth — the root cannot be published).
await hub.files.makeFilePublic(
  body: {'filesIntegrationId': 'nbin_1', 'path': 'docs/invoice.pdf'},
);
await hub.files.makeFolderPublic(
  body: {'filesIntegrationId': 'nbin_1', 'path': 'docs'},
);

// Take it back. makeFilePrivate is refused while a folder above the file
// is public — switch the folder off instead.
await hub.files.makeFilePrivate(
  body: {'filesIntegrationId': 'nbin_1', 'path': 'docs/invoice.pdf'},
);
await hub.files.makeFolderPrivate(
  body: {'filesIntegrationId': 'nbin_1', 'path': 'docs'},
);

Reading a published file is the one call in this SDK that goes out with no credentials — the link has to work in an e-mail, in an <img src>, or in a browser on a stranger's phone. It answers with the raw bytes:

final api = NorbixApi();
final bytes = await api.files.getPublicFile(
  publicId: 'nbpf_abc',
  name: '2026/q1/report.pdf', // slashes stay slashes for a folder link
);

Every miss — unknown id, wrong name, made private again, file gone — is the same plain 404, on purpose: a more precise answer would tell a stranger that the file exists.

Testing an integration before you save it #

hub.files.testFilesIntegration tries the credentials against the storage provider and answers whether they work. Nothing is saved — use it before saveFilesIntegration to tell a bad key from a bad bucket.

Testing a saved integration from the API #

api.files.testFilesIntegration runs a live probe against an integration that is already saved: it uploads a small file, reads it, lists the folder and deletes the file again. It answers one entry per step. Because it writes to the storage, the API key needs the files:create permission.

final api = NorbixApi(config: NorbixConfig(baseUrl: 'https://api.norbix.ai', apiKey: 'k'));
final res = await api.files.testFilesIntegration(filesIntegrationId: 'nbin_1')
    as Map<String, dynamic>;
for (final step in res['items'] as List) {
  print('${step['operation']}: ${step['result']}'); // UploadFile: OK, GetFile: OK, ...
}

Repo layout #

norbix-dart/
├── lib/
│   ├── norbix_api.dart           # API entry point (re-exports core + api client + resources)
│   ├── norbix_hub.dart           # Hub entry point (re-exports core + hub client + resources)
│   └── src/
│       ├── core/                 # handwritten: transport, config, errors
│       ├── api/                  # GENERATED — gitignored
│       └── hub/                  # GENERATED — gitignored
├── test/
│   ├── _fake_driver.dart
│   ├── core/                     # core tests
│   ├── api/                      # API client tests
│   └── hub/                      # Hub client tests
├── tool/
│   └── generate_resources.py     # codegen for resource modules (dev-task, never runs in CI)
├── pubspec.yaml                  # single `norbix` package
└── Makefile                      # gen / lint / test

Development #

dart pub get                      # install deps
make gen                          # regenerate resource modules (dev-only)
make test                         # run all tests
make lint                         # dart analyze

CI never runs make gen. The generated files under lib/src/api/ and lib/src/hub/ are gitignored. The dev runs the gen script locally and ships the SDK with the generated artifacts produced from the canonical route files.

Releases #

Versioned with Conventional Commits + semantic-release:

  • feat: — minor
  • fix: — patch
  • feat! / BREAKING CHANGE: — major

Channels: main → stable, next-rc.*, beta-beta.*.

On a push to main, release.yml runs analyze + tests, and semantic-release creates the vX.Y.Z tag and GitHub Release. It then starts publish.yml on that tag, which publishes to pub.dev with automated publishing (OIDC) — no stored credential. main is protected, so nothing is committed back: pubspec.yaml and CHANGELOG.md are stamped with the version only inside the publish job. To re-publish a tag, run Publish to pub.dev from the Actions tab with that tag selected.

0
likes
150
points
94
downloads

Documentation

API reference

Publisher

verified publishernorbix.ai

Weekly Downloads

Official Dart SDK for Norbix. Project-scoped data plane (api.norbix.ai) and account-scoped configuration plane (hub.norbix.ai), one package, two importable libraries.

Homepage
Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

crypto, http, meta

More

Packages that depend on norbix