@ignex/ninox 0.1.5 → 0.1.7

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
@@ -787,10 +787,15 @@ you need the code → status mapping yourself.
787
787
  > transient errors.
788
788
 
789
789
  Schema validation is enforced **by MongoDB** — the DB `$jsonSchema` validator
790
- derived from your schema is installed with `createSchema` (and hot-swapped with
791
- `updateSchema`), and the DB rejects violating writes with `VALIDATION_FAILED`.
792
- ninox deliberately does **not** validate *incoming user input* at runtime (no
793
- client-side/zod layer).
790
+ derived from your schema is installed with `createSchema`, and the DB rejects
791
+ violating writes with `VALIDATION_FAILED`. `createSchema` is **idempotent and
792
+ self-reconciling**: it provisions a NEW collection, and when the collection
793
+ already exists it upgrades the DB schema to match the current ORM schema
794
+ (hot-swapping the validator via `collMod` only when it has drifted, and
795
+ creating any newly-declared indexes) — so calling it on every boot keeps
796
+ 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).
794
799
 
795
800
  - **Strict by default** — `additionalProperties: false` is emitted unless a
796
801
  field opts out (`s.object({ ... }, { additionalProperties: true })`), so
@@ -814,7 +819,8 @@ client-side/zod layer).
814
819
  `DomainError` carries the offending field paths (`extra.fields`), the failing
815
820
  document id (`extra.documentId`), and the raw `extra.details`, so a rejected
816
821
  write names exactly what (and which document) violated the schema.
817
- - **Index lifecycle** — `createSchema` installs the declared indexes;
822
+ - **Index lifecycle** — `createSchema` installs the declared indexes (and, on
823
+ an existing collection, adds any newly-declared ones without dropping);
818
824
  `syncIndexes(collection)` reconciles drift (`await db.syncIndexes('products')`
819
825
  creates missing declared keys and drops undeclared ones, `_id_` kept).
820
826
 
@@ -1015,9 +1021,10 @@ unknown keys in object literals AND non-literal patches. The only way an unknown
1015
1021
  key passes is a variable explicitly typed with an index signature
1016
1022
  (`Record<string, any>`) — prefer inline literals for full checking.
1017
1023
 
1018
- **Index drift.** `createSchema` installs declared indexes; if they drift out of
1019
- band (manual drops, other tooling), `syncIndexes('collection')` reconciles —
1020
- creates missing declared indexes and drops undeclared ones (`_id_` kept).
1024
+ **Index drift.** `createSchema` installs declared indexes on creation and adds
1025
+ newly-declared ones on re-provision; if indexes drift out of band (manual
1026
+ drops, other tooling), `syncIndexes('collection')` reconciles — creates
1027
+ missing declared indexes and drops undeclared ones (`_id_` kept).
1021
1028
 
1022
1029
  **Observability.** `QueryCache.stats()` (hits/misses/evictions/…) and
1023
1030
  `hot.stats()` (per-query hits/misses/refreshes/loadErrors/evictions/…) give you
package/dist/index.d.ts CHANGED
@@ -2,7 +2,6 @@ import * as mongodb from 'mongodb';
2
2
  import { Db, MongoClient, ClientSession, Document, Filter, Hint, CollationOptions, ReadPreferenceLike, ReadConcern, WriteConcern, WithId, Condition, RootFilterOperators, Collection, FindOptions, Sort, FindCursor, ObjectId, Decimal128, ReadConcernLike, AggregateOptions, AggregationCursor, CreateCollectionOptions, TransactionOptions, InsertOneOptions, InsertOneResult, InsertManyResult, UpdateOptions, UpdateResult, FindOneAndUpdateOptions, DeleteResult, ChangeStreamOptions, ChangeStream } from 'mongodb';
3
3
  import { L as LoggerLike } from './timeout-B10CTiGm.js';
4
4
  export { C as CacheOptions, a as LRU, b as LRUOptions, c as LogFn, d as LogLevel, e as createCachedAsyncFactory, f as createCachedFactory, g as createConsoleLogger, h as createNoopLogger, i as hashString, s as sleep, j as sleepJittered, k as stableHash, l as stableStringify, w as withTimeout } from './timeout-B10CTiGm.js';
5
- import * as bson from 'bson';
6
5
 
7
6
  /**
8
7
  * Types + constants for the HotCache layer. All type-only (plus the stream-key
@@ -411,14 +410,6 @@ declare const createMongoCapabilitiesStore: () => MongoCapabilitiesStore;
411
410
  /** Probe a connected client/db via `hello`. */
412
411
  declare const probeMongoCapabilities: (client: MongoClient | Db | null | undefined) => Promise<MongoCapabilities>;
413
412
 
414
- /**
415
- * The ORM's error classes (`AppError` → `DomainError` / `InfraError` /
416
- * `BadRequest`), their type guards, and the `ERROR_HTTP_STATUS` table that sets
417
- * each error's `statusCode` at construction.
418
- *
419
- * A leaf module: nothing else in `src/errors/` imports this (so `driver-map.ts`
420
- * and `http-status.ts` can depend on it without cycles).
421
- */
422
413
  interface ErrorContext {
423
414
  db?: string;
424
415
  collection?: string;
@@ -437,23 +428,47 @@ declare class AppError extends Error {
437
428
  readonly code: string;
438
429
  readonly statusCode: number;
439
430
  readonly extra?: Record<string, unknown>;
440
- constructor(code: string, message: string, statusCode?: number, extra?: Record<string, unknown>);
441
431
  /**
442
- * Stable, client-safe serialization for framework error middleware: a
443
- * framework can forward `JSON.stringify(error)` (or `error.toJSON()`) straight
444
- * to the client — no parsing of server internals. `stack` is dropped; the
445
- * shape is always `{ name, code, message, statusCode, ...(extra) }`.
432
+ * Whether `message`/`extra` may be sent to a CLIENT.
433
+ *
434
+ * Fail-closed: a 4xx message is the caller's contract, a 5xx message is an
435
+ * operator detail that may quote a driver, a host or a query. Pass `expose`
436
+ * explicitly to override — only for a 5xx whose text was written for callers
437
+ * (e.g. `new InfraError("DRAINING", "queue is draining", extra, true)`).
438
+ */
439
+ readonly expose: boolean;
440
+ constructor(code: string, message: string, statusCode?: number, extra?: Record<string, unknown>, expose?: boolean);
441
+ /**
442
+ * Full, stack-free shape for LOGS and typed consumers:
443
+ * `{ name, code, message, statusCode, ...(extra) }`.
444
+ *
445
+ * It carries the real message, so it is NOT the client path — use
446
+ * {@link toClientPayload} (or `serializeError`) for anything a caller sees, and
447
+ * prefer `logger.error({ err })` so pino's own serializer keeps the stack in the
448
+ * log rather than in a payload.
446
449
  */
447
450
  toJSON(): Record<string, unknown>;
451
+ /**
452
+ * The client-safe payload — the same shape as {@link toJSON}, except that a
453
+ * non-exposed error's `message` is replaced by the canonical reason phrase and
454
+ * its `extra` is dropped. A framework can forward this (or `serializeError`'s
455
+ * result) without auditing what the message might contain.
456
+ *
457
+ * @param options - `expose: true` forces the real (masked) message + extra.
458
+ * @returns The client payload.
459
+ */
460
+ toClientPayload(options?: {
461
+ expose?: boolean;
462
+ }): Record<string, unknown>;
448
463
  }
449
464
  declare class DomainError extends AppError {
450
- constructor(code: string, message: string, extra?: Record<string, unknown>);
465
+ constructor(code: string, message: string, extra?: Record<string, unknown>, expose?: boolean);
451
466
  }
452
467
  declare class InfraError extends AppError {
453
- constructor(code: string, message: string, extra?: Record<string, unknown>);
468
+ constructor(code: string, message: string, extra?: Record<string, unknown>, expose?: boolean);
454
469
  }
455
470
  declare class BadRequest extends DomainError {
456
- constructor(message: string, extra?: Record<string, unknown>);
471
+ constructor(message: string, extra?: Record<string, unknown>, expose?: boolean);
457
472
  }
458
473
  declare const isAppError: (err: unknown) => err is AppError;
459
474
  declare const isDomainError: (err: unknown) => err is DomainError;
@@ -483,8 +498,16 @@ declare const isMappedMongoError: (error: unknown) => error is AppError;
483
498
 
484
499
  /** Effective HTTP status for any thrown value: AppError-aware, raw → 500 (503 transient). */
485
500
  declare const httpStatusForError: (err: unknown) => number;
501
+ /** Options for {@link serializeError}. */
502
+ interface SerializeErrorOptions {
503
+ /**
504
+ * Include the real message (and `extra`) even for a non-exposed error — the
505
+ * development / trusted-caller path. It is still credential-masked.
506
+ */
507
+ expose?: boolean;
508
+ }
486
509
  /**
487
- * Reduce any thrown value to a stable, client-safe error payload:
510
+ * Reduce any thrown value to a stable, CLIENT-SAFE error payload:
488
511
  * `{ name, code, message, statusCode, ...(extra) }`.
489
512
  *
490
513
  * Raw driver errors are mapped through `mapMongoDriverError` first, so a
@@ -495,8 +518,15 @@ declare const httpStatusForError: (err: unknown) => number;
495
518
  * const payload = serializeError(err, { db, collection, op });
496
519
  * res.status(payload.statusCode).json(payload);
497
520
  * });
521
+ *
522
+ * Fail-closed by default: a 4xx message/extra is the caller's contract, while a
523
+ * 5xx message is replaced by the canonical reason phrase (`Internal Server
524
+ * Error`, `Service Unavailable`, …) and `extra` is dropped — a driver message
525
+ * that quotes a host, a query or a stored value never reaches a client. The
526
+ * detail stays on the error itself for logs; pass `{ expose: true }` (or a
527
+ * per-error `expose`) to opt out for a trusted caller.
498
528
  */
499
- declare const serializeError: (err: unknown, ctx?: ErrorContext) => Record<string, unknown>;
529
+ declare const serializeError: (err: unknown, ctx?: ErrorContext, options?: SerializeErrorOptions) => Record<string, unknown>;
500
530
 
501
531
  /**
502
532
  * Transient-error detection: which MongoDB errors are safe to retry. This is a
@@ -1467,6 +1497,13 @@ interface DbOpMeta {
1467
1497
  physicalCollection?: string;
1468
1498
  db: string;
1469
1499
  op: string;
1500
+ /**
1501
+ * What was sent to the database (filter / document / update / pipeline /
1502
+ * options). Surfaced in the ignex debugbar as the span's `params`, so the
1503
+ * dashboard shows exactly what the query carried — not just its name.
1504
+ * Optional: ops without a payload worth showing omit it.
1505
+ */
1506
+ params?: unknown;
1470
1507
  }
1471
1508
 
1472
1509
  /**
@@ -1549,7 +1586,7 @@ interface TextSearchPagination {
1549
1586
 
1550
1587
  interface CrudOpDeps<TCollection> {
1551
1588
  trace: <T>(meta: DbOpMeta, fn: () => T | Promise<T>) => Promise<T>;
1552
- meta: (collection: TCollection, op: string) => DbOpMeta;
1589
+ meta: (collection: TCollection, op: string, params?: unknown) => DbOpMeta;
1553
1590
  }
1554
1591
 
1555
1592
  /**
@@ -2031,7 +2068,7 @@ interface CrudOpsOptions {
2031
2068
  * and fire lifecycle hooks.
2032
2069
  */
2033
2070
  declare const makeCrudOps: <TClients extends DbClientsDefinition, TDb extends ExtractDbNames<TClients>>(client: Db, dbLabel: string, logger: LoggerLike, opts: CrudOpsOptions) => {
2034
- watchCollection: <X extends ColNames<TClients, TDb>>(collection: X, pipeline?: bson.Document[], options?: mongodb.ChangeStreamOptions) => mongodb.ChangeStream<CollectionDoc<TClients[TDb]["collections"][X]>, mongodb.ChangeStreamDocument<CollectionDoc<TClients[TDb]["collections"][X]>>>;
2071
+ watchCollection: <X extends ColNames<TClients, TDb>>(collection: X, pipeline?: mongodb.Document[], options?: mongodb.ChangeStreamOptions) => mongodb.ChangeStream<CollectionDoc<TClients[TDb]["collections"][X]>, mongodb.ChangeStreamDocument<CollectionDoc<TClients[TDb]["collections"][X]>>>;
2035
2072
  query: <X extends ColNames<TClients, TDb>>(collection: X) => QueryBuilder<CollectionDoc<TClients[TDb]["collections"][X]>, CollectionDoc<TClients[TDb]["collections"][X]>>;
2036
2073
  insertOne: <X extends ColNames<TClients, TDb>>(collection: X, doc: InsertInput<CollectionDoc<TClients[TDb]["collections"][X]>>, options?: mongodb.InsertOneOptions & QueryOptions) => Promise<mongodb.InsertOneResult<CollectionDoc<TClients[TDb]["collections"][X]>>>;
2037
2074
  insertMany: <X extends ColNames<TClients, TDb>>(collection: X, docs: readonly InsertInput<CollectionDoc<TClients[TDb]["collections"][X]>>[], options?: mongodb.BulkWriteOptions & QueryOptions) => Promise<mongodb.InsertManyResult<CollectionDoc<TClients[TDb]["collections"][X]>>>;
@@ -2047,8 +2084,8 @@ declare const makeCrudOps: <TClients extends DbClientsDefinition, TDb extends Ex
2047
2084
  filter: FilterInput<CollectionDoc<TClients[TDb]["collections"][X]>>;
2048
2085
  update: UpdateInput<CollectionDoc<TClients[TDb]["collections"][X]>>;
2049
2086
  upsert?: boolean;
2050
- }[], options?: mongodb.BulkWriteOptions & QueryOptions) => Promise<bson.Document>;
2051
- bulkWrite: <X extends ColNames<TClients, TDb>>(collection: X, operations: mongodb.AnyBulkWriteOperation<CollectionDoc<TClients[TDb]["collections"][X]>>[], options?: mongodb.BulkWriteOptions & QueryOptions) => Promise<bson.Document>;
2087
+ }[], options?: mongodb.BulkWriteOptions & QueryOptions) => Promise<mongodb.Document>;
2088
+ bulkWrite: <X extends ColNames<TClients, TDb>>(collection: X, operations: mongodb.AnyBulkWriteOperation<CollectionDoc<TClients[TDb]["collections"][X]>>[], options?: mongodb.BulkWriteOptions & QueryOptions) => Promise<mongodb.Document>;
2052
2089
  updateWithVersion: <X extends ColNames<TClients, TDb>>(collection: X, filter: FilterInput<CollectionDoc<TClients[TDb]["collections"][X]>>, update: UpdateInput<CollectionDoc<TClients[TDb]["collections"][X]>>, options?: mongodb.FindOneAndUpdateOptions & QueryOptions) => Promise<VersionedUpdateResult<CollectionDoc<TClients[TDb]["collections"][X]>>>;
2053
2090
  findOneAndDelete: <X extends ColNames<TClients, TDb>>(collection: X, filter: FilterInput<CollectionDoc<TClients[TDb]["collections"][X]>>, options?: mongodb.FindOneAndDeleteOptions & QueryOptions) => Promise<CollectionDoc<TClients[TDb]["collections"][X]> | null>;
2054
2091
  replaceOne: <X extends ColNames<TClients, TDb>>(collection: X, filter: FilterInput<CollectionDoc<TClients[TDb]["collections"][X]>>, replacement: InsertInput<CollectionDoc<TClients[TDb]["collections"][X]>>, options?: mongodb.ReplaceOptions & QueryOptions) => Promise<mongodb.UpdateResult<CollectionDoc<TClients[TDb]["collections"][X]>>>;
@@ -2420,9 +2457,19 @@ interface SchemaOpsOptions {
2420
2457
  getDefinition: (logical: string) => CollectionRegistryEntry | undefined;
2421
2458
  }
2422
2459
  /**
2423
- * Schema-first collection management. `createSchema` provisions a collection
2424
- * with a `$jsonSchema` validator derived from the ORM schema (and creates any
2425
- * declared indexes); `updateSchema` hot-swaps the validator via `collMod`.
2460
+ * Schema-first collection management.
2461
+ *
2462
+ * `createSchema` is IDEMPOTENT and SELF-RECONCILING: on a NEW collection it
2463
+ * provisions the `$jsonSchema` validator derived from the ORM schema plus the
2464
+ * declared indexes; when the collection ALREADY exists it upgrades the DB
2465
+ * schema to match the current ORM schema — hot-swapping the validator via
2466
+ * `collMod` only when it has actually drifted, and creating any newly-declared
2467
+ * indexes. It never drops indexes or data (destructive reconciliation stays
2468
+ * explicit via `syncIndexes`), so it is safe to call on every boot: the Mongo
2469
+ * schema follows the app schema automatically as models change.
2470
+ *
2471
+ * `updateSchema` hot-swaps a validator explicitly (via `collMod`);
2472
+ * `syncIndexes` reconciles index drift (create missing, drop undeclared).
2426
2473
  */
2427
2474
  declare const makeSchemaOps: (client: Db, _logger: LoggerLike, opts: SchemaOpsOptions) => {
2428
2475
  createSchema: (collection: string, createCollectionOptions?: CreateCollectionOptions) => Promise<void>;