solibo-sdk

KMP-Generated Kotlin — Idiosyncrasies for JS/TS Consumers

When the Solibo SDK is compiled to JavaScript via Kotlin Multiplatform, certain Kotlin types don’t map 1:1 to JS primitives. This document covers what you actually have to deal with — things that are solved transparently by the query layer or polyfills are noted but not belaboured.


Quick reference

Kotlin type In queries (reading) In commands (writing)
List<T> / KtList<T> Handled — query hooks call .asJsReadonlyArrayView() toKtList(arr)
List<Long> / KtList<bigint> Handled toIdList(arr)
Map<K,V> / KtMap .asJsReadonlyMapView() available on the object KtMap.fromJsMap(map)
Long (API parameter) n/a toLong(n)
Long (JSON serialization) Handled — BigInt polyfill converts to string n/a
Instant Kotlin object — use factory helpers createInstant(date)
LocalDate Kotlin object — use factory helpers createLocalDate(year, month, day)
enum class .name (string key) or .value (string value) Pass SDK enum constant directly
Sealed / polymorphic Check .type string discriminator n/a

Bridge helpers

solibo-query exports three helpers that eliminate the most common boilerplate:

import { toLong, toKtList, toIdList } from '@solibo/solibo-query'

toLong(companyId)           // number | bigint | string → bigint
toKtList(myArray)           // T[] → KtList<T>
toIdList(myIds)             // (number | bigint | string)[] → KtList<bigint>

1. Paged Lists — PagedList<T>

All collection endpoints return a PagedList<T> instead of a plain list. The wrapper has three fields:

Field Type Description
.items KtList<T> The current page of results
.meta Meta Metadata — .count is the number of items on this page
.paging Paging Cursor tokens — .next / .previous for navigation; .next === "END_OF_LIST" means no more pages

In hooks (solibo-query)

Paged list hooks use useInfiniteQuery. Data is accumulated across pages in data.pages:

const { data, fetchNextPage, hasNextPage } = useIssues({ companyId })

// Flatten all pages into one array
const allIssues = data?.pages.flatMap(page => page.items) ?? []

Each page in data.pages is already a plain JS object — page.items is a readonly T[] array (the bridge layer calls .asJsReadonlyArrayView() internally via fromKt(body)).

Directly from the SDK (outside a hook)

const body = (await sdk.api.task.indexTasks(toLong(companyId))).body()
const tasks = body.items.asJsReadonlyArrayView()  // readonly T[]
const nextToken = body.paging.next                // string | null

2. Collections — KtList / KtMap

Reading (queries)

The solibo-query hooks pass the full PagedList body through fromKt(), which recursively converts KtList to readonly T[]. React components receive plain arrays — no manual conversion needed.

If you work directly with an SDK response outside a hook, convert manually:

const items = response.body().items.asJsReadonlyArrayView()
// items is now readonly T[] — iterate, map, find, etc.

Writing (commands)

When submitting a list to an SDK command, use toKtList:

import { toLong, toKtList } from '@solibo/solibo-query'
import { SectionTagCommand } from '@solibo/solibo-sdk'

sdk.api.sections.multiCreateTagOnSection(
    toLong(companyId),
    toLong(sectionId),
    toKtList(tags.map(t => new SectionTagCommand(t)))
)

For arrays of IDs specifically, use toIdList:

import { toIdList } from '@solibo/solibo-query'

sdk.api.thirdParty.sendWithDigiPost(
    toLong(companyId),
    toLong(thirdPartyId),
    toIdList(publishTo)   // (number | bigint)[] → KtList<bigint>
)

For maps:

import { KtMap } from '@solibo/solibo-sdk'

const ktMap = KtMap.fromJsMap(new Map(Object.entries(myObj)))

Use KtMutableList.fromJsArray() / KtMutableMap.fromJsMap() when the API requires a mutable variant.


3. Long — API parameters

Kotlin Long becomes a bigint on the JS side. All numeric ID parameters must be converted. Use toLong:

import { toLong } from '@solibo/solibo-query'

sdk.api.organization.showOrganization(toLong(organizationId))
sdk.api.task.indexTasks(toLong(companyId))

// Wrong — passes a plain number, which will cause a type error:
sdk.api.task.indexTasks(companyId)

toLong accepts number | bigint | string and returns a bigint primitive, which is what Kotlin/JS expects for Long parameters. (The BigInt(n).valueOf() pattern seen in older hooks is equivalent but more verbose.)

IDs returned in responses are also bigint. When comparing or using them as React Query keys, convert to string or number as needed:

const id = response.id.toString()      // safe for display, keys, URL params
const idNum = Number(response.id)      // only safe for values < 2^53

Serialization (handled automatically)

solibo-query ships a bigint-polyfill.ts that patches BigInt.prototype.toJSON to serialize as a string. This is imported at package entry and requires no action from consumers.


4. Date and time — Instant and LocalDate

Kotlin date types are not JS Date objects. Use the SDK factory helpers when constructing commands:

import { createLocalDate, createInstant } from '@solibo/solibo-sdk'

// For LocalDate fields (e.g. startDate, endDate):
createLocalDate(2025, 1, 31)   // year, month (1-based), day

// For Instant fields (e.g. validFrom, closedAt):
createInstant(new Date())      // wraps a JS Date

When reading date fields from responses, convert to a JS Date for display:

const date = new Date(response.createdAt.toEpochMilliseconds())

Do not pass a raw new Date() to a field typed as Instant — the SDK command constructor expects a Kotlin Instant.


5. Enums

Kotlin enums compile to singleton objects, not plain strings. Two properties are available:

// Reading:
expense.status.name === 'APPROVED'    // true
expense.status.value === 'APPROVED'   // true (usually identical)

// Comparing with SDK constant (preferred — survives renames):
expense.status === ExpenseStatus.APPROVED

// Writing — pass the enum constant, not a string:
CreateExpenseCommand({ status: ExpenseStatus.FOR_APPROVAL,  })

Do not compare with a raw string using === on the enum object itself — it is an object, not a primitive.


6. Sealed classes / polymorphic types

The API returns polymorphic types (e.g. Resident is a sealed superclass; PersonResident and OrgResident extend it). The concrete type is identified by a type field whose value is the fully-qualified Kotlin class name:

if (resident.type === 'solibo.residents.query.domain.PersonResident') {
    // narrow to PersonResident — access .email, .name, .mobile, etc.
}

instanceof checks do not work across the Kotlin/JS boundary.


7. null vs undefined

Kotlin nullable fields (String?, Long?, etc.) arrive as null in JS, not undefined. Use null-coalescing or loose null checks:

resident.email ?? 'fallback'     // correct
resident.email !== undefined     // may miss null — incorrect
resident.email != null           // correct (covers both)