@classytic/repo-core 0.1.0 → 0.3.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +153 -1
  2. package/README.md +28 -7
  3. package/dist/errors/contract.d.mts +37 -0
  4. package/dist/errors/contract.mjs +75 -0
  5. package/dist/errors/index.d.mts +3 -2
  6. package/dist/errors/index.mjs +3 -1
  7. package/dist/errors/types.d.mts +113 -8
  8. package/dist/errors/types.mjs +29 -0
  9. package/dist/pagination/canonical.d.mts +35 -0
  10. package/dist/pagination/canonical.mjs +32 -0
  11. package/dist/pagination/index.d.mts +3 -2
  12. package/dist/pagination/index.mjs +2 -1
  13. package/dist/pagination/types.d.mts +65 -1
  14. package/dist/repository/base.mjs +21 -0
  15. package/dist/repository/index.d.mts +3 -2
  16. package/dist/repository/types.d.mts +63 -10
  17. package/dist/schema/field-rules.d.mts +19 -8
  18. package/dist/schema/field-rules.mjs +29 -9
  19. package/dist/schema/generator.d.mts +72 -0
  20. package/dist/schema/generator.mjs +16 -0
  21. package/dist/schema/index.d.mts +2 -1
  22. package/dist/schema/index.mjs +2 -1
  23. package/dist/schema/types.d.mts +56 -3
  24. package/dist/tenant/index.d.mts +3 -0
  25. package/dist/tenant/index.mjs +2 -0
  26. package/dist/tenant/resolve.d.mts +27 -0
  27. package/dist/tenant/resolve.mjs +69 -0
  28. package/dist/tenant/types.d.mts +142 -0
  29. package/dist/update/builders.d.mts +52 -0
  30. package/dist/update/builders.mjs +92 -0
  31. package/dist/update/compile.d.mts +46 -0
  32. package/dist/update/compile.mjs +33 -0
  33. package/dist/update/guard.d.mts +20 -0
  34. package/dist/update/guard.mjs +24 -0
  35. package/dist/update/index.d.mts +5 -0
  36. package/dist/update/index.mjs +4 -0
  37. package/dist/update/types.d.mts +62 -0
  38. package/package.json +13 -1
package/CHANGELOG.md CHANGED
@@ -4,7 +4,159 @@ All notable changes to `@classytic/repo-core` are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [Unreleased]
7
+ ## [0.3.0] - 2026-04-29
8
+
9
+ ### Added — Aggregate pagination shapes
10
+
11
+ - `AggregatePaginationResultCore<TDoc>` and `AggregatePaginationResult<TDoc, TExtra>` join `Offset*` / `Keyset*` as the third pagination shape every kit reports. Mirrors offset (page / total / pages / hasNext / hasPrev) with `method: 'aggregate'` discriminant. Mongokit's existing local `AggregatePaginationResult` (3.10.x) becomes redundant — to be deleted in mongokit 4.0.
12
+ - `AnyPaginationResult<TDoc, TExtra>` — union over the three result shapes. Use as the input type to anything that converts repo results into HTTP envelopes.
13
+
14
+ ### Added — HTTP wire envelopes
15
+
16
+ The repository result shapes (`OffsetPaginationResult`, etc.) carry the `method` discriminant, so the corresponding HTTP wire envelope is just `{ success: true } & Result`. Adding the literal here closes the **server/client envelope mismatch** — arc's HTTP server was emitting flattened paginated responses without the `method` field while arc-next's typed responses required it.
17
+
18
+ - `OffsetPaginationResponse<TDoc, TExtra>` = `{ success: true } & OffsetPaginationResult<TDoc, TExtra>`
19
+ - `KeysetPaginationResponse<TDoc, TExtra>` = same for keyset
20
+ - `AggregatePaginationResponse<TDoc, TExtra>` = same for aggregate
21
+ - `BareListResponse<TDoc>` = `{ success: true; docs: TDoc[] }` for endpoints that don't paginate
22
+ - `PaginatedResponse<TDoc, TExtra>` = union over all four. The discriminated-union contract is `success: true` literal first, `method` second — typed clients (arc-next, SDKs) narrow with `if (res.success && 'method' in res && res.method === 'offset')`.
23
+
24
+ ### Added — `toCanonicalList()` runtime normalizer
25
+
26
+ ```ts
27
+ import { toCanonicalList } from '@classytic/repo-core/pagination';
28
+
29
+ const result = await userRepo.getAll(query);
30
+ reply.send(toCanonicalList(result)); // → PaginatedResponse<User>
31
+
32
+ reply.send(toCanonicalList([u1, u2])); // → BareListResponse<User>
33
+ ```
34
+
35
+ The single point where an internal `Result` becomes an external `Response`. Three overloads route bare arrays / paginated results to the right wire shape; `TExtra` fields (mongokit's `warning?: string`, etc.) flow through.
36
+
37
+ **Subtle behavior**: `success: true` is stamped *after* the spread, so a stale `success: false` accidentally present on the input cannot override the literal — paginated success path is always `success: true`. Tested.
38
+
39
+ ### Added — `isPaginatedResult()` type guard
40
+
41
+ Branches on the `method` discriminant rather than `Array.isArray`, so an empty paginated result still routes through the paginated branch. Used internally by `toCanonicalList`; exported for consumers writing custom envelope logic.
42
+
43
+ ### Test delta
44
+
45
+ 230 → 303 tests across 0.3.0. New coverage includes `tests/unit/pagination/canonical.test.ts` and the type-level coverage extensions in `result-types.test.ts` (35 tests landed with the aggregate / wire-envelope / `toCanonicalList` work), plus `tests/unit/repository/base-plugin-validation.test.ts` (6 tests for the `assertValidPlugin` guard).
46
+
47
+ ### Added — `SchemaGenerator<TModel>` interface in `/schema`
48
+
49
+ Canonical contract for repository kits' CRUD-schema generators. Mongokit's `buildCrudSchemasFromModel` and sqlitekit's `buildCrudSchemasFromTable` (and any future kit's equivalent) `satisfies SchemaGenerator<TKitModel>` at the call site, so arc's `MongooseAdapter.schemaGenerator` / `DrizzleAdapter.schemaGenerator` accept them by structural typing — no glue, no inheritance, no inline function signatures duplicated in every adapter.
50
+
51
+ - `SchemaGenerator<TModel = unknown>` — `(model, options?, context?) => CrudSchemas | Record<string, unknown>`.
52
+ - `SchemaGeneratorContext` — resource-level context threaded at boot (`idField`, `resourceName`).
53
+ - `isSchemaGenerator(value)` — runtime predicate (arity 1-3 functions). Conservative — doesn't invoke.
54
+
55
+ Each kit ships a compile-time conformance check (same playbook as mongokit's `RepositoryLike` conformance gate):
56
+
57
+ ```ts
58
+ const _conformance: SchemaGenerator<Model<unknown>> = buildCrudSchemasFromModel;
59
+ ```
60
+
61
+ Drift surfaces in the kit's typecheck immediately, before any consumer sees it.
62
+
63
+ 10 new tests in `tests/unit/schema/generator.test.ts`. Total repo-core: 293 → 303.
64
+
65
+ ### Added — `errors` module: canonical wire + throwable error contract
66
+
67
+ `@classytic/repo-core/errors` is now the single source of truth for error contracts across the org. Two complementary shapes:
68
+
69
+ - **`HttpError extends Error`** — the *throwable* shape. Plain `Error` with `status`, optional `code`, `meta`, `validationErrors`, `duplicate`. Kits classify their driver-specific errors into this shape; framework layers (arc) catch and serialize. Existing `HttpError` extended in 0.3 with `code?: string` and `meta?: Record<string, unknown>` (mongokit had these locally pre-3.12).
70
+ - **`ErrorContract`** — the *wire* shape (RFC 7807 / Stripe-style). What gets serialized to JSON responses, dead-letter records, audit trails, inter-service envelopes. Flat top-level `code` / `message` / `status` matches the org-wide `{ success, ... }` envelope convention.
71
+ - **`ErrorDetail`** — single field-scoped error (path / code / message). `ErrorContract.details` is `ReadonlyArray<ErrorDetail>`.
72
+ - **`ERROR_CODES` + `ErrorCode`** — canonical lowercase + snake_case codes (`'validation_error'`, `'not_found'`, `'conflict'`, `'unauthorized'`, `'forbidden'`, `'rate_limited'`, `'idempotency_conflict'`, `'precondition_failed'`, `'internal_error'`, `'service_unavailable'`, `'timeout'`). Domain packages extend hierarchically (`'order.validation.missing_line'`).
73
+ - **`toErrorContract(error)`** — converts any `Error` / `HttpError` / non-`Error` value to the canonical wire `ErrorContract`. `code` cascade: explicit `error.code` → status-derived → `'internal_error'`. Flattens mongokit-shaped `validationErrors` and `duplicate.fields` into the canonical `details[]` array.
74
+ - **`statusToErrorCode(status)`** — well-known HTTP status → canonical code. Conservative mapping; unknown statuses fall through to `'internal_error'` so domain handlers explicitly opt in.
75
+
76
+ Consumed by mongokit (drops local `HttpError`), arc (`ArcError implements HttpError` with `status` getter), and any future kit / service. Relocated from `@classytic/primitives/errors` (which had `ErrorContract` + `ERROR_CODES` but not the throwable contract) — same playbook as the pagination, tenant, and events relocations: errors are infrastructure-shaped, not domain primitives.
77
+
78
+ 14 new tests in `tests/unit/errors/contract.test.ts`. Total repo-core: 279 → 293.
79
+
80
+ ### Added — `tenant` subpath (canonical home for tenant scope contract)
81
+
82
+ New subpath `@classytic/repo-core/tenant` ships:
83
+ - `TenantConfig` — static config (`strategy`, `enabled`, `tenantField`, `fieldType`, `ref`, `contextKey`, `required`, `resolve`).
84
+ - `TenantStrategy = 'field' | 'none' | 'custom'`, `TenantFieldType = 'objectId' | 'string'`.
85
+ - `ResolvedTenantConfig` — the resolved-with-defaults shape returned by `resolveTenantConfig`.
86
+ - `DEFAULT_TENANT_CONFIG` — sensible org-wide defaults (`tenantField: 'organizationId'`, `fieldType: 'objectId'`, `ref: 'organization'`, `required: true`).
87
+ - `resolveTenantConfig(config?)` — normaliser; validates `'custom'` strategy requires `resolve`.
88
+
89
+ Relocated from `@classytic/primitives/tenant` (which has been removed in primitives 0.3 cleanup). Tenant scope is **infrastructure-shaped** — describes how queries get scoped, not a domain primitive like Money or Address. Repo-core is its proper home: it sits next to `context`, `filter`, `hooks`, `schema`, `cache` — every other repository contract — and lets mongokit / sqlitekit / future kits consume it through the existing `@classytic/repo-core` peer dep without pulling primitives just for one type.
90
+
91
+ **Custom tenancy escape hatch** unchanged: `strategy: 'custom'` + `resolve: (ctx) => filterShape` covers multi-field composites, region+partner shards, hash-derived filters, anything that doesn't fit `field === id`.
92
+
93
+ 14 new tests in `tests/unit/tenant/resolve.test.ts` (ported from primitives' suite). Total repo-core: 265 → 279 tests.
94
+
95
+ ### Added — schema-builder vocabulary
96
+
97
+ - **`SchemaBuilderOptions.excludeFields`** — global field exclusion. Fields listed here are dropped from create / update / response schemas in one place. Equivalent to setting `create.omitFields`, `update.omitFields`, AND `response.omitFields` to the same list. Use for fields that should never appear in any HTTP-facing schema.
98
+ - **`SchemaBuilderOptions.response`** with `omitFields?: string[]` — response-schema overrides. Drops extra fields from the response shape without marking them globally hidden.
99
+ - **`CrudSchemas.response?: JsonSchema`** — optional response-shape schema. Includes server-set fields (`createdAt`, `updatedAt`, `_id`, immutable / readonly / systemManaged) since those ARE returned to clients. Only `fieldRules[field].hidden: true` strips automatically. Set `additionalProperties: true` so virtuals / computed fields pass through.
100
+ - **`FieldRule.hidden?: boolean`** — strips the field from the response shape. Distinct from `systemManaged` (request-body concern). Use for passwords, secrets, internal scoring.
101
+ - **`collectFieldsToOmit(options, 'response')`** — third purpose alongside `'create'` / `'update'`. Implements the response policy (only `hidden` + `excludeFields` + `response.omitFields`).
102
+
103
+ These are the contracts mongokit 3.12 implements, arc 2.12's MongooseAdapter consumes, and any future kit (sqlitekit, prismakit) inherits for free.
104
+
105
+ ### Hardened — `RepositoryBase` plugin-shape validation
106
+
107
+ `RepositoryBase.use()` and the constructor's plugin loop now reject malformed plugin entries up front via `assertValidPlugin()`. The motivating field bug: `new Repository(Model, ['organizationId'], opts)` — passing a tenant-field string array where the constructor expected `plugins[]` — used to crash deep in the call site with `TypeError: plugin.apply is not a function`, cascade-failing every test that booted the app. The validator now throws a single descriptive `TypeError` at construction with the offending index and a hint about the common `tenantField`-in-the-wrong-slot mistake:
108
+
109
+ ```
110
+ [repo-core] Repository "Foo": plugin at index 0 has wrong type.
111
+ Expected a function or { name, apply(repo) } object — got string 'organizationId'.
112
+ Common cause: `new Repository(Model, [tenantField], opts)` — second argument must be a plugins array.
113
+ ```
114
+
115
+ Lock-in: `tests/unit/repository/base-plugin-validation.test.ts` (6 cases covering string/null/object-without-apply/function/object/post-construction `use()` paths).
116
+
117
+ ### Migration — mongokit 4.0, arc 2.12, arc-next 0.6
118
+
119
+ Three downstream changes drop their local copies and import directly:
120
+
121
+ 1. **mongokit 4.0** — deletes its local `AggregatePaginationResult` declaration and `PaginationResult` union; consumers that imported them from `@classytic/mongokit` must switch to `@classytic/repo-core/pagination`. (Breaking.)
122
+ 2. **arc 2.12** — `fastifyAdapter` calls `toCanonicalList()` once instead of inline-flattening offset and falling through keyset/aggregate as nested `data`. Closes a real wire-envelope-mismatch bug.
123
+ 3. **arc-next 0.6** — adds `@classytic/repo-core` as peer dep, deletes its local `OffsetPaginationResponse` / `KeysetPaginationResponse` / `AggregatePaginationResponse` / `PaginatedResponse` types. Server and client now share one declaration — the `method` field asymmetry is impossible by construction.
124
+
125
+ No breaking changes inside `@classytic/repo-core` itself — purely additive.
126
+
127
+ ## [0.2.0] - 2026-04-22
128
+
129
+ ### Added — Update IR (portable write-side primitive)
130
+
131
+ - **New `@classytic/repo-core/update` subpath.** The write-side counterpart to `@classytic/repo-core/filter`. Plugins and arc's infrastructure stores compose an `UpdateSpec` once; each kit compiles it to its native shape.
132
+ - **Types:** `UpdateSpec` (tagged union on `op: 'update'`, four buckets: `set` / `unset` / `setOnInsert` / `inc`), `UpdateInput` (union of `UpdateSpec` | kit-native `Record<string, unknown>` | Mongo pipeline `Record<string, unknown>[]`).
133
+ - **Builders:** `update({ set, unset, setOnInsert, inc })` (root), `setFields`, `unsetFields(...f)`, `setOnInsertFields`, `incFields`, `combineUpdates(...specs)` (later-wins merge, `unset` de-duplicates).
134
+ - **Guards:** `isUpdateSpec` (routes portable IR to the compiler), `isUpdatePipeline` (lets SQL kits short-circuit with `UnsupportedOperationError`).
135
+ - **Compilers:** `compileUpdateSpecToMongo(spec)` emits `{ $set, $unset, $setOnInsert, $inc }`. `compileUpdateSpecToSql(spec)` emits a `SqlUpdatePlan` with `data` / `unset` / `inc` / `insertDefaults` buckets, leaving SQL generation (quoting, `ON CONFLICT`, parameter binding) to the kit.
136
+ - **`StandardRepo.findOneAndUpdate` + `updateMany` widened to `UpdateInput`.** Accepts all three forms — portable `UpdateSpec`, kit-native record, Mongo aggregation pipeline. Kits dispatch with `isUpdateSpec`. The existing raw-record and pipeline paths remain unchanged; the IR is purely additive so consumers don't need to migrate. Arc's infrastructure stores (outbox, idempotency, audit) will switch to the IR over a subsequent release to close the "Mongo-shaped store" gap flagged in the April 2026 cross-surface review.
137
+
138
+ **Motivation (Arc April 2026 review):** arc's `EventOutbox`, `IdempotencyStore`, and `AuditStore` adapters use Mongo operator records (`$set`, `$inc`, `$unset`, `$setOnInsert`, `$or`, `$lte`, ...) directly against `RepositoryLike.findOneAndUpdate`. That works on mongokit but fails on sqlitekit — whose `findOneAndUpdate` treats `data` as flat column overwrites and would literally set a column named `$set`. The Update IR closes the gap without forcing every kit to ship its own Mongo-operator compatibility layer.
139
+
140
+ **Rationale for scope:** the IR covers the subset every backend supports (atomic set / unset / inc / insert-default). Kit-native features — Mongo `$push`/`$pull`/`$addToSet`, aggregation pipeline updates, Postgres `jsonb_set`, SQL `CASE` expressions — stay on the kit-native path via `UpdateInput`'s raw-record and pipeline forms. No lowest-common-denominator bloat; no feature loss for kits that already offer more.
141
+
142
+ **Test delta**: 193 → 230 tests (37 new across `tests/unit/update/builders`, `/guard`, `/compile`).
143
+
144
+ ### Changed — breaking: `StandardRepo` write signatures
145
+
146
+ `StandardRepo.findOneAndUpdate(filter, update, ...)` and `updateMany(filter, data, ...)` — the second parameter is now typed `UpdateInput` (was `Record<string, unknown> | Record<string, unknown>[]` and `Record<string, unknown>` respectively). Every call site that compiled against 0.1.0 keeps compiling: `Record<string, unknown>` and `Record<string, unknown>[]` are subtypes of `UpdateInput`. The break is on the **implementer** side — any kit that declared only the old parameter type no longer structurally satisfies `StandardRepo` under strict contravariance. mongokit 3.11.0 and sqlitekit 0.1.1 already ship with the widened signatures; third-party kits need to widen before bumping their `@classytic/repo-core` peer dep.
147
+
148
+ ### Changed — breaking: `updateMany` + `deleteMany` promoted to required members of `StandardRepo`
149
+
150
+ Both methods were optional (`updateMany?` / `deleteMany?`) in 0.1.0; they're required in 0.2.0. Rationale: every real backend has a native bulk-update and bulk-delete primitive, and arc's infrastructure stores (outbox, idempotency, audit cleanup) assume both are callable without feature-detection. Leaving them optional invited the "forgot to wire `batchOperationsPlugin`" runtime `TypeError` footgun that earlier releases of mongokit shipped — with the promotion, the type system catches missing implementations at the kit boundary.
151
+
152
+ **Impact**:
153
+ - **Kits**: any kit declaring `class FooRepo<T> implements StandardRepo<T>` must provide `updateMany` and `deleteMany` or fail to compile. mongokit 3.11.0 and sqlitekit 0.1.1 both ship these as class primitives, so their conformance stays green.
154
+ - **Consumers of `RepositoryLike<T> = MinimalRepo<T> & Partial<StandardRepo<T>>`** (arc's pattern) are unaffected — `Partial` reimposes optionality for feature detection at the arc adapter boundary. `if (repo.updateMany)` guards keep working.
155
+ - `bulkWrite` stays optional — the mongoose-shaped `BulkWriteOperation` has no clean SQL analogue, and every kit would ship an uninteresting fan-out wrapper otherwise.
156
+
157
+ ### Naming — `UpdateInput` collision with mongokit
158
+
159
+ `@classytic/repo-core/update` exports `UpdateInput` as the union `UpdateSpec | Record<string, unknown> | Record<string, unknown>[]`. `@classytic/mongokit` currently **also** exports a type named `UpdateInput<TDoc> = Partial<Omit<TDoc, '_id' | 'createdAt' | '__v'>>` — a completely different, document-typed shape used by `repo.update(id, data)`. If you write `import { UpdateInput } from '@classytic/mongokit'`, you get mongokit's generic; if you write `import type { UpdateInput } from '@classytic/repo-core/update'`, you get the union. Both names may appear in the same consumer file — import at least one with an alias (`import type { UpdateInput as UpdatePatch } from '@classytic/mongokit'`). mongokit will rename its local type in a follow-up release to close the collision permanently.
8
160
 
9
161
  ### Added
10
162
  - Phase 0 scaffold: package.json, tsconfig, tsdown, biome, vitest (4-tier), knip
package/README.md CHANGED
@@ -28,14 +28,34 @@ import { and, eq, gte, in_, like, buildTenantScope, matchFilter } from '@classyt
28
28
  // URL → ParsedQuery grammar. Backend frameworks (Express/Arc/Fastify) parse req.query here.
29
29
  import { parseUrl } from '@classytic/repo-core/query-parser';
30
30
 
31
- // Pagination primitives — cursor codec, keyset helpers, offset math.
32
- import { encodeCursor, decodeCursor, validateKeysetSort } from '@classytic/repo-core/pagination';
31
+ // Pagination primitives — cursor codec, keyset helpers, offset math, the canonical
32
+ // result types (`OffsetPaginationResult`, `KeysetPaginationResult`,
33
+ // `AggregatePaginationResult`, `PaginationResult`) and the wire helper
34
+ // `toCanonicalList()`. Single source of truth — primitives' duplicate dropped,
35
+ // mongokit/sqlitekit re-export from here.
36
+ import { encodeCursor, decodeCursor, validateKeysetSort, toCanonicalList } from '@classytic/repo-core/pagination';
37
+ import type { OffsetPaginationResult, KeysetPaginationResult, AggregatePaginationResult, PaginationResult } from '@classytic/repo-core/pagination';
38
+
39
+ // Tenant config — the canonical `TenantConfig`, `TenantStrategy`, `TenantFieldType`,
40
+ // `resolveTenantConfig`, `DEFAULT_TENANT_CONFIG`, `ResolvedTenantConfig`. Kits'
41
+ // `MultiTenantOptions extends Pick<TenantConfig, ...>`.
42
+ import { resolveTenantConfig, DEFAULT_TENANT_CONFIG } from '@classytic/repo-core/tenant';
43
+ import type { TenantConfig, ResolvedTenantConfig } from '@classytic/repo-core/tenant';
33
44
 
34
45
  // Cache plumbing — the CacheAdapter interface every kit's cachePlugin writes against.
35
46
  import { type CacheAdapter, stableStringify, createMemoryCacheAdapter } from '@classytic/repo-core/cache';
36
47
 
37
- // HTTP error envelope + duplicate-key contract.
38
- import { createError, conservativeMongoIsDuplicateKey } from '@classytic/repo-core/errors';
48
+ // Error contracts `HttpError` throwable + `ErrorContract` wire shape +
49
+ // `ErrorDetail` + `ErrorCode` + `ERROR_CODES` + `toErrorContract()` +
50
+ // `statusToErrorCode()`. Single source of truth — primitives' errors module dropped,
51
+ // mongokit's local `HttpError` dropped, `ArcError implements HttpError`.
52
+ import { toErrorContract, statusToErrorCode, ERROR_CODES, createError, conservativeMongoIsDuplicateKey } from '@classytic/repo-core/errors';
53
+ import type { HttpError, ErrorContract, ErrorDetail, ErrorCode } from '@classytic/repo-core/errors';
54
+
55
+ // Schema generator interface — kits ship `SchemaGenerator<TModel>` + the
56
+ // compile-time conformance assertion; arc adapters are typed against it.
57
+ import type { SchemaGenerator, SchemaGeneratorContext } from '@classytic/repo-core/schema';
58
+ import { isSchemaGenerator } from '@classytic/repo-core/schema';
39
59
 
40
60
  // Operation registry (for arc-level policy dispatch + doc generation).
41
61
  import { CORE_OP_REGISTRY, describe } from '@classytic/repo-core/operations';
@@ -141,11 +161,12 @@ Default `TExtra` is `Record<string, never>` — `OffsetPaginationResult<User>` b
141
161
 
142
162
  ## Status
143
163
 
144
- **v0.1.0 — initial release.**
164
+ **v0.3.0 — canonical contracts release.** Pagination types + wire envelope, tenant config, error contracts, and the `SchemaGenerator<TModel>` interface relocated from primitives / mongokit / arc to single sources of truth here.
145
165
 
146
166
  Consumed by:
147
- - `@classytic/mongokit` ≥ 3.10 — `Repository extends RepositoryBase`; hook engine, plugin-order validator, `HOOK_PRIORITY` sourced from repo-core. Mongokit's own `QueryParser` remains standalone (emits Mongo `$`-objects) but implements the same URL grammar by convention.
148
- - `@classytic/sqlitekit` (in development) — `SqliteRepository extends RepositoryBase`; Filter IR compiled to Drizzle / raw SQL natively. Uses `parseUrl` directly for URL parsing.
167
+ - `@classytic/mongokit` ≥ 3.12 — `Repository extends RepositoryBase`; hook engine, plugin-order validator, `HOOK_PRIORITY` sourced from repo-core. Pagination + `HttpError` types now flow from repo-core (mongokit's local copies dropped). `MultiTenantOptions extends Pick<TenantConfig, ...>`. `buildCrudSchemasFromModel` ships a compile-time `SchemaGenerator<TModel>` conformance assertion. Mongokit's own `QueryParser` remains standalone.
168
+ - `@classytic/sqlitekit` 0.2 — `SqliteRepository extends RepositoryBase`; Filter IR compiled to Drizzle / raw SQL natively. `MultiTenantOptions extends Pick<TenantConfig, ...>`. `buildCrudSchemasFromTable` ships the same `SchemaGenerator` conformance assertion.
169
+ - `@classytic/arc` ≥ 2.12 — adapters typed against `SchemaGenerator<TModel>`; `ArcError implements HttpError`; pagination wire envelope (`method` discriminant) emitted via `toCanonicalList()` with `reply.sendList()`.
149
170
 
150
171
  See [INFRA.md](./INFRA.md) for the architectural principles, subpath map, build/tooling decisions, and the roadmap for pgkit / prismakit.
151
172
 
@@ -0,0 +1,37 @@
1
+ import { ErrorCode, ErrorContract } from "./types.mjs";
2
+
3
+ //#region src/errors/contract.d.ts
4
+ /**
5
+ * Map an HTTP status code to the canonical {@link ErrorCode}. Used as a
6
+ * fallback when a thrown `HttpError` lacks an explicit `code` field.
7
+ *
8
+ * The mapping is conservative: only well-known status codes get a
9
+ * canonical code; unmapped statuses (406, 422, 410, ...) flow through as
10
+ * `'internal_error'` because there's no portable HTTP-Spec mapping
11
+ * everyone agrees on. Domain handlers should set `code` explicitly for
12
+ * any non-default status.
13
+ */
14
+ declare function statusToErrorCode(status: number): ErrorCode;
15
+ /**
16
+ * Convert a throwable {@link HttpError} (or any `Error` with a `status`
17
+ * field) into the canonical {@link ErrorContract} wire shape.
18
+ *
19
+ * `code` cascade:
20
+ * 1. `error.code` (explicit machine code on the throwable) — preferred.
21
+ * 2. {@link statusToErrorCode}(`error.status`) — derived from status.
22
+ * 3. `'internal_error'` — fallback for plain `Error` without status.
23
+ *
24
+ * `validationErrors` (mongokit-shaped throwable field) is mapped into the
25
+ * canonical `details` array so wire consumers see one shape regardless
26
+ * of which kit threw the error. Each `validationErrors[i]` becomes an
27
+ * `ErrorDetail` with `code: validator`, `message: error`, `path` left
28
+ * unset (kits that have field paths set them in their own ErrorDetail
29
+ * mapping).
30
+ *
31
+ * `duplicate.fields` is similarly flattened into `details` with the
32
+ * duplicate-key code so unique-constraint failures look uniform on the
33
+ * wire.
34
+ */
35
+ declare function toErrorContract(error: unknown): ErrorContract;
36
+ //#endregion
37
+ export { statusToErrorCode, toErrorContract };
@@ -0,0 +1,75 @@
1
+ import { ERROR_CODES } from "./types.mjs";
2
+ //#region src/errors/contract.ts
3
+ /**
4
+ * Map an HTTP status code to the canonical {@link ErrorCode}. Used as a
5
+ * fallback when a thrown `HttpError` lacks an explicit `code` field.
6
+ *
7
+ * The mapping is conservative: only well-known status codes get a
8
+ * canonical code; unmapped statuses (406, 422, 410, ...) flow through as
9
+ * `'internal_error'` because there's no portable HTTP-Spec mapping
10
+ * everyone agrees on. Domain handlers should set `code` explicitly for
11
+ * any non-default status.
12
+ */
13
+ function statusToErrorCode(status) {
14
+ switch (status) {
15
+ case 400: return ERROR_CODES.VALIDATION;
16
+ case 401: return ERROR_CODES.UNAUTHORIZED;
17
+ case 403: return ERROR_CODES.FORBIDDEN;
18
+ case 404: return ERROR_CODES.NOT_FOUND;
19
+ case 409: return ERROR_CODES.CONFLICT;
20
+ case 412: return ERROR_CODES.PRECONDITION_FAILED;
21
+ case 429: return ERROR_CODES.RATE_LIMITED;
22
+ case 503: return ERROR_CODES.UNAVAILABLE;
23
+ case 504: return ERROR_CODES.TIMEOUT;
24
+ default: return ERROR_CODES.INTERNAL;
25
+ }
26
+ }
27
+ /**
28
+ * Convert a throwable {@link HttpError} (or any `Error` with a `status`
29
+ * field) into the canonical {@link ErrorContract} wire shape.
30
+ *
31
+ * `code` cascade:
32
+ * 1. `error.code` (explicit machine code on the throwable) — preferred.
33
+ * 2. {@link statusToErrorCode}(`error.status`) — derived from status.
34
+ * 3. `'internal_error'` — fallback for plain `Error` without status.
35
+ *
36
+ * `validationErrors` (mongokit-shaped throwable field) is mapped into the
37
+ * canonical `details` array so wire consumers see one shape regardless
38
+ * of which kit threw the error. Each `validationErrors[i]` becomes an
39
+ * `ErrorDetail` with `code: validator`, `message: error`, `path` left
40
+ * unset (kits that have field paths set them in their own ErrorDetail
41
+ * mapping).
42
+ *
43
+ * `duplicate.fields` is similarly flattened into `details` with the
44
+ * duplicate-key code so unique-constraint failures look uniform on the
45
+ * wire.
46
+ */
47
+ function toErrorContract(error) {
48
+ if (!(error instanceof Error)) return {
49
+ code: "internal_error",
50
+ message: typeof error === "string" ? error : "Internal error",
51
+ status: 500
52
+ };
53
+ const e = error;
54
+ const status = typeof e.status === "number" ? e.status : 500;
55
+ const contract = {
56
+ code: e.code ?? statusToErrorCode(status),
57
+ message: e.message || "Internal error",
58
+ status
59
+ };
60
+ const details = [];
61
+ if (Array.isArray(e.validationErrors)) for (const v of e.validationErrors) details.push({
62
+ code: v.validator,
63
+ message: v.error
64
+ });
65
+ if (e.duplicate?.fields?.length) for (const field of e.duplicate.fields) details.push({
66
+ path: field,
67
+ code: "duplicate_key",
68
+ message: `Duplicate value for "${field}"`
69
+ });
70
+ if (details.length > 0) contract.details = details;
71
+ if (e.meta) contract.meta = e.meta;
72
+ return contract;
73
+ }
74
+ //#endregion
75
+ export { statusToErrorCode, toErrorContract };
@@ -1,4 +1,5 @@
1
- import { DuplicateKeyMeta, HttpError, ValidationErrorMeta } from "./types.mjs";
1
+ import { DuplicateKeyMeta, ERROR_CODES, ErrorCode, ErrorContract, ErrorDetail, HttpError, ValidationErrorMeta } from "./types.mjs";
2
+ import { statusToErrorCode, toErrorContract } from "./contract.mjs";
2
3
  import { createError, isHttpError } from "./create-error.mjs";
3
4
  import { IsDuplicateKeyErrorFn, ToDuplicateKeyHttpErrorOptions, conservativeMongoIsDuplicateKey, toDuplicateKeyHttpError } from "./duplicate-key.mjs";
4
- export { type DuplicateKeyMeta, type HttpError, type IsDuplicateKeyErrorFn, type ToDuplicateKeyHttpErrorOptions, type ValidationErrorMeta, conservativeMongoIsDuplicateKey, createError, isHttpError, toDuplicateKeyHttpError };
5
+ export { type DuplicateKeyMeta, ERROR_CODES, type ErrorCode, type ErrorContract, type ErrorDetail, type HttpError, type IsDuplicateKeyErrorFn, type ToDuplicateKeyHttpErrorOptions, type ValidationErrorMeta, conservativeMongoIsDuplicateKey, createError, isHttpError, statusToErrorCode, toDuplicateKeyHttpError, toErrorContract };
@@ -1,3 +1,5 @@
1
+ import { ERROR_CODES } from "./types.mjs";
2
+ import { statusToErrorCode, toErrorContract } from "./contract.mjs";
1
3
  import { createError, isHttpError } from "./create-error.mjs";
2
4
  import { conservativeMongoIsDuplicateKey, toDuplicateKeyHttpError } from "./duplicate-key.mjs";
3
- export { conservativeMongoIsDuplicateKey, createError, isHttpError, toDuplicateKeyHttpError };
5
+ export { ERROR_CODES, conservativeMongoIsDuplicateKey, createError, isHttpError, statusToErrorCode, toDuplicateKeyHttpError, toErrorContract };
@@ -1,12 +1,28 @@
1
1
  //#region src/errors/types.d.ts
2
2
  /**
3
- * HTTP-shaped error contract used across every driver kit.
3
+ * HTTP-shaped error contracts used across every driver kit, integrator,
4
+ * and HTTP-emitting service in the org.
4
5
  *
5
- * An `HttpError` is a plain `Error` with a `status` field and optional
6
- * structured fields for duplicate-key and validation conflicts. Kits
7
- * classify their driver-specific errors into this shape at the boundary,
8
- * so the framework layer (arc) never needs to know whether the error
9
- * originated from MongoDB `E11000`, Postgres `23505`, or Prisma `P2002`.
6
+ * **`@classytic/repo-core/errors` is the canonical home for the wire +
7
+ * throwable error contract.** Two complementary shapes live here:
8
+ *
9
+ * - {@link HttpError} the *throwable* shape. Plain `Error` with
10
+ * `status` and optional structured fields. Kits classify their
11
+ * driver-specific errors into this shape at the boundary; framework
12
+ * layers (arc) catch and serialize.
13
+ * - {@link ErrorContract} — the *wire* shape (RFC 7807 / Stripe-style).
14
+ * What gets serialized into JSON responses, queue dead-letter records,
15
+ * audit logs, and inter-service error envelopes.
16
+ *
17
+ * Throwable classes (arc's `ArcError` family, future `BillingError` etc.)
18
+ * `implements HttpError` and serialize to `ErrorContract` for the wire.
19
+ * One contract, one canonical home, every package follows the same shape.
20
+ *
21
+ * **Custom-domain escape hatch.** {@link ErrorCode} is a documented union
22
+ * of canonical codes; `code: string` accepts ANY string so domain packages
23
+ * can extend (`'order.validation.missing_line'`, `'payment.gateway.timeout'`).
24
+ * The canonical codes cover cross-cutting concerns; domain extensions
25
+ * hierarchically narrow.
10
26
  */
11
27
  /** Structured metadata for duplicate-key (unique-constraint) errors. */
12
28
  interface DuplicateKeyMeta {
@@ -24,14 +40,103 @@ interface ValidationErrorMeta {
24
40
  validator: string;
25
41
  error: string;
26
42
  }
27
- /** HTTP-shaped error — the envelope every repository error resolves to. */
43
+ /**
44
+ * HTTP-shaped error — the throwable envelope every repository error and
45
+ * arc handler resolves to.
46
+ *
47
+ * Note: `code` and `meta` are optional but every long-lived production
48
+ * handler should populate them. `code` lets clients switch on
49
+ * machine-readable identifiers without grepping `message`; `meta` carries
50
+ * structured diagnostics safe for logs.
51
+ */
28
52
  interface HttpError extends Error {
29
53
  /** HTTP status code (400, 404, 409, 500, ...). */
30
54
  status: number;
55
+ /**
56
+ * Stable machine-readable error code. Use one of {@link ErrorCode} for
57
+ * cross-cutting cases or extend hierarchically with a domain prefix
58
+ * (`'order.validation.missing_line'`, `'payment.gateway.timeout'`).
59
+ * Hosts switch on this in catch blocks instead of regex-matching
60
+ * `message`.
61
+ */
62
+ code?: string;
63
+ /**
64
+ * Free-form structured metadata for diagnostics. Pairs with `code` so
65
+ * hosts can render a clearer message in their own UI without parsing
66
+ * `message`. Safe for logs (don't include PII).
67
+ */
68
+ meta?: Record<string, unknown>;
31
69
  /** Structured validation failures when `status` is 400. */
32
70
  validationErrors?: ValidationErrorMeta[];
33
71
  /** Structured duplicate-key metadata when `status` is 409. */
34
72
  duplicate?: DuplicateKeyMeta;
35
73
  }
74
+ /**
75
+ * Standard error contract — a framework-agnostic JSON shape that maps
76
+ * cleanly to HTTP responses, worker failure logs, and inter-service
77
+ * errors. Loosely matches RFC 7807 (`application/problem+json`); shape
78
+ * matches Stripe / Shopify / Slack API conventions.
79
+ *
80
+ * Packages throw `HttpError` instances. Hosts (HTTP adapters, workers)
81
+ * serialize those errors into this shape on the wire via
82
+ * {@link toErrorContract}. Wire shape is FLAT (top-level `code` /
83
+ * `message` / `status`), not nested under `{ error: { ... } }` — matches
84
+ * the existing org-wide envelope convention. Hosts that need a
85
+ * Stripe-style nested envelope wrap once at the edge.
86
+ */
87
+ interface ErrorContract {
88
+ /** Machine-readable, hierarchical code — e.g. `'order.validation.missing_line'`. */
89
+ code: string;
90
+ /** Human-readable, safe-for-client message. */
91
+ message: string;
92
+ /** Suggested HTTP status code — hosts may override. */
93
+ status?: number;
94
+ /**
95
+ * Field-scoped structured details. Populated for validation failures
96
+ * (one entry per offending field) or domain errors that map to multiple
97
+ * sub-codes. Distinct from `HttpError.validationErrors` (which is a
98
+ * mongokit-shaped throwable field) — the wire form is canonical.
99
+ */
100
+ details?: readonly ErrorDetail[];
101
+ /** Correlation / trace identifier for support lookups. */
102
+ correlationId?: string;
103
+ /** Non-PII metadata (safe to log, safe to return to clients). */
104
+ meta?: Readonly<Record<string, unknown>>;
105
+ }
106
+ /** A single field-scoped error detail. */
107
+ interface ErrorDetail {
108
+ /** Dot-path pointer to the offending field, e.g. `'lines.0.quantity'`. */
109
+ path?: string;
110
+ code: string;
111
+ message: string;
112
+ meta?: Readonly<Record<string, unknown>>;
113
+ }
114
+ /**
115
+ * Cross-cutting error codes used across the org. Every canonical code is
116
+ * lowercase + snake_case to match RFC 7807, Stripe, and Shopify
117
+ * conventions. Domain packages add their own hierarchical codes
118
+ * (`'order.validation.*'`, `'payment.gateway.*'`); these cover the
119
+ * universal cases every HTTP-emitting layer needs.
120
+ *
121
+ * **Arc compatibility note.** Arc's `ArcError` hierarchy historically
122
+ * uses UPPER_SNAKE codes (`'NOT_FOUND'`, `'VALIDATION_ERROR'`) for
123
+ * back-compat with hosts that switch on those values. New code should
124
+ * prefer the canonical lowercase codes; arc keeps emitting its UPPER_SNAKE
125
+ * codes on the wire so existing client switches keep working.
126
+ */
127
+ declare const ERROR_CODES: {
128
+ readonly VALIDATION: "validation_error";
129
+ readonly NOT_FOUND: "not_found";
130
+ readonly CONFLICT: "conflict";
131
+ readonly UNAUTHORIZED: "unauthorized";
132
+ readonly FORBIDDEN: "forbidden";
133
+ readonly RATE_LIMITED: "rate_limited";
134
+ readonly IDEMPOTENCY_CONFLICT: "idempotency_conflict";
135
+ readonly PRECONDITION_FAILED: "precondition_failed";
136
+ readonly INTERNAL: "internal_error";
137
+ readonly UNAVAILABLE: "service_unavailable";
138
+ readonly TIMEOUT: "timeout";
139
+ };
140
+ type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
36
141
  //#endregion
37
- export { DuplicateKeyMeta, HttpError, ValidationErrorMeta };
142
+ export { DuplicateKeyMeta, ERROR_CODES, ErrorCode, ErrorContract, ErrorDetail, HttpError, ValidationErrorMeta };
@@ -0,0 +1,29 @@
1
+ //#region src/errors/types.ts
2
+ /**
3
+ * Cross-cutting error codes used across the org. Every canonical code is
4
+ * lowercase + snake_case to match RFC 7807, Stripe, and Shopify
5
+ * conventions. Domain packages add their own hierarchical codes
6
+ * (`'order.validation.*'`, `'payment.gateway.*'`); these cover the
7
+ * universal cases every HTTP-emitting layer needs.
8
+ *
9
+ * **Arc compatibility note.** Arc's `ArcError` hierarchy historically
10
+ * uses UPPER_SNAKE codes (`'NOT_FOUND'`, `'VALIDATION_ERROR'`) for
11
+ * back-compat with hosts that switch on those values. New code should
12
+ * prefer the canonical lowercase codes; arc keeps emitting its UPPER_SNAKE
13
+ * codes on the wire so existing client switches keep working.
14
+ */
15
+ const ERROR_CODES = {
16
+ VALIDATION: "validation_error",
17
+ NOT_FOUND: "not_found",
18
+ CONFLICT: "conflict",
19
+ UNAUTHORIZED: "unauthorized",
20
+ FORBIDDEN: "forbidden",
21
+ RATE_LIMITED: "rate_limited",
22
+ IDEMPOTENCY_CONFLICT: "idempotency_conflict",
23
+ PRECONDITION_FAILED: "precondition_failed",
24
+ INTERNAL: "internal_error",
25
+ UNAVAILABLE: "service_unavailable",
26
+ TIMEOUT: "timeout"
27
+ };
28
+ //#endregion
29
+ export { ERROR_CODES };
@@ -0,0 +1,35 @@
1
+ import { AnyPaginationResult, BareListResponse, PaginatedResponse } from "./types.mjs";
2
+
3
+ //#region src/pagination/canonical.d.ts
4
+ /**
5
+ * Type guard: is this value a paginated result envelope (vs a bare array
6
+ * or some other shape)?
7
+ *
8
+ * Checks for the `method` discriminant rather than `Array.isArray` so a
9
+ * paginated result whose `docs` field happens to contain zero items still
10
+ * routes through the paginated branch.
11
+ *
12
+ * Accepts `unknown` (rather than `T[] | AnyPaginationResult<T>`) so wire-
13
+ * boundary callers can guard arbitrary inputs without pre-narrowing — the
14
+ * arc / arc-next response pipeline routinely sees `{ docs: unknown[] }`
15
+ * shapes that are neither a bare array nor a paginated result, and forcing
16
+ * those callers to cast first defeats the guard's purpose.
17
+ */
18
+ declare function isPaginatedResult<TDoc>(input: unknown): input is AnyPaginationResult<TDoc>;
19
+ /**
20
+ * Normalise a list-shaped value into the canonical wire envelope.
21
+ *
22
+ * Overloads keep the return type tight:
23
+ * - bare array → {@link BareListResponse}
24
+ * - paginated → {@link PaginatedResponse} (preserves method discriminant)
25
+ *
26
+ * The mutable-array overload widens to `TDoc[]` because that's the most
27
+ * common server input (kit results return `TDoc[]` for `docs`); the
28
+ * readonly overload covers callers passing `readonly TDoc[]`.
29
+ */
30
+ declare function toCanonicalList<TDoc>(input: TDoc[]): BareListResponse<TDoc>;
31
+ declare function toCanonicalList<TDoc>(input: readonly TDoc[]): BareListResponse<TDoc>;
32
+ declare function toCanonicalList<TDoc, TExtra extends Record<string, unknown>>(input: AnyPaginationResult<TDoc, TExtra>): PaginatedResponse<TDoc, TExtra>;
33
+ declare function toCanonicalList<TDoc>(input: readonly TDoc[] | AnyPaginationResult<TDoc>): PaginatedResponse<TDoc>;
34
+ //#endregion
35
+ export { isPaginatedResult, toCanonicalList };
@@ -0,0 +1,32 @@
1
+ //#region src/pagination/canonical.ts
2
+ /**
3
+ * Type guard: is this value a paginated result envelope (vs a bare array
4
+ * or some other shape)?
5
+ *
6
+ * Checks for the `method` discriminant rather than `Array.isArray` so a
7
+ * paginated result whose `docs` field happens to contain zero items still
8
+ * routes through the paginated branch.
9
+ *
10
+ * Accepts `unknown` (rather than `T[] | AnyPaginationResult<T>`) so wire-
11
+ * boundary callers can guard arbitrary inputs without pre-narrowing — the
12
+ * arc / arc-next response pipeline routinely sees `{ docs: unknown[] }`
13
+ * shapes that are neither a bare array nor a paginated result, and forcing
14
+ * those callers to cast first defeats the guard's purpose.
15
+ */
16
+ function isPaginatedResult(input) {
17
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return false;
18
+ const method = input.method;
19
+ return method === "offset" || method === "keyset" || method === "aggregate";
20
+ }
21
+ function toCanonicalList(input) {
22
+ if (isPaginatedResult(input)) return {
23
+ ...input,
24
+ success: true
25
+ };
26
+ return {
27
+ success: true,
28
+ docs: [...input]
29
+ };
30
+ }
31
+ //#endregion
32
+ export { isPaginatedResult, toCanonicalList };
@@ -1,5 +1,6 @@
1
- import { CursorPayload, DecodedCursor, KeysetPaginationResult, KeysetPaginationResultCore, OffsetPaginationResult, OffsetPaginationResultCore, PaginationConfig, SortDirection, SortSpec, ValueType } from "./types.mjs";
1
+ import { AggregatePaginationResponse, AggregatePaginationResult, AggregatePaginationResultCore, AnyPaginationResult, BareListResponse, CursorPayload, DecodedCursor, KeysetPaginationResponse, KeysetPaginationResult, KeysetPaginationResultCore, OffsetPaginationResponse, OffsetPaginationResult, OffsetPaginationResultCore, PaginatedResponse, PaginationConfig, SortDirection, SortSpec, ValueType } from "./types.mjs";
2
+ import { isPaginatedResult, toCanonicalList } from "./canonical.mjs";
2
3
  import { decodeCursor, encodeCursor, validateCursorSort, validateCursorVersion } from "./cursor.mjs";
3
4
  import { getPrimaryField, invertSort, normalizeSort, validateKeysetSort } from "./keyset.mjs";
4
5
  import { calculateSkip, calculateTotalPages, shouldWarnDeepPagination, validateLimit, validatePage } from "./offset.mjs";
5
- export { type CursorPayload, type DecodedCursor, type KeysetPaginationResult, type KeysetPaginationResultCore, type OffsetPaginationResult, type OffsetPaginationResultCore, type PaginationConfig, type SortDirection, type SortSpec, type ValueType, calculateSkip, calculateTotalPages, decodeCursor, encodeCursor, getPrimaryField, invertSort, normalizeSort, shouldWarnDeepPagination, validateCursorSort, validateCursorVersion, validateKeysetSort, validateLimit, validatePage };
6
+ export { type AggregatePaginationResponse, type AggregatePaginationResult, type AggregatePaginationResultCore, type AnyPaginationResult, type BareListResponse, type CursorPayload, type DecodedCursor, type KeysetPaginationResponse, type KeysetPaginationResult, type KeysetPaginationResultCore, type OffsetPaginationResponse, type OffsetPaginationResult, type OffsetPaginationResultCore, type PaginatedResponse, type PaginationConfig, type SortDirection, type SortSpec, type ValueType, calculateSkip, calculateTotalPages, decodeCursor, encodeCursor, getPrimaryField, invertSort, isPaginatedResult, normalizeSort, shouldWarnDeepPagination, toCanonicalList, validateCursorSort, validateCursorVersion, validateKeysetSort, validateLimit, validatePage };
@@ -1,4 +1,5 @@
1
+ import { isPaginatedResult, toCanonicalList } from "./canonical.mjs";
1
2
  import { decodeCursor, encodeCursor, validateCursorSort, validateCursorVersion } from "./cursor.mjs";
2
3
  import { getPrimaryField, invertSort, normalizeSort, validateKeysetSort } from "./keyset.mjs";
3
4
  import { calculateSkip, calculateTotalPages, shouldWarnDeepPagination, validateLimit, validatePage } from "./offset.mjs";
4
- export { calculateSkip, calculateTotalPages, decodeCursor, encodeCursor, getPrimaryField, invertSort, normalizeSort, shouldWarnDeepPagination, validateCursorSort, validateCursorVersion, validateKeysetSort, validateLimit, validatePage };
5
+ export { calculateSkip, calculateTotalPages, decodeCursor, encodeCursor, getPrimaryField, invertSort, isPaginatedResult, normalizeSort, shouldWarnDeepPagination, toCanonicalList, validateCursorSort, validateCursorVersion, validateKeysetSort, validateLimit, validatePage };