Changelog
This project follows Keep a Changelog and Semantic Versioning.
1.6.1 - 2026-10-22
Section titled “1.6.1 - 2026-10-22”- The PostgREST driver places a short last page on the page that was asked for. It sized a page by
the span of
Content-Range, which is the page size only on a full page, so30-33/34at a limit of 10 parsed as page 8 of 9 instead of page 4 of 4, and40-44/45at a limit of 20 as page 9 of 9 instead of page 3 of 3.Paginator.paginate()synced that page into the store, so the nextRangeheader (160-179) oroffsetasked for rows past the end.IResponseStrategy.paginate()takes the requested page size as an optional fourth argument,limit, whichPaginatorpasses from the store; called without one, the PostgREST strategy still sizes a page by its span. Both pagination modes were affected, and ng-qubee 3.8.0 parsed the same way, so this is not a regression (#45) - The PostgREST driver reads an empty collection,
Content-Range: */0, as a total of 0 and one page. The header did not parse, so the total and last page were unknown and a last page synced from an earlier, wider query survived (#45)
Changed
Section titled “Changed”DRIVERSis 10.9 kB gzipped, up from 10.8 kB, andcreateQubee32 bytes larger minified. A single-driver import is unchanged at 2.7 kB (#45)
1.6.0 - 2026-10-19
Section titled “1.6.0 - 2026-10-19”- A FastAPI driver.
FASTAPI_DRIVER(DriverEnum.FASTAPI,'fastapi') speaks fastapi-filter on the request and fastapi-pagination’sPageon the response. Filters are flatfield=value, several values one comma-separatedfield__in=a,b; operators are fastapi-filter suffixes (GTE→__gte,NOT→__neq/__not_in,BTW→__gte+__lte,NULL→__isnull=true|false), withCONTAINS,ILIKEandSWsent aslike/ilikepatterns carrying their own%wildcards. Sorts areorder_by=-created_at,title, a repeated field sent once because fastapi-filter refuses it; search issearch=term, and pagination ispage+size. No select, fields or includes. The{ items, total, page, size, pages }body is read without configuration, as are thelinksoffastapi_pagination.links.Page. Thenulltotal ofUseIncludeTotal(False)reads as unknown, dropping thelastlink fastapi-pagination then points at page 1, and an empty result reads as one page with nofrom/to.LimitOffsetPageand the cursor page are not supported. Code that maps everyDriver, such as aRecord<Driver, …>or an exhaustiveswitch, needs afastapientry (#44)
Changed
Section titled “Changed”- The package’s npm page links the documentation site, https://qubeejs.andreatantimonaco.me, as
its homepage. With no
homepageinpackage.json, npm linked the repository’s README, so the repository showed up twice and the docs were not linked at all (#43) DRIVERSis 57 kB minified and 10.8 kB gzipped with the twentieth driver, up from 54 kB and 10.4 kB. A single-driver import is unchanged at 2.7 kB;FASTAPI_DRIVERalone is 2.6 kB (#44)
1.5.0 - 2026-10-19
Section titled “1.5.0 - 2026-10-19”- A Ransack (Rails) driver.
RANSACK_DRIVER(DriverEnum.RANSACK,'ransack') nests conditions under theqsearch key:q[status_eq]=published, several values as repeatedq[status_in][]=…keys, operators as Ransack predicates (GTE→_gteq,ILIKE→_i_cont,SW→_start,BTW→_gteq+_lteq,NULL→_null=1/_not_null=1), and sorts asq[s][]=created_at+desc. Pagination ispage+limit, Pagy’s defaults; Kaminari’sper_pageisrequest: { limit: 'per_page' }. Filters, operators and sort only: Ransack has no select, includes or search, and a search across columns is a combinator field name (title_or_body+ILIKE→q[title_or_body_i_cont]=…). The body is a bare array and the totals arrive in headers, read from Pagy (total-count,total-pages,current-page,page-limit) or Kaminari + api-pagination (total,per-page) without configuration; navigation comes from thelinkheader, and what no header names is derived. Code that maps everyDriver, such as aRecord<Driver, …>or an exhaustiveswitch, needs aransackentry (#42)
Changed
Section titled “Changed”- The WordPress driver reads its
Linkheader through the parser it now shares with Ransack.relis found anywhere among a link’s parameters, quoted or not, relation names match case-insensitively and a space-separatedrellist is read, sorel="Next"and<…>; type="…"; rel="next"are no longer ignored; everything WordPress sends is read as before (#42) DRIVERSis 54 kB minified and 10.4 kB gzipped with the nineteenth driver, up from 50 kB and 9.7 kB. A single-driver import is unchanged at 2.7 kB;RANSACK_DRIVERalone is 3.0 kB (#42)
1.4.1 - 2026-10-05
Section titled “1.4.1 - 2026-10-05”- A list’s input no longer depends on how its type is written. TypeScript read the input from the
type arguments only when a list’s type was spelled
ListDefinition<…>; through another alias (type LooseList = ListDefinition<ListParams>), a list spread into a new object or an object type written by hand, it inferred the input fromapply’s shape, and anapplywith two parameters or an input ofneverfell back to{}, “an input is required”.ListInput<TList>reported that input, andbuildListRequest(loose, state)failed with “Expected 3 arguments, but got 2”. Both now read the input fromapply’s parameters.buildListRequest()now takes the list’s own type as its one type parameter,<TList>instead of<TParams, TInput>, withstatetypedParamsState<TList['params']>; calls that leave the type arguments to inference are unchanged (#39) defineList()no longer declares an input for anapplywhose input is annotated as none.input?: never,input: undefinedandinput?: undefinedwere inferred asundefined, which the input’s constraint refuses, so the input fell back to{}andbuildListRequest()asked for a third argument. They now declare no input, as leaving the parameter out does;nullis still refused (#40)readListState(),buildListHref()andListState<TList>take a list written by hand whoseapplyrequires its input. They tookListDefinition<TParams>, whose input isnever, which such a list is not assignable to, so its state could not be read nor its links built, andListStatewasnever;buildListRequest()already took it. They now take any input, asbuildListRequest()does (#41)
1.4.0 - 2026-10-05
Section titled “1.4.0 - 2026-10-05”- Lists that need more than the URL. A list declares a typed input, what its request needs
besides URL state (such as a project id from the route path), by annotating
apply’s third parameter, andbuildListRequest(list, state, input)passes it toapply. Leaving a declared input out is a compile error, and so is passing one to a list that declares none;nullandundefinedare refused as inputs.ListInput<TList>reads a list’s input, for adapters. The input never reaches the URL:readListState()andbuildListHref()are unchanged, and the page is still applied last. Existing lists and two-argumentapplys are unchanged, and code that callsapplywith two arguments keeps compiling (#35)
- Exactly one slash joins the base URL and the resource. A trailing slash on the base URL
(
https://api.dev/) no longer giveshttps://api.dev//articles, and a resource with a leading slash (/projects/42/tasks) no longer gives the protocol-relative//projects/42/tasks, whichfetchresolves against a host namedprojects. A trailing slash on the resource is kept, and the state still holds both values as given (#34) - JSON:API and Spatie no longer guess the field type of a nested resource.
addFields()onusers/42/followersthrewKey users/42/followers is missing in the fields objectwhatever the key; the type cannot be read from the path (that resource returnsusers, andusers/meends inme), so the fields now go out as given. A single-segment resource is still checked, now with slashes around it ignored (#34)
1.3.0 - 2026-09-30
Section titled “1.3.0 - 2026-09-30”QubeeConfig.baseUrl:createQubee({ baseUrl, driver })prefixes every generated URI, assetBaseUrl()does, so one config object describes a whole instance.reset()clears it, as it clearssetBaseUrl()(#24)- Lists in the page URL.
defineList()declares a list once — its resource, qubee configuration, page-URL params and how its state becomes builder calls — andreadListState()reads a page’s query into typed state. It never throws: a missing, malformed or repeated value takes the param’s default — a single-value param falls back on a repeated key, whilelistParammerges repeated keys. Six params ship —integerParam,stringParam,enumParam,booleanParam,listParam,sortParam— and any object of theListParam<T>shape is a param.toSearchParams()normalises aURLSearchParamsfrom any realm, a query string or Next’ssearchParamsrecord. The core’s boundary is now pure transformations in core; navigation, subscriptions and fetching in the adapters — it still never navigates, subscribes to a router or fetches (#25) DuplicateListParamError(DUPLICATE_LIST_PARAM), thrown bydefineList()when two params share a page-URL key or a key is empty (#25)buildListHref()writes list state back into a link. Defaults stay out of the URL; a change to anything but the page returns to page 1, while setting a param to its current value keeps it and an explicitpagealways wins; keys the list does not own are kept; commas stay readable (sort=-publishedAt,title) (#26)buildListRequest()turns list state into{ uri, headers, paginate }on a freshcreateQubee()instance. It applies the page last — after the list’s filters, sorts and limit, which reset it — so the page in the URL is the page requested.headerscarries PostgREST’sRangein RANGE mode, andpaginateparses with the same instance’s paginator. A URL page the store would reject — zero, negative or fractional — falls back to the list’s default page (#27)- Helpers for list controls.
getPageWindow()gives page numbers with gaps, at a constant length, and never throws on an unknown last page.getPageRange()gives the numbers behind “21–34 of 34”.toggleSort()andgetAriaSort()serve sortable column headers, andtoggleSort()flips only the primary sort and starts any other field ascending (#28, #32)
QubeeStore.subscribeandgetSnapshotare bound to their store, so the documenteduseSyncExternalStore(store.subscribe, store.getSnapshot)works. Both were prototype methods and threwCannot read properties of undefinedwhen passed on their own. They are now arrow-function members — one per store, with a stable identity — and read as properties in the API reference. Because they are now instance properties, a subclass that overrides them, orvi.spyOn(QubeeStore.prototype, 'subscribe'), no longer works (#23)
1.2.0 - 2026-09-30
Section titled “1.2.0 - 2026-09-30”- Parameters no driver models.
QueryBuilder.setParam(key, ...values)anddeleteParams(...keys)emit what a backend accepts beyond the driver’s own parameters — a top-levelstatus, anincludeon a driver without includes.generateUri()appends them after the request strategy returns, so all eighteen drivers emit them, and so does a custom strategy that implementsIRequestStrategydirectly. No capability gates them. Keys are literal; each value is percent-encoded like a filter value, then the values are joined with a literal,(setParam('ids', 'a,b', 'c')→ids=a%2Cb,c). Both methods reset the page to 1, andreset()clears them. They live in the newQueryBuilderState.params(Params), which is optional in the type so state literals written against 1.1 still compile; the store always sets it (#22) ParamCollisionError(PARAM_COLLISION), thrown bygenerateUri()when a param would duplicate or override one the driver emits: its key equals one in the generated URI, or one key is the other followed by[(pageagainst JSON:API’spage[number]). The check reads the URI the driver actually produced, so it covers custom strategies and leaves a key free until the driver needs it — except pagination keys: the next page’s URI is checked too, so PostgREST’soffsetand OData’s$skipcollide on page 1, where they are not yet emitted (#22)DriverId,Driver | (string & {}): any string, while editors keep suggesting the eighteen built-in ids (#22)PaginatedCollection.toPlain(), returning aPaginatedResult<T>: an object literal with a newdataarray and every pagination field,nullwhere the backend reported nothing soJSON.stringifykeeps every key. React Server Components refuse to pass class instances to Client Components; this is the shape to hand them. The collection itself is unchanged (#22)
Changed
Section titled “Changed”- Custom drivers can name themselves.
DriverDefinition.id,QueryBuilder’sdriverargument andUnsupportedCapabilityError.driver(with its eight subclasses) are typedDriverIdrather thanDriver. A driver defined outside the package no longer has to borrow a built-in id, so its capability errors name it.DRIVERSstays keyed byDriverEnumandConfig.driverstaysDriver, since both resolve through the registry. Code that passes a definition’sidsomewhere typedDrivernow needs a check first (#22) - JSDoc names
QueryBuilder,PaginatorandQubeeStorewhere it still named ng-qubee’sNgQubeeService,PaginationService,NestService,nest()andprovideNgQubee— onPaginationModeEnum,StrategyCapabilities,IRequestStrategy,QubeeStore,QueryBuilderand several strategies.QueryBuilderandPaginatedCollectiongain the class comments they lacked (#22) - The generated API reference renders
{}, index signatures,keyofand rest parameters as declared, soDriverId,ParamsandsetParam()read as they are written (#22) - A single-driver import is 2.7 kB gzipped, up from 2.6 kB: every response strategy now carries
toPlain(). All eighteen drivers stay at 9.6 kB (#22)
PaginationNotSyncedErrortold core consumers to callPaginationService.paginate(), which exists only in ng-qubee. It now namesPaginator.paginate(); the code is unchanged (#22)
1.1.0 - 2026-09-25
Section titled “1.1.0 - 2026-09-25”Changed
Section titled “Changed”- Documentation site now matches the provided design: a custom 404, a changelog page generated
from
CHANGELOG.md, top-level section navigation with a version chip, and the bee mark inside “Bee aware” callouts (#19) - Landing page rebuilt against the design markup: two-column hero with the bee mark, radial glow, pill badge, hexagon feature icons, framed code sample and the driver grid
- Doc-page chrome matched to the design — sidebar rails, table of contents, previous/next cards, heading scale, honey-tinted inline code, and a breadcrumb above each title
- Repository renamed from
qubee-coretoqubeejs-core, matching the@qubeejs/corepackage name. All repository links — the CI badge, the docs edit and GitHub links, and the changelog comparison links — now point at the new URL; GitHub redirects the old one - A single-driver import is 2.6 kB gzipped, up from 2.5 kB: every driver now carries the value encoder (#21)
package.jsondeclares itsrepository. npm trusted publishing requiresrepository.urlto match the publishing GitHub repository, and the npm page now links back to it
PaginatedObjectisobjectrather thanRecord<string, unknown>, sopaginate<User>()andPaginatedCollection<User>accept aninterfaceor a class row again, asng-qubee3.x did. TypeScript grants an implicit index signature to atypealias but never to an interface, and the old constraint depended on it (#20)- Filter values and search terms are percent-encoded. No driver encoded them, so a
&split the value into a new parameter, a#cut off everything after it — pagination included — and a%became an invalid escape; user input could inject parameters of its own. Values are now encoded once, inAbstractRequestStrategy.buildUri(), while keys, operator syntax and the,joining several values stay literal. If you calledencodeURIComponent()on values yourself, remove it — it will now double-encode. Custom strategies extending the base receive encoded values and must not encode them again (#21) - PocketBase joined filter clauses with a raw
&&, so any filter with more than one condition reached the server cut off at the first&. It is now sent as%26%26(#21) - PostgREST
CONTAINSand PocketBaseSWwrote a bare%wildcard into the URI — an invalid escape. PostgREST now uses its documented*wildcard; PocketBase sends%25(#21)
Internal
Section titled “Internal”- The parity harness records deliberate divergences from
ng-qubee@3.8.0, each with its issue and a rewrite ofng-qubee’s URI into the expected one, so the comparison stays byte for byte. PocketBase is the first: 17 drivers identical, 1 diverged as documented (#21)
1.0.0 - 2026-09-08
Section titled “1.0.0 - 2026-09-08”First release. @qubeejs/core is the framework-agnostic engine extracted from
ng-qubee, which remains supported and unaffected.
Every URI this library builds was verified byte-for-byte against ng-qubee@3.8.0 across all
eighteen drivers before release — see test/parity/ and npm run test:parity.
- The core, with no Angular, no React, no RxJS and no Signals, and zero runtime dependencies: 18 backend drivers, each with a request and a response strategy (#1)
QueryBuilder— the fluent, capability-checked builder.generateUri()is synchronous and throws; URI construction never performed I/O (#6)QubeeStore— state exposed asgetSnapshot()+subscribe(), deliberately React’suseSyncExternalStorecontract, so each adapter supplies its own reactivity (#5)Paginator— parses a response through the driver’s response strategy and syncs the page and last page back into the store, so navigation helpers stop throwing (#16)createQubee({ driver })— wires the three together sharing one store, at a measured cost of 108 bytes over hand-wiring. Takes a driver definition rather than an id, so it never pulls the registry into a consumer’s bundle (#18)QubeeErrorbase carrying a machine-readablecode, diagnosticcontextand ES2022cause, withObject.setPrototypeOfhardening soinstanceofsurvives downlevelling (#8)- Derived union types —
Driver,FilterOperator,PaginationMode,SortDirection— alongside the enums, so{ driver: 'strapi' }and{ driver: DriverEnum.STRAPI }both typecheck (#2) RawResponse, modelling the bare-array body PostgREST and the WordPress REST API return, which the previous types could not express (#7)PaginatedCollection.normalize()accepts a selector function as well as a key name (#7)idonDriverDefinition, so a driver passed on its own can still name itself in errors (#18)- Public API as named re-exports from
src/index.ts— including the threeAbstract*bases,StrategyCapabilitiesand the registry, so third parties can author a driver (#11) - One file per driver under
src/drivers/, which is what makes a single-driver import tree-shakeable (#15) - A documentation site: 145 pages, with the API reference, all 18 driver pages and the capability matrix generated from source (#17)
Carried over from ng-qubee, where these are still present:
ResponseOptionsmerged with||, so a subclass passing''to mean “this driver derives the field” had its intent replaced by the Laravel default. Affected 11 of 14 drivers (#9)- All eight
Unsupported*Errormessages named specific drivers, and every one had become factually wrong — “Filters are only supported by the Spatie and NestJS drivers” when 16 of 18 support them. Messages are now derived from the capability and the active driver (#8) KeyNotFoundErrorandUnselectableModelErrornever setthis.name, soerr.namewas'Error'(#8)- Response strategies widened
paginate()toRecord<string, any>while the interface declaredRecord<string, unknown>; TypeScript’s bivariance hid the mismatch (#7) - Dot-path resolution could not read OData envelope keys, which contain literal dots
(
@odata.count). An exact key match is now preferred before splitting (#7) - The two response bases disagreed about a missing page: one defaulted to 1, the other yielded
undefinedfor a field declarednumber, which then threw from the store (#18) hasOwnPropertycalled directly on instances, and${array}relying on implicitArray.toString()coercion (#13)
Changed
Section titled “Changed”qsis gone. It was used only forstringify(payload, { encode: false }); a 40-line internal serialiser replaces it, verified againstqscase by case. A single-driver import fell from 15.7 kB to 2.5 kB gzipped (#10)- Data shapes are un-prefixed
types in*.type.ts.Iand*.interface.tsare reserved for the two interfaces a class actually implements (#2) header-bag.interface.tssplit into a type and a util — it exported a runtime function from a.interface.tsfile, which type-only elision could have dropped (#3)- Snapshots handed out by
QubeeStoreare deeply frozen. Identity stays stable between writes, asuseSyncExternalStorerequires, and tampering throws instead of silently corrupting a later read (#5)
Internal
Section titled “Internal”- 1,270 tests ported and added; 98.6% statement coverage, enforced in CI (#4)
test/conventions.spec.tsenforces the structural standards — one declaration kind per file, filename suffixes, theIprefix, constant casing — so a slip fails the build (#14)- CI verifies both entry points resolve, that there are no runtime dependencies, and that a single-driver import still tree-shakes (#12)
