@ignex/ninox 0.1.6 → 0.2.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.
package/README.md CHANGED
@@ -281,7 +281,7 @@ builder, and the MongoDB `$jsonSchema` validator + indexes installed by
281
281
  | field kind | `s.*` | inferred TS type |
282
282
  | --- | --- | --- |
283
283
  | string / int / number | `s.string()` / `s.integer()` / `s.number()` | `string` / `number` / `number` |
284
- | exact BSON kinds | `s.double()` / `s.long()` / `s.decimal()` | `number` / `number` / `Decimal128` |
284
+ | exact BSON kinds | `s.double()` / `s.long()` / `s.decimal()` | `number` / `Long` / `Decimal128` |
285
285
  | other scalars | `s.boolean()` / `s.date()` / `s.objectId()` | `boolean` / `Date` / `ObjectId` |
286
286
  | enum | `s.enum(['a', 'b'] as const)` | `'a' \| 'b'` (string or numeric literals) |
287
287
  | array | `s.array(s.string())` | `string[]` |
@@ -293,8 +293,10 @@ builder, and the MongoDB `$jsonSchema` validator + indexes installed by
293
293
  Constraints mirror `$jsonSchema`: strings (`minLength` / `maxLength` / `pattern`),
294
294
  numbers (`minimum` / `maximum` / `multipleOf` / `exclusiveMinimum` /
295
295
  `exclusiveMaximum`), arrays (`minItems` / `maxItems` / `uniqueItems`), objects
296
- (`minProperties` / `maxProperties` / `additionalProperties`). `format` is
297
- accepted but a no-op (MongoDB rejects the `$jsonSchema` keyword).
296
+ (`minProperties` / `maxProperties` / `additionalProperties`). String `format`
297
+ is validated client-side for `email`, `uri`, `date-time`, `date`, `uuid`,
298
+ `hostname`, `ipv4`, and `ipv6`; it is not sent to MongoDB because the server
299
+ rejects that `$jsonSchema` keyword.
298
300
 
299
301
  ```ts
300
302
  const userSchema = s.object({
@@ -342,7 +344,10 @@ do {
342
344
  ```
343
345
 
344
346
  `paginateCursor` supports `after` (forward) and `before` (backward); a sort whose
345
- keys don't match the cursor's is rejected at runtime.
347
+ keys don't match the cursor's is rejected at runtime. The final sort key must be
348
+ `_id` or an explicitly named `uniqueTiebreaker` with a unique index. Dotted paths
349
+ and BSON values are supported. Offset pagination rejects offsets above 10,000 by
350
+ default (`maxOffset` overrides); use keyset pagination for deep traversal.
346
351
 
347
352
  ## Geospatial
348
353
 
@@ -423,6 +428,11 @@ await db.populate(products, [
423
428
  // orders[0].customer, orders[0].more, products[0].tags are all typed.
424
429
  ```
425
430
 
431
+ Populate reads are fresh by default. Pass `session`, `maxTimeMS`, read concern,
432
+ or read preference in the options. `maxDocs` is an optional safety cap that
433
+ throws on overflow rather than silently truncating a relation; `queryCache:
434
+ true` explicitly opts into the shared read cache.
435
+
426
436
  ## Repository layer
427
437
 
428
438
  `createRepository(manager, 'collection')` wraps a manager + collection into a
@@ -454,12 +464,14 @@ const users = defineCollection('users', userSchema, {
454
464
 
455
465
  ## Transactions & migrations
456
466
 
457
- - `db.transaction(async (session) => ...)` — graceful session transactions with a
458
- capability probe; on standalone servers (no transactions) it falls back to
459
- plain non-transactional execution with `session === null`.
467
+ - `db.transaction(async (session) => ...)` throws on standalone servers by
468
+ default. To accept non-atomic execution explicitly, pass
469
+ `{ allowNonTransactionalFallback: true }`; the callback then receives `null`.
460
470
  - `createMongoMigrationRunner(service, { migrationDir })` — file-based migrations
461
- (`NNN_name.ts`, each exporting `up` / `down`) with a claim-based idempotency
462
- journal (`_migrations`), safe under concurrent runners.
471
+ (`NNN_name.ts`, each exporting `up` / `down`) with a lease- and owner-token-
472
+ fenced journal (`_migrations`), safe under concurrent runners and stale-worker
473
+ takeover. Migrations should remain idempotent: file migrations are not
474
+ automatically wrapped in a database transaction.
463
475
 
464
476
  ```ts
465
477
  const runner = createMongoMigrationRunner(service, { migrationDir: './migrations' });
@@ -501,8 +513,8 @@ reports current size plus lifetime `hits` / `misses` / `sets` / `deletes` /
501
513
  > ⚠️ **Cache invalidation is write-through only.** Every ninox write drops that
502
514
  > collection's cached reads, but reads are **not** invalidated by external
503
515
  > writers — other processes, the raw `client` escape hatch, or direct DB
504
- > writes. With the default `ttlMs: 0` those reads stay stale indefinitely, so
505
- > multi-writer deployments should set `cache: { ttlMs }`, disable the cache,
516
+ > writes. The default `ttlMs: 30_000` bounds that staleness; `ttlMs: 0` is an
517
+ > explicit unbounded-cache opt-in. Multi-writer deployments can disable the cache,
506
518
  > route latency-critical reads through the change-stream-aware
507
519
  > [`createHotCache()`](#hot-cache-high-throughput), or — on a replica set — opt
508
520
  > into `cacheWatch: true`:
@@ -786,16 +798,18 @@ you need the code → status mapping yourself.
786
798
  > (at-least-once); opt in per op with `retryWrites: true`. Reads always retry
787
799
  > transient errors.
788
800
 
789
- Schema validation is enforced **by MongoDB** — the DB `$jsonSchema` validator
790
- derived from your schema is installed with `createSchema`, and the DB rejects
791
- violating writes with `VALIDATION_FAILED`. `createSchema` is **idempotent and
801
+ Schema validation is enforced **by ninox and MongoDB** — inserts and full
802
+ replacements are validated client-side by default (`validateWrites: false` opts
803
+ out), while the DB `$jsonSchema` validator remains the backstop for partial
804
+ updates, raw writes, and other clients. Violations raise `VALIDATION_FAILED`. `createSchema` is **idempotent and
792
805
  self-reconciling**: it provisions a NEW collection, and when the collection
793
806
  already exists it upgrades the DB schema to match the current ORM schema
794
807
  (hot-swapping the validator via `collMod` only when it has drifted, and
795
808
  creating any newly-declared indexes) — so calling it on every boot keeps
796
809
  Mongo's schema in lockstep with your models as they change. `updateSchema`
797
- hot-swaps a validator explicitly (via `collMod`). ninox deliberately does
798
- **not** validate *incoming user input* at runtime (no client-side/zod layer).
810
+ hot-swaps a validator explicitly (via `collMod`) and updates the manager's
811
+ active schema. Client validation covers the DSL and raw `$jsonSchema` keywords;
812
+ partial update operators are still enforced by the database.
799
813
 
800
814
  - **Strict by default** — `additionalProperties: false` is emitted unless a
801
815
  field opts out (`s.object({ ... }, { additionalProperties: true })`), so
@@ -898,6 +912,8 @@ createMongoService(clients, {
898
912
  cache: { maxSize: 1000, ttlMs: 30_000, clone: true }, // configure the default cache
899
913
  cacheWatch: true, // change-stream invalidation of the cache (replica sets)
900
914
  drift: 'report', // read-time schema-drift detection ('report'|'throw'|'off')
915
+ validateWrites: true, // validate inserts/replacements before the DB call
916
+ allowNonTransactionalFallback: false, // throw instead of losing atomicity
901
917
  dedupeReads: true,
902
918
  perf: true, // master switch for cache + dedup
903
919
  });
@@ -908,8 +924,8 @@ createMongoService(clients, {
908
924
  - `service.health()` — ping every connected DB and report `{ ok, latencyMs, dbs }`.
909
925
  - **Multi-DB services** — define several clients in one service; each gets its own
910
926
  typed manager (`service.db.primaryClient`, `service.db.auditClient`, …), one
911
- connection pool per URL, and **cache/dedup keys are namespaced by database** so
912
- same-named collections in different databases never collide.
927
+ connection pool per URL, and **cache/dedup keys are namespaced by deployment,
928
+ database, and collection** so same-named collections never collide.
913
929
  - `watchCollection` / repository `.watch()` — caller-owned change streams.
914
930
  - `eachDb(fn)` — run a function over every connected manager.
915
931
 
@@ -1000,8 +1016,8 @@ bun run bench # → bench/results/summary.json
1000
1016
 
1001
1017
  **Multi-DB cache isolation.** A single service can define several DB clients; the
1002
1018
  query cache and in-flight dedup are shared across them, and their keys are
1003
- namespaced by **database** — so same-named physical collections in different
1004
- databases can never share (or coalesce) entries. No action needed; it's handled.
1019
+ namespaced by **deployment and database** — same-named physical collections in
1020
+ different databases or clusters can never share (or coalesce) entries.
1005
1021
 
1006
1022
  **Standalone HotCache freshness is opt-in per query.** In standalone mode the
1007
1023
  ticker only refreshes queries that set `refreshIntervalMs`. A query with neither
@@ -1037,9 +1053,16 @@ the cache-effectiveness signals to watch in production.
1037
1053
  ## Tests
1038
1054
 
1039
1055
  ```bash
1040
- bun test # unit + integration (integration skips when no Mongo is reachable)
1056
+ bun test # unit + integration (Mongo suites skip when no server is reachable)
1057
+ bun run test:e2e # public-API workflows; fails unless MONGO_URL is reachable
1041
1058
  ```
1042
1059
 
1060
+ The e2e suite exercises real MongoDB schema validation, CRUD/repository flows,
1061
+ relations, aggregation, both pagination strategies, cache consistency,
1062
+ migrations, multi-DB behavior, and health checks. On a replica set it also
1063
+ verifies transaction commit/rollback; on standalone it verifies the explicit
1064
+ non-transactional fallback. Set `MONGO_URL` to select the server.
1065
+
1043
1066
  ## Development
1044
1067
 
1045
1068
  ```bash
@@ -1058,7 +1081,8 @@ npm pack --dry-run # inspect the published tarball contents
1058
1081
  The `files` allowlist ships `dist/`, `LICENSE`, and `README.md` — no `src/`,
1059
1082
  tests, or examples. CI enforces typecheck, lint, dependency audit, coverage
1060
1083
  (≥ 85%), the full example suite, a replica-set job for the HotCache
1061
- change-stream tests, and an `npm pack` → fresh Node consumer smoke test.
1084
+ change-stream tests, e2e compatibility jobs for MongoDB 4.4/5/6/7/8, and an
1085
+ `npm pack` → fresh Node consumer that runs real CRUD and pagination.
1062
1086
 
1063
1087
  Release notes are kept in [`CHANGELOG.md`](./CHANGELOG.md). The publish gate
1064
1088
  (`prepublishOnly`) runs typecheck + lint + build.