@geolonia/geonicdb-sdk
v0.8.0
Published
GeonicDB JavaScript SDK — NGSI-LD Context Broker client for browser and Node.js
Readme
@geolonia/geonicdb-sdk
GeonicDB JavaScript SDK — NGSI-LD Context Broker client for browser and Node.js.
Installation
npm install @geolonia/geonicdb-sdkQuick Start
Node.js / TypeScript
import GeonicDB from '@geolonia/geonicdb-sdk';
const db = new GeonicDB({
apiKey: 'your-api-key',
tenant: 'your-tenant',
baseUrl: 'https://your-geonicdb.example.com',
});
// Query entities
const rooms = await db.getEntities({ type: 'Room' });
console.log(rooms);
// Create entity
await db.createEntity({
id: 'urn:ngsi-ld:Room:001',
type: 'Room',
temperature: { type: 'Property', value: 23.5 },
});TypeScript
The SDK ships type definitions (geonicdb.d.ts) in the published package. Named imports of error classes and types resolve cleanly even under strict tsconfig settings (strict, verbatimModuleSyntax, etc.):
import GeonicDB, {
AuthorizationError,
AuthenticationError,
type EntityEvent,
type RefreshedCredentials,
} from '@geolonia/geonicdb-sdk';
try {
await db.deleteEntity(entityId);
} catch (err) {
if (err instanceof AuthorizationError) {
// Type-safe branch on XACML denial.
}
}See Error Handling below for the full list of exported error classes.
Browser (script tag)
<script src="https://unpkg.com/@geolonia/geonicdb-sdk/geonicdb.iife.js"></script>
<script>
var db = new GeonicDB({
apiKey: 'your-api-key',
tenant: 'your-tenant',
baseUrl: 'https://your-geonicdb.example.com',
});
db.getEntities({ type: 'Room' }).then(console.log);
</script>Authentication
API Key (DPoP + Proof of Work)
const db = new GeonicDB({
apiKey: 'your-api-key',
tenant: 'your-tenant',
baseUrl: 'https://your-geonicdb.example.com',
});
// Authentication is automatic — DPoP key pair generation,
// PoW solving, and token exchange happen transparently.Bearer JWT (Email/Password Login)
const db = new GeonicDB({
tenant: 'your-tenant',
baseUrl: 'https://your-geonicdb.example.com',
});
await db.login('[email protected]', 'password');
// Token refresh is automatic via refresh token.External Token
db.setCredentials({
token: 'your-jwt-token',
tokenType: 'Bearer',
expiresIn: 3600,
refreshToken: 'your-refresh-token',
});Anonymous Mode (public viewers)
For public-facing apps such as GeoJSON viewers or BI dashboards that should
serve unregistered visitors without forcing a login, pass anonymous: true
to skip token acquisition entirely:
const db = new GeonicDB({
baseUrl: 'https://your-geonicdb.example.com',
tenant: 'public',
anonymous: true, // no Authorization header, no /auth/nonce, no /oauth/token
});
// The server treats this as `role: 'anonymous'` and the XACML policy engine
// decides whether to permit. Tenant admins must register a custom policy that
// permits the anonymous role for the resources to expose — the default
// ANONYMOUS_DEFAULT_POLICY denies everything.
const entities = await db.getEntities({ type: 'GeoJSON' });apiKeyandanonymous: truecannot be combined (constructor throws).db.login()upgrades the SDK out of anonymous mode (Bearer JWT).db.setCredentials({ token, tokenType: 'Bearer', ... })upgrades from anonymous.tokenType: 'DPoP'is rejected for anonymous-built instances (no DPoP key pair is generated for them) — use a non-anonymous SDK instance for DPoP credentials.db.logout()reverts back to anonymous (db.isAnonymous() === trueagain).- WebSocket
db.connect()is not supported in anonymous mode — calllogin()/setCredentials()first.
Entity CRUD
// Create
await db.createEntity({ id: 'urn:ngsi-ld:Room:001', type: 'Room', ... });
// Read
const entity = await db.getEntity('urn:ngsi-ld:Room:001');
// Query (NGSI-LD §5.7.2 のクエリパラメータを網羅)
const entities = await db.getEntities({
type: 'Room',
limit: 10,
// scopeQ: '/OpenData;/OpenData/#', // exact + descendants
// georel: 'near;maxDistance==1000',
// geometry: 'Point',
// coordinates: '139.7,35.6',
// attrs: 'name,location',
// pick: 'name', omit: 'createdAt',
// lang: 'ja',
// orderBy: 'modifiedAt', orderDirection: 'desc',
// count: true,
});
// Update
await db.updateEntity('urn:ngsi-ld:Room:001', { temperature: { type: 'Property', value: 25 } });
// Delete
await db.deleteEntity('urn:ngsi-ld:Room:001');Types / Attributes Discovery
const types = await db.getTypes();
const roomType = await db.getType('Room');
const attributes = await db.getAttributes();
const tempAttr = await db.getAttribute('temperature');Temporal API
const history = await db.getTemporalEntities({
type: 'Room',
timerel: 'between',
timeAt: '2026-01-01T00:00:00Z',
endTimeAt: '2026-01-02T00:00:00Z',
});
const entityHistory = await db.getTemporalEntity('urn:ngsi-ld:Room:001');Batch Operations
// Create multiple entities
await db.batchCreate([
{ id: 'urn:ngsi-ld:Room:001', type: 'Room', ... },
{ id: 'urn:ngsi-ld:Room:002', type: 'Room', ... },
]);
// Upsert, Update, Delete
await db.batchUpsert(entities);
await db.batchUpdate(entities);
await db.batchDelete(['urn:ngsi-ld:Room:001', 'urn:ngsi-ld:Room:002']);Generic API Request
// Access any API endpoint with automatic authentication
const result = await db.request('GET', '/ngsi-ld/v1/subscriptions');WebSocket Event Streaming
db.subscribe({ entityTypes: ['Room'] });
db.on('entityUpdated', (event) => {
// event.entity contains the complete NGSI-LD entity ({ id, type, ...attributes })
console.log(`${event.entityId} updated:`, event.entity);
});
db.on('error', (err) => console.error(err));
await db.connect();Events
| Event | Payload | Description |
|-------|---------|-------------|
| entityCreated | EntityEvent | Entity was created |
| entityUpdated | EntityEvent | Entity was updated |
| entityDeleted | EntityEvent | Entity was deleted |
| open | — | WebSocket connection opened |
| connected | — | WebSocket ready |
| close | { code, reason } | WebSocket closed |
| disconnected | — | Unexpected disconnect |
| reconnecting | { attempt, delay } | Auto-reconnect starting |
| error | Error | Error occurred |
| tokenRefresh | RefreshedCredentials | Bearer token was refreshed |
| cacheHit | CacheEvent | Request served from in-memory cache (304 received) |
| cacheMiss | CacheEvent | Cache miss — fresh response fetched from origin |
| cacheInvalidated | CacheEvent | Cache entry dropped via explicit clearCache(). WebSocket entity events do not trigger cache invalidation — see "Client-side Cache & Polling" |
Client-side Cache & Polling
The SDK includes an in-memory cache that handles ETag / If-None-Match
negotiation transparently. Subsequent reads of an unchanged resource are
served as 304 Not Modified — the SDK presents them to your code as a normal
200 response with the cached body.
// First call: fresh fetch, ETag stored
const rooms1 = await db.getEntities({ type: 'Room' });
// Second call: server returns 304, SDK returns the cached body
const rooms2 = await db.getEntities({ type: 'Room' });
// Concurrent calls to the same path are deduplicated:
const [a, b, c] = await Promise.all([
db.getEntities({ type: 'Room' }),
db.getEntities({ type: 'Room' }),
db.getEntities({ type: 'Room' }),
]); // → only one HTTP request hits the networkCache is enabled by default; pass cache: false to opt out.
Cache lifetime under WebSocket activity
WebSocket entity events (entityCreated / entityUpdated / entityDeleted)
do not automatically invalidate the SDK cache. This is intentional:
- Data endpoints respond with
Cache-Control: private, no-cache, so every cached read already revalidates with the origin viaIf-None-Match. - If the entity actually changed, the server's freshly computed
ETagwill not match the client'sIf-None-Match→ server returns200with the fresh body → SDK replaces the cache entry. - If the WebSocket event was noisy / unrelated to a particular cached query
(or the data hasn't propagated to the read replica yet), the server returns
304 Not Modified→ SDK serves the cached body with no body bytes transferred.
Deleting cache entries on every WebSocket event would force the next read to
miss the cache and fetch a full 200 body — defeating the bandwidth-saving
304 path. To force a full reset (e.g. on auth change), call clearCache()
explicitly.
Polling
db.poll(params, options) repeats getEntities() at an interval and reports
back whether the data actually changed. Internally it leverages the cache's
ETag negotiation, so unchanged ticks transfer almost no bytes.
const handle = db.poll({ type: 'Room' }, {
interval: 5000,
onData: (rooms) => render(rooms),
onNoChange: () => {}, // server returned 304
onError: (err) => console.error(err),
});
// Stop later
handle.stop();API Reference
Constructor
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| new GeonicDB(options?) | GeonicDBOptions | GeonicDB | Create a new SDK instance |
GeonicDBOptions
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| apiKey | string | No | API key (or use data-api-key attribute on script tag) |
| tenant | string | No | Tenant name (or use data-tenant attribute on script tag) |
| baseUrl | string | No | API base URL (auto-detected from script src if omitted) |
| wsEndpoint | string | No | WebSocket endpoint URL (auto-detected from baseUrl if omitted) |
| debug | boolean | No | Enable debug logging to console (default: false) |
| cache | boolean | No | Enable in-memory cache with ETag/304 + request dedup (default: true) |
| cacheMaxEntries | number | No | LRU cache capacity when cache is enabled (default: 1000) |
| anonymous | boolean | No | Anonymous mode — skip token acquisition and send no Authorization header. Cannot be combined with apiKey. See Anonymous Mode (default: false) |
Authentication
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| login(email, password) | string, string | Promise<LoginResponse> | Login with email and password (Bearer JWT). Upgrades the SDK out of anonymous mode |
| setCredentials(opts) | CredentialsOptions | this | Set credentials externally (e.g. from a login API response). Upgrades the SDK out of anonymous mode when tokenType: 'Bearer'. Throws when tokenType: 'DPoP' is passed to an anonymous-built instance — anonymous instances do not generate a DPoP key pair |
| logout() | — | void | Clear all credentials and disconnect WebSocket. Reverts to anonymous mode if anonymous: true was set at construction |
| isAnonymous() | — | boolean | Whether explicit anonymous mode is active. Returns the explicit mode flag — not derived from credential presence — so a refresh failure that clears _token does NOT silently flip this back to true |
Entity CRUD
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| createEntity(entity) | Record<string, unknown> | Promise<{ created: true }> | Create a new entity |
| getEntity(entityId) | string | Promise<Record<string, unknown>> | Get a single entity by ID |
| getEntities(params?) | GetEntitiesParams | Promise<Record<string, unknown>[]> | Query entities with optional filters |
| count(params?) | CountEntitiesParams | Promise<number> | Count entities matching the given filters |
| updateEntity(entityId, attrs) | string, Record<string, unknown> | Promise<{ updated: true }> | Update entity attributes (partial) |
| deleteEntity(entityId) | string | Promise<{ deleted: true }> | Delete an entity |
GetEntitiesParams
| Property | Type | Description |
|----------|------|-------------|
| type | string | Entity type filter |
| limit | number | Max results |
| offset | number | Pagination offset |
| q | string | NGSI-LD query expression |
CountEntitiesParams
| Property | Type | Description |
|----------|------|-------------|
| type | string | Entity type filter |
| q | string | NGSI-LD query expression |
Types / Attributes Discovery
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| getTypes() | — | Promise<Record<string, unknown>[]> | List all entity types |
| getType(typeName) | string | Promise<Record<string, unknown>> | Get details for a specific entity type |
| getAttributes() | — | Promise<Record<string, unknown>[]> | List all attributes |
| getAttribute(attrName) | string | Promise<Record<string, unknown>> | Get details for a specific attribute |
Temporal API
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| getTemporalEntities(params?) | GetTemporalEntitiesParams | Promise<Record<string, unknown>[]> | Query temporal entities |
| getTemporalEntity(entityId) | string | Promise<Record<string, unknown>> | Get temporal representation of a single entity |
GetTemporalEntitiesParams
| Property | Type | Description |
|----------|------|-------------|
| type | string | Entity type filter |
| id | string | Entity ID |
| idPattern | string | Entity ID pattern (regex) |
| attrs | string | Attribute names to retrieve |
| q | string | NGSI-LD query expression |
| timeproperty | string | Time property (observedAt, createdAt, modifiedAt) |
| timeAt | string | Start time (ISO 8601) |
| endTimeAt | string | End time (ISO 8601) |
| timerel | string | Temporal relation (before, after, between) |
| limit | number | Max results |
| offset | number | Pagination offset |
Batch Operations
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| batchCreate(entities) | Record<string, unknown>[] | Promise<Record<string, unknown>> | Create multiple entities |
| batchUpsert(entities) | Record<string, unknown>[] | Promise<Record<string, unknown>> | Upsert multiple entities |
| batchUpdate(entities) | Record<string, unknown>[] | Promise<Record<string, unknown>> | Update multiple entities |
| batchDelete(entityIds) | string[] | Promise<Record<string, unknown>> | Delete multiple entities by ID |
Generic Request
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| request(method, path, body?) | string, string, unknown | Promise<unknown> | Authenticated request with automatic JSON parsing |
| requestRaw(method, path, body?) | string, string, unknown | Promise<Response> | Authenticated request returning raw Response (for accessing headers) |
Cache & Polling
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| poll(params, options) | GetEntitiesParams \| undefined, PollOptions<T> | PollHandle | ETag-based polling — fires onData only when the data actually changes. options is required (onData must be specified to receive data) |
| clearCache() | — | void | Drop all cached responses |
PollOptions<T>
| Property | Type | Description |
|----------|------|-------------|
| interval | number | Polling interval in milliseconds (default: 5000) |
| onData | (data: T) => void | Called whenever the server returns fresh data (200) |
| onNoChange | () => void | Called when the server returns 304 Not Modified |
| onError | (err: Error) => void | Called on any fetch/parse failure |
PollHandle
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| stop() | — | void | Stop the polling timer |
CacheEvent
| Property | Type | Description |
|----------|------|-------------|
| key | string | Cache key (METHOD:path) |
| path | string | Request path |
WebSocket
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| connect() | — | Promise<void> | Establish WebSocket connection |
| reconnect() | — | Promise<void> | Force-reconnect (preserves subscriptions) |
| disconnect() | — | void | Disconnect WebSocket |
| isConnected() | — | boolean | Check if WebSocket is currently open |
| subscribe(options?) | SubscribeOptions | void | Subscribe to entity events |
SubscribeOptions
| Property | Type | Description |
|----------|------|-------------|
| entityTypes | string[] | Entity types to subscribe to |
| idPattern | string | Entity ID regex pattern |
Event Handling
| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| on(event, listener) | string, Function | this | Register an event listener |
| off(event, listener) | string, Function | this | Remove an event listener |
Error Handling
All async methods throw typed errors extending GeonicDBError. Use instanceof to distinguish error types.
import { GeonicDBError, AuthenticationError, NotFoundError } from '@geolonia/geonicdb-sdk';
try {
await db.getEntity('urn:ngsi-ld:Room:404');
} catch (err) {
if (err instanceof AuthenticationError) {
// 401 — re-login needed
} else if (err instanceof NotFoundError) {
// 404 — entity not found
} else if (err instanceof GeonicDBError) {
console.error(err.message, err.statusCode);
} else {
console.error(err);
}
}| Class | Status | Description |
|-------|--------|-------------|
| GeonicDBError | — | Base class (statusCode property) |
| AuthenticationError | 401 | Invalid credentials or expired token |
| AuthorizationError | 403 | Insufficient permissions |
| NotFoundError | 404 | Resource not found |
| ConflictError | 409 | Entity already exists |
| ValidationError | 422 | Bad request payload |
| RateLimitError | 429 | Rate limited (retryAfter property) |
| NetworkError | — | Fetch failure, DNS error, timeout |
License
MIT
