Conversation
- fix stale aws-sdk import and missing name field in facet examples - add pagination, FAQs, and best practices content - document TTL, write sharding, conditional puts, query filters, batch/retry limits, and the GSI1..GSI20 range
Replaces jest/ts-jest/ts-node/@types/jest/jest-junit with vitest and @vitest/coverage-v8. ts-node is no longer needed since vitest handles TypeScript natively. Vitest's built-in junit reporter replaces jest-junit; coverage now uses v8. Test file is unchanged — globals (describe/test/expect/beforeAll) stay available via globals: true and vitest/globals types. Also tightens engines.node to >=20.19.0 to match vite 8's floor.
Bumps typescript devDep from ^5.0.0 to ^6.0.0. TS 6 deprecates moduleResolution: "node" and requires module/moduleResolution to be paired, so both switch to "NodeNext" — still emits CommonJS because package.json has no "type": "module".
Adds an explicit include to tsconfig.json and excludes **/*.test.ts so test files no longer get compiled next to sources and picked up by package.json's files glob — previously lib/facet.test.js (~12 kB) and its .d.ts were shipping to npm. Moves vitest/globals out of the library tsconfig into a new tsconfig.test.json (extends the root, noEmit) so vitest's ambient declarations don't load during the library build. Adds a typecheck:tests script and wires it into CI so the test-only config can't silently break. Also drops the strict sub-flags (noImplicitThis, alwaysStrict, strictBindCallApply, strictNullChecks, strictFunctionTypes, strictPropertyInitialization) already implied by strict: true.
deleteBatch was inspecting `unprocessedRequest.PutRequest?.Item` to attribute failures, but a delete batch never contains PutRequest entries, so the branch was dead code and every item was reported as deleted even when DynamoDB had refused the write after five retries. Both deleteBatch and putBatch also looked at the original batchWriteItem response's UnprocessedItems instead of the post-retry list, so items that succeeded on retry were still reported as failed. Extract a shared `batchWriteWithRetry` helper that takes a small adapter describing how to translate the domain item to a WriteRequest (and back, for failure attribution). put and delete now each supply a 3-method adapter. Closes #31 and #51. Add `lib/batch-write.test.ts` regression tests that stub batchWriteItem with vi.spyOn — the existing integration tests never hit this path because DynamoDB Local does not throttle.
…conds
Facet#in computed a normalised `ttlAttribute` (Date → unix seconds,
numeric string → number) but then wrote `model[this.ttl]` to the
output record, discarding the normalisation. For Date TTLs the
converter marshals the value as `{ S: <iso-string> }`, which
DynamoDB's TTL reaper ignores — items silently outlive their TTL.
Numeric strings were likewise stored as `{ S }`.
Use the normalised number in the output record, fix the missing
parseInt radix, and narrow the intermediate type to `number |
undefined` so marshalling always produces `{ N }`. Add integration
tests that read the raw item via getItem and assert `ttl.N` for both
Date and string inputs. Closes #32.
The `delete recordToValidate['ttl']` lived inside the registered indexes loop, so a facet configured with a TTL but no GSIs would leak the synthetic `ttl` attribute into the validator. Move it after the loop so it runs exactly once per out() call. Also tighten the intermediate type from `any` to `Record<string, unknown>`. Closes #33.
…lders deleteSingleItem copied `names` and `values` onto the DeleteItemInput unconditionally. DynamoDB rejects requests where ExpressionAttributeNames/Values is present but empty, which the common condition form like `['fieldName', 'exists']` produces (empty values map, non-empty names map). putSingleItem already guarded against this; delete was the copy-paste miss. Extract the guarded assignment into `applyCondition` in `lib/condition.ts` and use it from both put and delete so the two paths cannot drift again. Adds `Conditional Deletes` integration test. Closes #38.
The shard-selection branch used `if (shard)` to decide between "use the caller-supplied shard" and "hash from model" — a falsy check that silently fell through to the hash path when `shard === 0`. So `facet.query(partition, 0)` could not target shard 0, and `pk(model, 0)` returned a hash-derived key. Switch to `shard !== undefined`. The outer guard already handles `null` (explicit opt-out), so this only changes behaviour for `shard === 0`. Add a regression test that asserts each of the four shards on a sharded GSI produces a distinct, well-formed PK and that one of them matches the hashed default. Closes #36.
`Facet#sk` and `FacetIndex#sk` were coalescing an unset `shard` argument to `null` before calling `buildKey`. `buildKey`'s outer guard is `shard !== null`, so the coalescing short-circuited the entire shard block and any `KeyConfiguration.shard` on a sort key was silently discarded — including on `in()` writes, which go through `this.sk(model)` with no shard argument. Drop the `?? null`. The pk/sk call paths now treat `shard: undefined` consistently as "compute from model", matching `Facet#pk` which already had this behaviour. Closes #37.
addIndex had a hasOwnProperty guard for the alias but none for the
GSI slot itself. Calling addIndex twice with the same index would
overwrite `#indexes.get(index)` and `this[index]` while leaving any
prior alias dangling against a stale FacetIndex. The type-level
cast made it compile cleanly, so mistakes were silent.
Add the same guard for the index slot and hoist both guards to the
top so a failed check doesn't leave the facet half-mutated (the old
alias check ran after `#indexes.set` and `Object.assign(this,
{[index]})` had already fired). Closes #53.
Facet.in builds the stored record as `{...model, ...facetKeys, facet,
ttl}`. If the model itself had a field named PK, SK, facet, ttl, or
GSIxPK/GSIxSK, the synthetic key silently overwrote it and the
original value vanished on the round trip (since Facet.out strips
those same attribute names). No compile error, no runtime warning.
Two complementary fixes:
1. Add a `ReservedAttributeName` template-literal type (`PK` | `SK` |
`facet` | `ttl` | `` `GSI${number}PK` `` | `` `GSI${number}SK` ``)
and a `WithoutReservedAttributes` constraint (optional `?: never`
fields). `Facet`, `FacetOptions`, `FacetIndex`, `FacetIndexKeys`,
`AddIndexOptions`, `PartitionQuery`, and every put/get/delete
helper now require `T extends WithoutReservedAttributes`. A model
that declares one of the reserved names now fails to compile.
2. `Facet.in` calls `assertNoReservedAttributes(model)` so a dynamic
caller that bypasses the types (e.g., JSON.parse output) gets a
thrown error instead of a silent clobber.
Tests cover the runtime guard directly and the type-level guard via
`@ts-expect-error` on `Facet<{PK: string}>`, `Facet<{ttl: number}>`,
and `Facet<{GSI1PK: string}>`. Closes #54.
Idempotent at runtime but a plain typo. Closes #39.
aws-sdk v3 errors expose the exception class name via `.name` (and `instanceof`), not via the v2 `.code` field. The createTable catch block in the test helper was checking `error.code === 'ResourceInUseException'`, which is always undefined — so the "reset the existing TEST table if it already exists" branch never fired. The suite passed on fresh Docker containers but would see stale data on warm ones and fail deterministic assertions like `expect(posts.records.length).toBe(300)`. Switch to `instanceof ResourceInUseException` and re-throw anything else so setup errors stop being silently swallowed. Verified by running `npm test` twice against the same persistent DynamoDB Local container — both runs green. Closes #40.
The base tsconfig.json excludes **/*.test.ts from its compile set. tsconfig.test.json's own include was re-adding them, but exclude is not overridden on extends so the inherited exclusion won. As a result npm run typecheck:tests was silently checking zero test files — a gap that masked a broken constraint (see follow-up) for the whole v6 branch.
The constraint was { [K in ReservedAttributeName]?: never } — a mapped
type over the template-literal union `GSI\${number}PK`/`GSI\${number}SK`
which TypeScript realises as an index signature. No concrete model
type structurally matches that signature, so the bound rejected every
`T`, not just types with reserved keys. The tsconfig.test.json bug
hid this because tests (the only places concrete Ts appear) were never
type-checked.
Switch to { [K in keyof T]: K extends ReservedAttributeName ? never : T[K] }
and use `T extends WithoutReservedAttributes<T>` as the bound, so a
model that names a reserved field has at least one impossible property
and fails while clean models pass.
Also change the PK/SK default generics from `Keys<T>` to `never` so
`SK: { keys: [] }` configurations infer SK correctly (no required sort
key fields on `get(...)`), rather than widening to the full key set.
BREAKING: exported `WithoutReservedAttributes` now takes a type
parameter. Consumers writing `Facet<T, ...>` via the library's own
generics are unaffected; direct users of the constraint type need
to update to `WithoutReservedAttributes<T>`.
Users can opt into DynamoDB ProjectionExpression reads by passing
{ select: ['attr1', 'attr2'] } to Facet.get(). The return type narrows
to Pick<T, K | PK | SK> — the facet's PK/SK fields are always included
because callers need them for identity and round-tripping.
Projected reads need a PickValidator, a factory that derives a
sub-validator for any requested subset of keys. The factory shape
matches how real validator libraries work: the outer function runs
once per key-set (expensive derive step — Zod .pick(), Ajv compile()),
the returned function runs per record (cheap parse). Worked Zod and
Ajv examples in the PickValidator tsdoc.
The capability is gated at compile time. Facet now carries a phantom
generic PV on top of <T, PK, SK>; the exported `Facet` const is typed
by FacetConstructor, whose overloads narrow PV to PickValidator<T>
when the opts object contains a pickValidator, or to undefined
otherwise. The projected get overloads use
`this: [PV] extends [PickValidator<T>] ? this : never`, so
`.get(q, { select: [...] })` on a facet configured without a
pickValidator is a type error rather than a runtime throw. Facet.pick
keeps a defensive runtime throw as a safety net for paths that bypass
the type gate (upcasts, direct calls).
Projection on query (PartitionQuery / FacetIndex.query) is not yet
implemented — only Facet.get reads honour select. The expression
builder, auto-included key fields, and pickValidator wiring are
positioned to extend to queries in a follow-up.
Exposed new exports: Facet (both value and type), FacetConstructor,
PickValidator, WithoutReservedAttributes (now a generic).
Assumption (noted in CLAUDE.md): GSIs are created with
ProjectionType: ALL. Get uses the base table and isn't affected,
but the assumption is load-bearing for the planned query-level
projection extension.
Every public method on Facet and PartitionQuery now carries a real tsdoc summary, @param / @returns, and at least one @example block. VS Code's hover and TypeDoc's generated reference both benefit. Facet.get's overload order was reversed: the single-item forms now come first, so IDE parameter hints show "Pick<T, PK | SK>" property completions at the caret instead of Array.prototype methods when the caller types `get({`. The runtime behaviour is unchanged. Also annotate the one intentional no-redeclare on the exported `Facet` identifier, which is both a type alias and the typed constructor const. In the README, add a "Composite sort keys" section that explains lexicographic comparison of the joined sort key, and calls out the greaterThan/lessThan bleed that happens when the query crosses leading-field values. Add a "Query pattern reference" section that maps common access patterns ("most recent record", "range scoped to one value of a leading SK field", etc.) to their idiomatic call. Add a Best Practices bullet pointing at the new section so readers who skim the bullets still see the warning.
Local scratchpad for one-off type-ergonomics tests (e.g. the EmailMetadata playground) that should never ship.
Gives the TypeScript language server and CLI visibility into files under _scratch/ without affecting what ships. The directory is git- ignored, compiled artifacts (_scratch/**/*.js and .d.ts) are not covered by the package.json "files" allowlist, and the pattern is a no-op when _scratch/ doesn't exist.
Extends the select projection capability that ships on Facet.get to
cover every PartitionQuery operator: equals, greaterThan,
greaterThanOrEqual, lessThan, lessThanOrEqual, beginsWith, list,
first, and between. Works on base-facet queries and index queries
alike.
Threads a PV phantom generic through PartitionQuery, PartitionQueryOptions,
FacetIndex, FacetIndexKeys, Facet.query, and FacetIndex.query so the
facet's "does it have a pickValidator?" state flows into the query
surface.
Each operator now has two typed overloads plus the implementation:
- A plain overload whose options type is
QueryOptions<...> & { select?: never }. The select?: never guard is
load-bearing: TypeScript resolves overloads top to bottom, so without
it a projected call would bind to the plain overload and skip the
gate entirely. The guard forces the plain overload to reject any
options object that carries select.
- A projected overload gated by
`this: [PV] extends [PickValidator<T>] ? this : never`. Callers on
facets constructed with a pickValidator resolve to this signature
and get QueryResult<Pick<T, K | AutoKeys<PK, SK, GSIPK, GSISK>>>.
Callers on facets without a pickValidator get a compile error.
AutoKeys is PK | SK for base-facet queries and PK | SK | GSIPK | GSISK
for index queries. The index case relies on the library's documented
assumption that GSIs are created with ProjectionType: ALL, which makes
the base-table key fields available on the index.
Execution paths are refactored around three private single-signature
helpers — #compareExec, #beginsWithExec, #betweenExec — plus a shared
#execute that merges cursor, filter, and projection into the
QueryInput before running it. Public impls route through these
helpers, not through other public methods, so overload resolution on
internal dispatch can't drag the plain overload's select?: never guard
into a projected call site. Adds a keyFields accessor on FacetIndex
mirroring the one on Facet so PartitionQuery can auto-include the
index's PK/SK field names on projected index queries.
Tests for projection on query land separately.
Integration tests covering every operator (equals, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, beginsWith, list, first, between) with select, plus: - Base-facet vs index-facet auto-key inclusion (index queries auto-include both the base PK/SK fields and the index GSIPK/GSISK fields). - Type-level narrowing verified via @ts-expect-error probes on fields that weren't selected. - Type-level gate verified via @ts-expect-error on every operator of a facet constructed without a pickValidator. - Dedup when select repeats a key or overlaps with an auto-included field. - filter + select combined (the placeholder namespaces — #F_* for filter, #p* for projection, #PK/#SK for the key condition — are documented as disjoint, this confirms they merge cleanly at runtime). - Cursor pagination across a projected query (confirms the opaque LastEvaluatedKey survives being emitted by a projected read). 47 tests pass (up from 29).
Adds a "Projected reads" section to the README covering the feature end to end: why (payload size and JSON-parse cost, not read capacity), the pickValidator factory shape, configuring the facet, using select on Facet.get and on every PartitionQuery operator, auto-inclusion of base PK/SK plus index GSIPK/GSISK fields on index queries, the compile-time gate on facets without a pickValidator, and the opt-out identity pickValidator pattern. Adds a new entry in the existing Query pattern reference for projected queries, and a Best Practices bullet noting that ProjectionExpression reduces the wire payload but not DynamoDB's read-capacity charge. Extends tsdoc on the nine PartitionQuery methods. equals, list, first, beginsWith, and between each gain a second worked @example showing a projected call. The four remaining comparison operators (greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual) get a short @remarks pointing readers to equals for the projected shape, rather than duplicating the same example four more times.
Sort-key args on equals/greaterThan*/lessThan*/beginsWith/between were
typed `Partial<Pick<T, GSISK>>`. For base-facet queries GSISK is `never`,
collapsing to `{}` — the sort arg accepted any object shape and only the
string escape hatch was meaningfully typed.
Add an `ActiveSK<SK, GSIPK, GSISK>` helper that picks `SK` on base queries
(where GSIPK is `never`) and `GSISK` on index queries, and thread it
through every public overload. No new generics on PartitionQuery.
Fixes #41.
Four exports were declared but never consumed anywhere — not in lib, not in tests, not re-exported from index.ts: - IndexPrivatePropertyMap (20-line `_GSI*` name map) - isIndex (type guard) - IndexKeyConfiguration - IndexKeyOptions (~60-line interface with 40 generic parameters) Drop all four. Add a compile-time regression test that re-adding any of them fails type-checking. Fixes #42.
shard.keys was typed `Keys<T>[]`, so any property of T qualified — including Date, object, and array fields. Those would silently contribute `[object Object]`, a timezone-dependent string, or a joined array to the CRC hash input. Deterministic, but an opaque footgun. Add a `PrimitiveShardKey<T>` helper that picks keys whose value extends `string | number | bigint | boolean | undefined`. The tuple wrap (`[T[K]] extends [...]`) keeps the check non-distributive so a union like `string | Date` is rejected as a whole rather than matching on the primitive half. KeyConfiguration.keys is intentionally left unchanged — buildKey already handles Date via toISOString for composite keys, and silently skips other non-primitives. The narrowing only matters for the hash path. Fixes #44.
Matches the style already used in put.ts, delete.ts, and condition.ts. Query.ts was the last namespace-import holdout. Alias the imported `filter` function as `buildFilterExpression` to avoid colliding with the local `filter` variable destructured from options. Behaviour-only refactor; existing filter tests cover the change. Fixes #45.
The cursor encodes DynamoDB's LastEvaluatedKey: a small map of
attribute names to AttributeValue objects. Two invariants this library
maintains let us skip CBOR's general-purpose encoding entirely:
1. Every key value is a string. `buildKey` only emits strings, so
LastEvaluatedKey always contains `{S: string}` values.
2. Attribute names come from a fixed 42-name set — `PK`, `SK`, and
`GSI1PK..GSI20SK`. Each encodes to a single byte.
New format: base64url of a byte stream of (code:u8)(len:varint)(utf8)
tuples. On a realistic 4-entry GSI cursor this is about 45% smaller
than base64url(JSON) and about 20% smaller than the previous CBOR
encoding, with zero runtime dependencies.
Varint decoding uses Number arithmetic rather than 32-bit bitwise
shifts so values above 2^31 don't wrap silently, and rejects input
that would overflow MAX_SAFE_INTEGER. Also switches base64 to
base64url so cursors are URL-safe.
Breaking: cursors minted by earlier versions won't decode. Acceptable
for v6.
Drops the `cbor` dependency.
Facet's constructor used to fall back to `new DynamoDB({})` when the
caller omitted `connection.dynamoDb`. That silently produced a client
with default config — wrong region, wrong credentials, wrong endpoint
— and was the only runtime use of `@aws-sdk/client-dynamodb` in the
library. Drop the fallback. `connection.dynamoDb` is now required.
Convert the SDK import to `import type`. Every remaining use in the
library is a type reference; the emitted .js files no longer import
the SDK at all, so the library is purely type-coupled to it.
Previously the peer declared `>=3.0.0 <=3.859.0`, which pinned below the current SDK minor. Every consumer on a newer SDK (i.e. most of them; latest is 3.1032.0) was triggering a peer warning or forced into duplicate installs. The tagged-union `AttributeValue` shape this library depends on has been stable since 3.0.0 GA (Dec 2020), and AWS SDK v3 has shipped ~1000 minors since without a v4 on the horizon. `^3.0.0` matches the ecosystem pattern (dynamodb-toolbox et al.) and accepts any current or future 3.x. Dev dep bumped to `^3.1032.0` so CI exercises a current SDK; the lockfile supplies run-to-run reproducibility, so the manifest doesn't need to pin.
Two bugs in the previous sharding logic:
1. `CRC32.bstr(string) >>> 1 % shardCount` in crc-shard.ts: `>>> 1` was
meant to coerce signed int32 to unsigned, but the correct idiom is
`>>> 0`. The `>>> 1` form silently discards the low bit, losing one
bit of entropy. Distribution was still uniform but the low bit of
the CRC was never used on power-of-two shard counts.
2. `valuesToHash.join('')` in keys.ts: multi-key shard configs were
concatenated without a separator, so `{a: 'ab', b: 'c'}` and
`{a: 'a', b: 'bc'}` hashed to the same shard. Now joined with a
null byte, which can't appear in real IDs and never leaves the
hash function.
Also swap the `crc-32` npm package (~100KB, pure JS) for Node's
built-in `node:zlib.crc32` (backported to 20.15.0 so the existing
engines floor of 20.19.0 covers it). Native implementation, hardware
acceleration where available, drops a dependency.
Breaking for v6: existing sharded data will rebalance. Single-key
ASCII, single-key non-ASCII, and multi-key configs all see new shard
assignments.
Tests added: dedicated crc-shard test file (determinism, range,
padding, uniform distribution, NaN clamping, fractional truncation,
UTF-8 test vector `"123456789" → 0xcbf43926`) and a multi-key
delimiter collision test proving the `\x00` join prevents the old
stringify-concat collision.
Previously `tsconfig.json` was the build config that excluded tests, and `tsconfig.test.json` was a separate config with `vitest/globals` used only by `npm run typecheck:tests`. Since VS Code picks the nearest tsconfig for a given file, opening a test file landed on tsconfig.json — which excluded the file, so the language service fell back to defaults and flagged `expect`/`test`/`describe` as unresolved. Invert the split: tsconfig.json is now the dev-wide view covering lib + tests + scratch + vitest.config.ts, with vitest/globals in `types` and `noEmit: true`. A new `tsconfig.build.json` extends it, flips noEmit off, adds declaration/removeComments, and excludes tests. That's the config used by `npm run build`. The test-only config is gone. `npm run typecheck` covers everything via `tsc --noEmit` against the base config (replacing the former `npm run typecheck:tests`). CI step renamed to match.
This branch has already landed multiple breaking changes: required `connection.dynamoDb` (no default client fallback), cursor format rewrite, shard-id algorithm changes, and widened AWS SDK peer range. SemVer demands the major bump; setting the manifest now makes the intent explicit rather than carrying 5.0.0 through the v6 branch.
Match the convention for mid-sized TypeScript libraries (kysely,
drizzle-orm, dynamodb-toolbox, zod, valibot): export everything
useful from the root, and fence `./lib/**` subpath imports via the
`exports` field in package.json.
index.ts gains:
- `PK`, `SK`, `IndexKeyNameMap` constants for CDK / table-setup
code that would otherwise hardcode attribute names.
- `buildKey` for advanced callers constructing composite keys
outside a facet (migrations, cross-facet scatters).
- `FacetIndex` and `PartitionQuery` as value exports alongside
their existing type exports, enabling `instanceof` checks and
completing the symmetry with `Facet`.
- `GetOptions` type — real gap; siblings `PutOptions`,
`DeleteOptions`, `QueryOptions` were already exported.
- `Keys`, `PrimitiveShardKey`, `ReservedAttributeName` utility
types for users writing their own helpers against facet
models.
package.json gains an `exports` field exposing only `.` and
`./package.json`. Subpath imports like
`require('@faceteer/facet/lib/keys')` now fail with
`ERR_PACKAGE_PATH_NOT_EXPORTED`. Breaking for anyone reaching into
internals, appropriate on v6. The benefit: internal file layout
under `lib/` becomes freely refactorable without breaking
consumers.
Added `@internal` JSDoc to `FacetIndex`'s constructor to steer
consumers toward `Facet.addIndex(...)` — a manually constructed
`FacetIndex` isn't wired into the parent facet's index map and
would be inert.
Cursor helpers, shard helper, batch internals, condition helpers,
and direct impl functions (`putSingleItem`, `getBatch`, etc.) stay
internal by omission. They're wrapped by `Facet` methods and
exposing them would invite coupling to implementation details.
- migrate to ESLint 10 flat config (eslint.config.mjs) using defineConfig
from eslint/config; delete legacy .eslintrc.js
- replace @typescript-eslint/{parser,eslint-plugin} with the unified
typescript-eslint v8 package
- layer @eslint/js recommended, strict-type-checked, stylistic-type-checked,
eslint-plugin-import-x (no-cycle + no-extraneous-dependencies only), and
eslint-config-prettier/flat
- drop eslint-plugin-prettier: Prettier runs standalone via npm run
format / format:check, per prettier.io guidance
- remove unused plugins: eslint-config-google (unmaintained since 2019),
eslint-config-standard, eslint-plugin-import, eslint-plugin-n,
eslint-plugin-promise, eslint-import-resolver-node
- bump typedoc to ^0.28.19 so npm install resolves cleanly under TS 6
without --legacy-peer-deps
- fix strict-type-checked findings across put/delete/facet/query (unsafe
any unmarshalling, forbidden non-null assertions, dynamic delete on
reserved keys)
- add lint/format/format:check scripts and wire them into CI
Fixes #47. The batch helpers chunked input into 25/25/100-item batches then fired every batch at once via Promise.all(Settled), so a 10k-item call produced 400 concurrent BatchWriteItem / 100 concurrent BatchGetItem requests — enough to exhaust on-demand burst capacity and touch off SDK retry storms on a hot table. Add a small mapWithConcurrency helper and route the outer fan-out through it. Each of PutOptions / DeleteOptions / GetOptions gains a concurrency?: number field; default is 8, tuned to sit just above a new on-demand table's starting 4k WCU (25-item batches × ~30ms p50 ≈ 830 WCU/s per in-flight worker, so 8 workers ≈ 6.7k WCU/s) while staying well under the AWS SDK v3 http maxSockets default.
- Version → 6.0.0-alpha.0 to make the pre-publish status explicit. - Switch to type: module with an ESM-only exports map. Relative imports carry explicit .js extensions, as NodeNext requires. - Bump engines to >=22.12.0 so consumers land on the first Node LTS with require(esm) unflagged. CJS callers can still require() the package on supported runtimes. - Add sideEffects: false so bundlers can tree-shake unused exports.
Carve .vscode/settings.json out of the .vscode/ ignore so everyone who opens the repo gets the same import-module-specifier-ending preference. With the project on ESM + NodeNext, TypeScript now requires explicit .js extensions on relative imports; this setting makes the VS Code auto-import suggestion insert them automatically. Other .vscode/ files remain personal.
Mirrors faceteer/cdk's publish pipeline (tag push → full CI → OIDC Trusted Publishing → dist-tag picked from the version suffix) and adds one step cdk is missing: the workflow lifts the matching `## [X.Y.Z]` block out of CHANGELOG.md and uses it as the body of the GitHub Release, so every version leaves a human- and LLM-readable record of what shipped. Seeds CHANGELOG.md with a [Unreleased] block and an initial [6.0.0-alpha.0] entry covering the full v5 → v6 diff. Documents the release recipe in CLAUDE.md so future sessions can cut a tag from a short script: draft notes from git log, rename [Unreleased] to the new version, bump package.json, commit, tag, push. CLAUDE.md's Node-version line was also stale after the earlier ESM migration; updated to match the current >=22.12 engines range.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #56 +/- ##
==========================================
+ Coverage 79.34% 87.52% +8.18%
==========================================
Files 9 13 +4
Lines 489 465 -24
Branches 123 92 -31
==========================================
+ Hits 388 407 +19
+ Misses 101 32 -69
- Partials 0 26 +26 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First publish of the v6 line. Breaking changes from v5 touch module format, the DynamoDB client wiring, the cursor format, and the public API surface — migrating existing v5 consumers is not a drop-in upgrade.
Changed (breaking)
"type": "module") with a singleexportsentry. CJS consumers on Node ≥ 22.12 can stillrequire()viarequire(esm); older Node cannot.>=22.12.0(the first LTS whererequire(esm)is unflagged).connection.dynamoDb. The library no longer constructs a client internally; consumers own credential chains, endpoint overrides, and retry config.@aws-sdk/client-dynamodbis also a peer dep now, not a direct dep.import { Facet } from '@faceteer/facet') is supported (#55).PK,SK,facet,ttl,GSI*PK/GSI*SK) are now rejected at both the type level (viaWithoutReservedAttributes) and at runtime in the constructor. v5 silently clobbered colliding model fields (#54).ShardConfiguration.keysis restricted to primitive-typed fields at the type level; non-primitive fields are no longer silently hashed as[object Object](#44).Added
PutOptions.concurrency/DeleteOptions.concurrency/GetOptions.concurrency— cap outer fan-out on batch put/delete/get. Defaults to 8, tuned to sit just above new-on-demand starting capacity. Fixes unbounded fan-out that triggered throttling storms on large batches (#47).selectonFacet.get(single + batch) and everyPartitionQueryoperator. Returns aPick<T, K | PK | SK>and validates through a newpickValidatorfactory. PK/SK fields are always re-projected, even if omitted fromselect.Facet.addIndex(..., { alias })— register a human-readable alias (facet.PagePostStatus.query(...)) alongside the rawGSInaccessor. Type-level collision check prevents alias/index-name overlap.PartitionQuerymethods (equals/beginsWith/between/etc.).index.tsbarrel widened to cover the full public surface and typedoc-validated.Facet, including guidance on composite sort-key patterns.Fixed
UnprocessedItems, not the pre-retry snapshot (#31, #35).WriteRequestshape, so failure reporting actually fires (#34).Facet.out()strips syntheticttleven when the facet has no registered indexes, and the delete-ttl step no longer runs N times per read (#33, #49).Ntype with epoch-seconds value, not the rawS-typed ISO string. Date-typed TTL fields were silently broken (#32).deleteSingleItemno longer sends emptyExpressionAttributeNames/ExpressionAttributeValuesmaps, which DynamoDB rejects withValidationException. Conditional deletes without value placeholders now succeed (#38).SK.shardconfiguration is no longer silently dropped byFacet.sk()(#37).buildKey()now honours an explicitshard: 0; v5 treated it as unspecified (#36).addIndexrejects silent overwrite of an already-registered GSI slot or alias (#53).WithoutReservedAttributes<T>constraint no longer structurally rejects every concreteT: it maps overkeyof Tso only colliding fields becomenever.error.nameinstead of v2error.code; the reset path was never firing (#40).this.#PK = PKassignment in theFacetconstructor (#39).tsconfig.test.jsonno longer inherits theexcludepattern that dropped test files from the default type-check graph.Removed
crc-32npm dependency — shard hashing now usesnode:zlib.crc32(available from Node 20 onward).lib/keys.ts:IndexPrivatePropertyMap,isIndex,IndexKeyConfiguration,IndexKeyOptions(#42).Infrastructure
@aws-sdk/client-dynamodbpeer dep range to^3.0.0..jsextension for NodeNext module resolution.