@classytic/repo-core 0.5.0 → 0.6.1

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.
@@ -0,0 +1,159 @@
1
+ //#region src/repository/capabilities.d.ts
2
+ /**
3
+ * Runtime capability descriptor — the feature-detection contract every
4
+ * kit declares so hosts (and arc) can branch on backend support at boot
5
+ * instead of discovering an `UnsupportedOperationError` at runtime.
6
+ *
7
+ * One shape, two consumers:
8
+ *
9
+ * - **Runtime**: `repo.capabilities.arrayOperators` tells a kit-portable
10
+ * host whether `$push` / `$pull` updates will work before it ships a
11
+ * write that throws on SQLite.
12
+ * - **Conformance**: the cross-kit test harness declares the same shape
13
+ * (`ConformanceFeatures` in `@classytic/repo-core/testing` is an alias
14
+ * of this type) — the flags a kit declares at runtime are exactly the
15
+ * scenarios the conformance suite exercises. One source of truth; the
16
+ * two can't drift.
17
+ *
18
+ * **Stability contract.** Adding a flag is additive — kits that don't
19
+ * declare a new optional key default to "not supported", the conservative
20
+ * read. Renaming or removing a flag is a breaking change.
21
+ *
22
+ * **Naming convention.** Flag names match the surface they gate
23
+ * (`percentile` → `AggMeasure.op === 'percentile'`, `changeStreams` →
24
+ * `StandardRepo.watch`). When in doubt, grep the contract types and use
25
+ * the same identifier.
26
+ */
27
+ /**
28
+ * Per-aggregate-op support matrix. Some aggregate ops aren't portable
29
+ * across every backend — `percentile` requires Mongo 7+'s `$percentile`
30
+ * accumulator or SQL's `PERCENTILE_CONT`, neither of which sqlitekit
31
+ * ships. Kits opt INTO support; absent keys mean "not supported".
32
+ */
33
+ interface AggregateOpsSupport {
34
+ /**
35
+ * `{ op: 'percentile', field, p }` measure. Mongokit (Mongo 7+)
36
+ * supports it; sqlitekit throws by design (no native function).
37
+ * Hosts targeting percentile dashboards pin to a kit that supports it.
38
+ */
39
+ percentile?: boolean;
40
+ /**
41
+ * `{ op: 'stddev', field }` / `{ op: 'stddevPop', field }` measures.
42
+ * Mongokit supports both via native `$stdDevSamp` / `$stdDevPop`
43
+ * (Welford). Sqlitekit throws — SQLite has no native STDDEV and
44
+ * the computational formula is numerically unstable. Hosts pin
45
+ * to mongokit / future pgkit when stddev is load-bearing.
46
+ */
47
+ stddev?: boolean;
48
+ /**
49
+ * `topN: { partitionBy, sortBy, limit, ties }` filter. Both
50
+ * mongokit and sqlitekit support it as of repo-core 0.4.x; the
51
+ * flag exists for future kits that may not ship window-function
52
+ * equivalents.
53
+ */
54
+ topN?: boolean;
55
+ /**
56
+ * `dateBuckets: { ..., interval: { every, unit } }` custom-bin
57
+ * form. Kits that only support named-bucket form can leave this
58
+ * `false`; tests for `'minute'` / `'hour'` named intervals are
59
+ * gated separately via `dateBucketSubMinute`.
60
+ */
61
+ customDateBuckets?: boolean;
62
+ /**
63
+ * Sub-day-granularity named buckets (`'minute'` / `'hour'`).
64
+ * Older kits may only support day+ named intervals; flag exists
65
+ * to gate those scenarios cleanly.
66
+ */
67
+ dateBucketSubMinute?: boolean;
68
+ /**
69
+ * Per-request `cache?: AggCacheOptions` slot — TTL / tags / SWR /
70
+ * bypass / `repo.invalidateAggregateCache(tags)`. Both mongokit
71
+ * and sqlitekit support it as of repo-core 0.4.x. Future kits
72
+ * without the wiring can leave this false to skip cache scenarios.
73
+ *
74
+ * Independent of which CACHE BACKEND the harness wires — test
75
+ * scenarios construct their own `createMemoryCacheAdapter()` so
76
+ * this flag is purely "does the kit honour the request slot".
77
+ */
78
+ cache?: boolean;
79
+ }
80
+ /**
81
+ * Per-kit capability flags. Every `StandardRepo` implementation declares
82
+ * one of these as `readonly capabilities` — the runtime twin of the
83
+ * conformance harness's feature declaration.
84
+ *
85
+ * Hosts that target multiple kits feature-detect once at boot:
86
+ *
87
+ * ```ts
88
+ * if (!repo.capabilities.arrayOperators) {
89
+ * // SQL kit — model tags as a join table instead of $push on a JSON column
90
+ * }
91
+ * ```
92
+ */
93
+ interface RepoCapabilities {
94
+ /** `withTransaction(fn)` — D1 throws, standalone Mongo throws 263. */
95
+ transactions: boolean;
96
+ /**
97
+ * True if calling `withTransaction` inside another `withTransaction`
98
+ * callback is expected to work. Mongo's driver supports it via the
99
+ * same session; SQL drivers typically reject it.
100
+ */
101
+ nestedTransactions: boolean;
102
+ /** `findOneAndUpdate` with upsert: true. */
103
+ upsert: boolean;
104
+ /** `isDuplicateKeyError(err)` classifier. */
105
+ duplicateKeyError: boolean;
106
+ /** `distinct(field)`. */
107
+ distinct: boolean;
108
+ /**
109
+ * Portable `aggregate({ measures, groupBy, having })`. Coarse
110
+ * top-level flag. Per-op flags live on `aggregateOps` for asymmetric
111
+ * capabilities (percentile, custom date bins, etc.).
112
+ */
113
+ aggregate: boolean;
114
+ /**
115
+ * Per-op feature matrix for the aggregate surface. Optional —
116
+ * absent matrix or absent key both mean "not supported", so kits
117
+ * opt INTO ops they implement.
118
+ */
119
+ aggregateOps?: AggregateOpsSupport;
120
+ /** `getOrCreate(filter, data)`. */
121
+ getOrCreate: boolean;
122
+ /** `count(filter)` and `exists(filter)`. */
123
+ countAndExists: boolean;
124
+ /**
125
+ * `purgeByField(field, value, strategy, options)` — compliance-grade
126
+ * tenant cleanup primitive.
127
+ */
128
+ purgeByField?: boolean;
129
+ /**
130
+ * Mongo-style array update operators (`$push`, `$pull`, `$addToSet`,
131
+ * `$pop`, `$pullAll`). Mongokit: native. Sqlitekit: implemented over
132
+ * JSON TEXT columns via `json_insert` / `json_each` rewrites — see
133
+ * the sqlitekit docs for the supported subset.
134
+ */
135
+ arrayOperators?: boolean;
136
+ /**
137
+ * Filter IR `regex` op. Mongokit: native `$regex`. Sqlitekit throws
138
+ * unless the host registers a `REGEXP` SQL function on the connection.
139
+ */
140
+ regexFilter?: boolean;
141
+ /**
142
+ * `watch(filter?)` change feed — `AsyncIterable<ChangeEvent<TDoc>>`.
143
+ * Mongokit: Mongo change streams (replica set required). Kits without
144
+ * a native feed leave this false and omit the method.
145
+ */
146
+ changeStreams?: boolean;
147
+ /**
148
+ * `lean: true` read option — return plain objects instead of driver
149
+ * documents. SQL kits return plain rows always (trivially true);
150
+ * mongokit opts in once reads honor the flag.
151
+ */
152
+ lean?: boolean;
153
+ /** Portable `lookupPopulate(options)` join IR. */
154
+ lookupPopulate?: boolean;
155
+ /** `cursor(filter, options)` streaming reads (AsyncIterable batches). */
156
+ streaming?: boolean;
157
+ }
158
+ //#endregion
159
+ export { AggregateOpsSupport, RepoCapabilities };
@@ -3,7 +3,9 @@ import { UpdateInput } from "../update/types.mjs";
3
3
  import { nestDottedKeys, nestDottedKeysAll } from "./agg-output.mjs";
4
4
  import { PLUGIN_ORDER_CONSTRAINTS, Plugin, PluginFunction, PluginType, validatePluginOrder } from "./plugin-types.mjs";
5
5
  import { RepositoryBase, RepositoryBaseOptions } from "./base.mjs";
6
+ import { AggregateOpsSupport, RepoCapabilities } from "./capabilities.mjs";
6
7
  import { STANDARD_REPO_OPTION_KEYS, StandardRepoOptionKey } from "./options.mjs";
7
- import { AggCacheOptions, AggDateBucket, AggDateBucketInterval, AggDateBucketUnit, AggExecutionHints, AggMeasure, AggPaginationRequest, AggRequest, AggResult, AggRow, AggTopN, AggTopNTies, BulkCreateResult, BulkWriteOperation, BulkWriteResult, ClaimTransition, ClaimVersionTransition, DeleteManyResult, DeleteOptions, DeleteResult, FilterInput, FindAllOptions, FindOneAndUpdateOptions, InferDoc, KeysetAggPaginationResult, MinimalRepo, PaginationParams, PurgeRetryPolicy, QueryOptions, RepositorySession, StandardRepo, TenantPurgeOptions, TenantPurgeProgress, TenantPurgeResult, TenantPurgeStrategy, UpdateManyResult, WriteOptions } from "./types.mjs";
8
+ import { RetryPolicy, throwIfAborted, withRetry } from "./resilience.mjs";
9
+ import { AggCacheOptions, AggDateBucket, AggDateBucketInterval, AggDateBucketUnit, AggExecutionHints, AggMeasure, AggPaginationRequest, AggRequest, AggResult, AggRow, AggTopN, AggTopNTies, BulkCreateResult, BulkWriteOperation, BulkWriteResult, ChangeEvent, ClaimTransition, ClaimVersionTransition, DeleteManyResult, DeleteOptions, DeleteResult, FilterInput, FindAllOptions, FindOneAndUpdateOptions, InferDoc, KeysetAggPaginationResult, MinimalRepo, PaginationParams, QueryOptions, RepositorySession, StandardRepo, TenantPurgeOptions, TenantPurgeProgress, TenantPurgeResult, TenantPurgeStrategy, UpdateManyResult, WatchOptions, WriteOptions } from "./types.mjs";
8
10
  import { PurgePort, WritingPurgeStrategy, runChunkedPurge } from "./purge.mjs";
9
- export { type AggCacheOptions, type AggDateBucket, type AggDateBucketInterval, type AggDateBucketUnit, type AggExecutionHints, type AggMeasure, type AggPaginationRequest, type AggRequest, type AggResult, type AggRow, type AggTopN, type AggTopNTies, type BulkCreateResult, type BulkWriteOperation, type BulkWriteResult, type ClaimTransition, type ClaimVersionTransition, type DeleteManyResult, type DeleteOptions, type DeleteResult, type FilterInput, type FindAllOptions, type FindOneAndUpdateOptions, type InferDoc, type KeysetAggPaginationResult, type LookupPopulateOptions, type LookupPopulateResult, type LookupRow, type LookupSpec, type MinimalRepo, PLUGIN_ORDER_CONSTRAINTS, type PaginationParams, type Plugin, type PluginFunction, type PluginType, type PurgePort, type PurgeRetryPolicy, type QueryOptions, RepositoryBase, type RepositoryBaseOptions, type RepositorySession, STANDARD_REPO_OPTION_KEYS, type StandardRepo, type StandardRepoOptionKey, type TenantPurgeOptions, type TenantPurgeProgress, type TenantPurgeResult, type TenantPurgeStrategy, type UpdateInput, type UpdateManyResult, type WriteOptions, type WritingPurgeStrategy, nestDottedKeys, nestDottedKeysAll, runChunkedPurge, validatePluginOrder };
11
+ export { type AggCacheOptions, type AggDateBucket, type AggDateBucketInterval, type AggDateBucketUnit, type AggExecutionHints, type AggMeasure, type AggPaginationRequest, type AggRequest, type AggResult, type AggRow, type AggTopN, type AggTopNTies, type AggregateOpsSupport, type BulkCreateResult, type BulkWriteOperation, type BulkWriteResult, type ChangeEvent, type ClaimTransition, type ClaimVersionTransition, type DeleteManyResult, type DeleteOptions, type DeleteResult, type FilterInput, type FindAllOptions, type FindOneAndUpdateOptions, type InferDoc, type KeysetAggPaginationResult, type LookupPopulateOptions, type LookupPopulateResult, type LookupRow, type LookupSpec, type MinimalRepo, PLUGIN_ORDER_CONSTRAINTS, type PaginationParams, type Plugin, type PluginFunction, type PluginType, type PurgePort, type QueryOptions, type RepoCapabilities, RepositoryBase, type RepositoryBaseOptions, type RepositorySession, type RetryPolicy, STANDARD_REPO_OPTION_KEYS, type StandardRepo, type StandardRepoOptionKey, type TenantPurgeOptions, type TenantPurgeProgress, type TenantPurgeResult, type TenantPurgeStrategy, type UpdateInput, type UpdateManyResult, type WatchOptions, type WriteOptions, type WritingPurgeStrategy, nestDottedKeys, nestDottedKeysAll, runChunkedPurge, throwIfAborted, validatePluginOrder, withRetry };
@@ -2,5 +2,6 @@ import { nestDottedKeys, nestDottedKeysAll } from "./agg-output.mjs";
2
2
  import { PLUGIN_ORDER_CONSTRAINTS, validatePluginOrder } from "./plugin-types.mjs";
3
3
  import { RepositoryBase } from "./base.mjs";
4
4
  import { STANDARD_REPO_OPTION_KEYS } from "./options.mjs";
5
+ import { throwIfAborted, withRetry } from "./resilience.mjs";
5
6
  import { runChunkedPurge } from "./purge.mjs";
6
- export { PLUGIN_ORDER_CONSTRAINTS, RepositoryBase, STANDARD_REPO_OPTION_KEYS, nestDottedKeys, nestDottedKeysAll, runChunkedPurge, validatePluginOrder };
7
+ export { PLUGIN_ORDER_CONSTRAINTS, RepositoryBase, STANDARD_REPO_OPTION_KEYS, nestDottedKeys, nestDottedKeysAll, runChunkedPurge, throwIfAborted, validatePluginOrder, withRetry };
@@ -40,13 +40,17 @@
40
40
  * boundary.
41
41
  * - `requestId` — request correlation id for trace stitching across
42
42
  * logs, events, and downstream service calls.
43
+ * - `traceId` — distributed-tracing trace id (W3C traceparent /
44
+ * OpenTelemetry). Observability plugins read it to join repo spans
45
+ * onto the host's trace; distinct from `requestId`, which is the
46
+ * host's own correlation id and may outlive a single trace.
43
47
  *
44
48
  * Frameworks should treat this set as the canonical forward list:
45
49
  * peel matching keys off the request context, drop them into the
46
50
  * options bag, and let kit plugins read what they implement. Unknown
47
51
  * ctx keys do NOT forward — the bag stays narrow.
48
52
  */
49
- declare const STANDARD_REPO_OPTION_KEYS: readonly ["organizationId", "userId", "user", "session", "requestId"];
53
+ declare const STANDARD_REPO_OPTION_KEYS: readonly ["organizationId", "userId", "user", "session", "requestId", "traceId"];
50
54
  /**
51
55
  * Type-level union of canonical option keys. Use to constrain
52
56
  * framework helpers that thread request context into repo options:
@@ -40,6 +40,10 @@
40
40
  * boundary.
41
41
  * - `requestId` — request correlation id for trace stitching across
42
42
  * logs, events, and downstream service calls.
43
+ * - `traceId` — distributed-tracing trace id (W3C traceparent /
44
+ * OpenTelemetry). Observability plugins read it to join repo spans
45
+ * onto the host's trace; distinct from `requestId`, which is the
46
+ * host's own correlation id and may outlive a single trace.
43
47
  *
44
48
  * Frameworks should treat this set as the canonical forward list:
45
49
  * peel matching keys off the request context, drop them into the
@@ -51,7 +55,8 @@ const STANDARD_REPO_OPTION_KEYS = [
51
55
  "userId",
52
56
  "user",
53
57
  "session",
54
- "requestId"
58
+ "requestId",
59
+ "traceId"
55
60
  ];
56
61
  //#endregion
57
62
  export { STANDARD_REPO_OPTION_KEYS };
@@ -1,5 +1,25 @@
1
+ import { withRetry } from "./resilience.mjs";
1
2
  //#region src/repository/purge.ts
2
3
  /**
4
+ * Chunked tenant-purge orchestrator — kit-agnostic.
5
+ *
6
+ * Owns the loop / signal / progress / retry / error envelope for
7
+ * `StandardRepo.purgeByField`. Each kit (mongokit, sqlitekit, future
8
+ * pgkit) plugs in a `PurgePort` that knows how to talk to its driver;
9
+ * the orchestrator drives the chunked work.
10
+ *
11
+ * **Why a single-method port** (`purgeChunk(strategy, limit)`): each
12
+ * driver has different round-trip optima — sqlite hard-strategy compiles
13
+ * to one `DELETE … LIMIT` (no SELECT), mongo hard-strategy needs SELECT
14
+ * + deleteMany, anonymize-with-function-form needs SELECT + bulkWrite
15
+ * to batch heterogeneous patches in one round-trip. A two-method port
16
+ * (`selectChunkIds` + `applyStrategy`) forces 2 round-trips for every
17
+ * kit; the single method lets each port pick its own access shape.
18
+ *
19
+ * Hexagonal pattern: the orchestrator is the use-case; `PurgePort` is
20
+ * the driving port; each kit's port factory is the adapter.
21
+ */
22
+ /**
3
23
  * Drive a chunked purge to completion. Returns a `TenantPurgeResult`
4
24
  * envelope describing what happened — never throws for in-strategy
5
25
  * errors (those wrap into `result.error`); only throws for invalid
@@ -30,7 +50,7 @@ async function runChunkedPurge(strategy, options, port) {
30
50
  ok: false,
31
51
  durationMs: Date.now() - start
32
52
  };
33
- const chunkSize = await runChunkWithRetry(() => port.purgeChunk(strategy, batchSize), retry);
53
+ const chunkSize = await withRetry(() => port.purgeChunk(strategy, batchSize), retry, options.signal);
34
54
  if (chunkSize === 0) break;
35
55
  processed += chunkSize;
36
56
  if (options.onProgress) await options.onProgress({
@@ -59,25 +79,5 @@ async function runChunkedPurge(strategy, options, port) {
59
79
  durationMs: Date.now() - start
60
80
  };
61
81
  }
62
- /**
63
- * Run `fn` with exponential backoff when retry is enabled. Falls through
64
- * to a single attempt when `retry` is undefined (default behavior).
65
- */
66
- async function runChunkWithRetry(fn, retry) {
67
- if (!retry) return fn();
68
- const maxAttempts = retry.maxAttempts ?? 3;
69
- const baseDelayMs = retry.baseDelayMs ?? 100;
70
- const shouldRetry = retry.shouldRetry ?? (() => true);
71
- let lastErr;
72
- for (let attempt = 0; attempt < maxAttempts; attempt++) try {
73
- return await fn();
74
- } catch (err) {
75
- lastErr = err;
76
- if (attempt === maxAttempts - 1) break;
77
- if (!shouldRetry(err, attempt + 1)) break;
78
- await new Promise((r) => setTimeout(r, baseDelayMs * 2 ** attempt));
79
- }
80
- throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
81
- }
82
82
  //#endregion
83
83
  export { runChunkedPurge };
@@ -0,0 +1,55 @@
1
+ //#region src/repository/resilience.d.ts
2
+ /**
3
+ * Resilience primitives — the single retry/abort contract every kit and
4
+ * every chunked orchestrator (purge, batch imports, outbox relays) uses.
5
+ *
6
+ * One `RetryPolicy` shape across the contract: `QueryOptions.retryPolicy`,
7
+ * `TenantPurgeOptions.retry`, and any kit-internal retry loop all accept
8
+ * the same three knobs. One `withRetry` implementation so backoff math
9
+ * never drifts between call sites.
10
+ */
11
+ /**
12
+ * Retry policy for transient failures (network blips, write conflicts,
13
+ * busy-locks, connection resets).
14
+ *
15
+ * **Don't retry blindly.** Validation errors, schema errors, permission
16
+ * errors are NOT transient — retrying just delays the same failure.
17
+ * Mongo `WriteConflict`, SQLite `SQLITE_BUSY`, `ECONNRESET` ARE transient
18
+ * — backoff + retry recovers. Pass `shouldRetry` to narrow when you know
19
+ * your driver's error taxonomy:
20
+ *
21
+ * ```ts
22
+ * retryPolicy: {
23
+ * maxAttempts: 3, // default 3 when block present
24
+ * baseDelayMs: 100, // exponential: 100ms, 200ms, 400ms
25
+ * shouldRetry: (err) =>
26
+ * /WriteConflict|SQLITE_BUSY|ECONNRESET/i.test(String(err)),
27
+ * }
28
+ * ```
29
+ */
30
+ interface RetryPolicy {
31
+ /** Max attempts (including the first try). Default 3 when a policy is present. */
32
+ maxAttempts?: number;
33
+ /** Base delay (ms) for exponential backoff. Default 100ms; doubles each attempt. */
34
+ baseDelayMs?: number;
35
+ /** Decide whether a given error is transient. Default: retry every error. */
36
+ shouldRetry?: (err: unknown, attempt: number) => boolean;
37
+ }
38
+ /**
39
+ * Run `fn` with exponential backoff when a policy is provided. Falls
40
+ * through to a single attempt when `policy` is undefined — callers wrap
41
+ * unconditionally and the no-policy path costs nothing.
42
+ *
43
+ * Honors `signal`: aborts between attempts (never mid-attempt) by
44
+ * rethrowing the signal's abort reason.
45
+ */
46
+ declare function withRetry<T>(fn: () => Promise<T>, policy: RetryPolicy | undefined, signal?: AbortSignal): Promise<T>;
47
+ /**
48
+ * Abort guard for op boundaries. Kits call this at the top of every
49
+ * operation (and between chunks of chunked work) when the caller passed
50
+ * `options.signal` — cancelled requests stop before the next driver
51
+ * round-trip instead of running to completion.
52
+ */
53
+ declare function throwIfAborted(signal: AbortSignal | undefined): void;
54
+ //#endregion
55
+ export { RetryPolicy, throwIfAborted, withRetry };
@@ -0,0 +1,39 @@
1
+ //#region src/repository/resilience.ts
2
+ /**
3
+ * Run `fn` with exponential backoff when a policy is provided. Falls
4
+ * through to a single attempt when `policy` is undefined — callers wrap
5
+ * unconditionally and the no-policy path costs nothing.
6
+ *
7
+ * Honors `signal`: aborts between attempts (never mid-attempt) by
8
+ * rethrowing the signal's abort reason.
9
+ */
10
+ async function withRetry(fn, policy, signal) {
11
+ if (!policy) return fn();
12
+ const maxAttempts = policy.maxAttempts ?? 3;
13
+ const baseDelayMs = policy.baseDelayMs ?? 100;
14
+ const shouldRetry = policy.shouldRetry ?? (() => true);
15
+ let lastErr;
16
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
17
+ signal?.throwIfAborted();
18
+ try {
19
+ return await fn();
20
+ } catch (err) {
21
+ lastErr = err;
22
+ if (attempt === maxAttempts - 1) break;
23
+ if (!shouldRetry(err, attempt + 1)) break;
24
+ await new Promise((r) => setTimeout(r, baseDelayMs * 2 ** attempt));
25
+ }
26
+ }
27
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
28
+ }
29
+ /**
30
+ * Abort guard for op boundaries. Kits call this at the top of every
31
+ * operation (and between chunks of chunked work) when the caller passed
32
+ * `options.signal` — cancelled requests stop before the next driver
33
+ * round-trip instead of running to completion.
34
+ */
35
+ function throwIfAborted(signal) {
36
+ signal?.throwIfAborted();
37
+ }
38
+ //#endregion
39
+ export { throwIfAborted, withRetry };
@@ -2,7 +2,9 @@ import { Filter } from "../filter/types.mjs";
2
2
  import { OffsetPaginationResult } from "../pagination/types.mjs";
3
3
  import { LookupPopulateOptions, LookupPopulateResult, LookupSpec } from "../lookup/types.mjs";
4
4
  import { UpdateInput } from "../update/types.mjs";
5
+ import { RepoCapabilities } from "./capabilities.mjs";
5
6
  import { CacheOptions } from "../cache/options.mjs";
7
+ import { RetryPolicy } from "./resilience.mjs";
6
8
 
7
9
  //#region src/repository/types.d.ts
8
10
  /**
@@ -51,6 +53,18 @@ interface QueryOptions {
51
53
  user?: Record<string, unknown>;
52
54
  /** Arc request context (orgId, roles, requestId, ...). */
53
55
  context?: Record<string, unknown>;
56
+ /**
57
+ * Abort signal. Kits check it at the op boundary (and between chunks of
58
+ * chunked work) — cancelled requests stop before the next driver
59
+ * round-trip. Aborting never rolls back a write that already committed.
60
+ */
61
+ signal?: AbortSignal;
62
+ /**
63
+ * Retry transient driver failures with exponential backoff. Same shape
64
+ * everywhere (`TenantPurgeOptions.retry`, kit-internal loops) — see
65
+ * {@link RetryPolicy}. Default: no retry.
66
+ */
67
+ retryPolicy?: RetryPolicy;
54
68
  /** Driver-specific escape hatch — see JSDoc. */
55
69
  [key: string]: unknown;
56
70
  }
@@ -195,19 +209,7 @@ interface TenantPurgeOptions {
195
209
  * stay committed. A retry that eventually succeeds reports `ok: true`;
196
210
  * one that exhausts `maxAttempts` aborts with the underlying error.
197
211
  */
198
- retry?: PurgeRetryPolicy;
199
- }
200
- /**
201
- * Retry policy for `purgeByField` chunk-level failures. See
202
- * `TenantPurgeOptions.retry` for the full contract.
203
- */
204
- interface PurgeRetryPolicy {
205
- /** Max attempts per chunk (including the first try). Default 3. */
206
- maxAttempts?: number;
207
- /** Base delay (ms) for exponential backoff. Default 100ms. */
208
- baseDelayMs?: number;
209
- /** Decide whether a given error is transient. Default: retry every error. */
210
- shouldRetry?: (err: unknown, attempt: number) => boolean;
212
+ retry?: RetryPolicy;
211
213
  }
212
214
  /** Chunk-level progress event for `purgeByField`. */
213
215
  interface TenantPurgeProgress {
@@ -1275,6 +1277,18 @@ interface MinimalRepo<TDoc> {
1275
1277
  * kit-native.
1276
1278
  */
1277
1279
  interface StandardRepo<TDoc> extends MinimalRepo<TDoc> {
1280
+ /**
1281
+ * Runtime capability descriptor — feature-detection at boot instead of
1282
+ * `UnsupportedOperationError` at runtime. Required: every kit declares
1283
+ * what its backend supports (`arrayOperators`, `changeStreams`,
1284
+ * `aggregateOps.percentile`, ...) so kit-portable hosts and arc can
1285
+ * branch once instead of try/catching per call.
1286
+ *
1287
+ * The same shape gates the cross-kit conformance suite
1288
+ * (`ConformanceFeatures` is an alias) — runtime declaration and test
1289
+ * coverage cannot drift.
1290
+ */
1291
+ readonly capabilities: RepoCapabilities;
1278
1292
  /**
1279
1293
  * Atomic compare-and-set. Match one document, mutate it, return the
1280
1294
  * post-update doc (or pre-update when `returnDocument: 'before'`).
@@ -1608,6 +1622,51 @@ interface StandardRepo<TDoc> extends MinimalRepo<TDoc> {
1608
1622
  * ```
1609
1623
  */
1610
1624
  withTransaction?<T>(fn: (txRepo: StandardRepo<TDoc>) => Promise<T>, options?: Record<string, unknown>): Promise<T>;
1625
+ /**
1626
+ * Portable change feed — `for await` over committed mutations:
1627
+ *
1628
+ * ```ts
1629
+ * for await (const change of repo.watch!({ status: 'pending' })) {
1630
+ * if (change.operation === 'create') enqueue(change.doc!);
1631
+ * }
1632
+ * ```
1633
+ *
1634
+ * Backends differ wildly here, so the method is optional and gated by
1635
+ * `capabilities.changeStreams`:
1636
+ * - mongokit — Mongo change streams (`Model.watch`); requires a
1637
+ * replica set. `fullDocument: 'updateLookup'` semantics for updates.
1638
+ * - SQL kits — typically absent (no native feed). Hosts that need
1639
+ * a feed on SQL pair the repo with `events` emission instead.
1640
+ *
1641
+ * The iterator ends when `options.signal` aborts. Errors from the
1642
+ * underlying stream propagate to the consumer.
1643
+ */
1644
+ watch?(filter?: FilterInput, options?: WatchOptions): AsyncIterable<ChangeEvent<TDoc>>;
1645
+ }
1646
+ /** A single committed mutation observed by `watch()`. */
1647
+ interface ChangeEvent<TDoc = unknown> {
1648
+ /** What happened. `replace` = full-document overwrite (Mongo `replaceOne`). */
1649
+ operation: 'create' | 'update' | 'delete' | 'replace';
1650
+ /** Primary key of the affected document. */
1651
+ id?: unknown;
1652
+ /**
1653
+ * The post-image document — present on create/replace always, on update
1654
+ * when the backend supports post-image lookup, absent on delete.
1655
+ */
1656
+ doc?: TDoc;
1657
+ /** Commit timestamp as reported by the backend. */
1658
+ timestamp: Date;
1659
+ }
1660
+ /** Options for `StandardRepo.watch()`. */
1661
+ interface WatchOptions {
1662
+ /** End the iterator. The only portable way to stop a change feed. */
1663
+ signal?: AbortSignal;
1664
+ /**
1665
+ * Resume token / cursor from a previous stream (kit-specific shape —
1666
+ * Mongo resume tokens are opaque BSON). Hosts persist and replay it
1667
+ * for at-least-once consumption across restarts.
1668
+ */
1669
+ resumeAfter?: unknown;
1611
1670
  }
1612
1671
  //#endregion
1613
- export { AggCacheOptions, AggDateBucket, AggDateBucketInterval, AggDateBucketUnit, AggExecutionHints, AggMeasure, AggPaginationRequest, AggRequest, AggResult, AggRow, AggTopN, AggTopNTies, BulkCreateResult, BulkWriteOperation, BulkWriteResult, ClaimTransition, ClaimVersionTransition, DeleteManyResult, DeleteOptions, DeleteResult, FilterInput, FindAllOptions, FindOneAndUpdateOptions, InferDoc, KeysetAggPaginationResult, MinimalRepo, PaginationParams, PurgeRetryPolicy, QueryOptions, RepositorySession, StandardRepo, TenantPurgeOptions, TenantPurgeProgress, TenantPurgeResult, TenantPurgeStrategy, UpdateManyResult, WriteOptions };
1672
+ export { AggCacheOptions, AggDateBucket, AggDateBucketInterval, AggDateBucketUnit, AggExecutionHints, AggMeasure, AggPaginationRequest, AggRequest, AggResult, AggRow, AggTopN, AggTopNTies, BulkCreateResult, BulkWriteOperation, BulkWriteResult, ChangeEvent, ClaimTransition, ClaimVersionTransition, DeleteManyResult, DeleteOptions, DeleteResult, FilterInput, FindAllOptions, FindOneAndUpdateOptions, InferDoc, KeysetAggPaginationResult, MinimalRepo, PaginationParams, QueryOptions, RepositorySession, StandardRepo, TenantPurgeOptions, TenantPurgeProgress, TenantPurgeResult, TenantPurgeStrategy, UpdateManyResult, WatchOptions, WriteOptions };
@@ -1,4 +1,5 @@
1
+ import { StandardSchemaV1, validateStandardSchema } from "./standard-schema.mjs";
1
2
  import { CrudSchemas, FieldRule, FieldRules, JsonSchema, SchemaBuilderOptions, ValidationResult } from "./types.mjs";
2
3
  import { applyFieldRules, applyNullable, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, mergeFieldRuleConstraints, validateUpdateBody } from "./field-rules.mjs";
3
4
  import { SchemaGenerator, SchemaGeneratorContext, isSchemaGenerator } from "./generator.mjs";
4
- export { type CrudSchemas, type FieldRule, type FieldRules, type JsonSchema, type SchemaBuilderOptions, type SchemaGenerator, type SchemaGeneratorContext, type ValidationResult, applyFieldRules, applyNullable, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, isSchemaGenerator, mergeFieldRuleConstraints, validateUpdateBody };
5
+ export { type CrudSchemas, type FieldRule, type FieldRules, type JsonSchema, type SchemaBuilderOptions, type SchemaGenerator, type SchemaGeneratorContext, type StandardSchemaV1, type ValidationResult, applyFieldRules, applyNullable, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, isSchemaGenerator, mergeFieldRuleConstraints, validateStandardSchema, validateUpdateBody };
@@ -1,3 +1,4 @@
1
+ import { validateStandardSchema } from "./standard-schema.mjs";
1
2
  import { applyFieldRules, applyNullable, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, mergeFieldRuleConstraints, validateUpdateBody } from "./field-rules.mjs";
2
3
  import { isSchemaGenerator } from "./generator.mjs";
3
- export { applyFieldRules, applyNullable, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, isSchemaGenerator, mergeFieldRuleConstraints, validateUpdateBody };
4
+ export { applyFieldRules, applyNullable, collectFieldsToOmit, getImmutableFields, getSystemManagedFields, isFieldUpdateAllowed, isSchemaGenerator, mergeFieldRuleConstraints, validateStandardSchema, validateUpdateBody };
@@ -0,0 +1,89 @@
1
+ //#region src/schema/standard-schema.d.ts
2
+ /**
3
+ * Standard Schema integration — the validator-agnostic validation slot.
4
+ *
5
+ * [Standard Schema](https://standardschema.dev) is the shared interface
6
+ * implemented by Zod 3.24+, Valibot 1.0+, ArkType 2.0+, Effect Schema and
7
+ * others. Vendoring the interface (officially encouraged — it's a
8
+ * types-only spec designed to be copied) keeps repo-core's zero-dependency
9
+ * guarantee while letting hosts plug ANY conforming validator into a
10
+ * repository:
11
+ *
12
+ * ```ts
13
+ * import { z } from 'zod';
14
+ *
15
+ * const repo = createRepository(UserModel, {
16
+ * schema: z.object({ name: z.string(), email: z.string().email() }),
17
+ * });
18
+ * await repo.create({ name: 1 }); // throws HttpError 400 with validationErrors
19
+ * ```
20
+ *
21
+ * `RepositoryBase` wires `schema` / `updateSchema` into `before:create` /
22
+ * `before:createMany` / `before:update` hooks at `HOOK_PRIORITY.VALIDATION`
23
+ * — after policy plugins (so tenant-stamped fields are present) and before
24
+ * cache/observability.
25
+ */
26
+ /** The Standard Schema interface. Any conforming validator satisfies it. */
27
+ interface StandardSchemaV1<Input = unknown, Output = Input> {
28
+ /** The Standard Schema properties. */
29
+ readonly '~standard': StandardSchemaV1.Props<Input, Output>;
30
+ }
31
+ declare namespace StandardSchemaV1 {
32
+ /** The Standard Schema properties interface. */
33
+ interface Props<Input = unknown, Output = Input> {
34
+ /** The version number of the standard. */
35
+ readonly version: 1;
36
+ /** The vendor name of the schema library. */
37
+ readonly vendor: string;
38
+ /** Validates unknown input values. */
39
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
40
+ /** Inferred types associated with the schema. */
41
+ readonly types?: Types<Input, Output> | undefined;
42
+ }
43
+ /** The result interface of the validate function. */
44
+ type Result<Output> = SuccessResult<Output> | FailureResult;
45
+ /** The result interface if validation succeeds. */
46
+ interface SuccessResult<Output> {
47
+ /** The typed output value. */
48
+ readonly value: Output;
49
+ /** The non-existent issues. */
50
+ readonly issues?: undefined;
51
+ }
52
+ /** The result interface if validation fails. */
53
+ interface FailureResult {
54
+ /** The issues of failed validation. */
55
+ readonly issues: readonly Issue[];
56
+ }
57
+ /** The issue interface of the failure output. */
58
+ interface Issue {
59
+ /** The error message of the issue. */
60
+ readonly message: string;
61
+ /** The path of the issue, if any. */
62
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
63
+ }
64
+ /** The path segment interface of the issue. */
65
+ interface PathSegment {
66
+ /** The key representing a path segment. */
67
+ readonly key: PropertyKey;
68
+ }
69
+ /** The Standard Schema types interface. */
70
+ interface Types<Input = unknown, Output = Input> {
71
+ /** The input type of the schema. */
72
+ readonly input: Input;
73
+ /** The output type of the schema. */
74
+ readonly output: Output;
75
+ }
76
+ /** Infers the input type of a Standard Schema. */
77
+ type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input'];
78
+ /** Infers the output type of a Standard Schema. */
79
+ type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output'];
80
+ }
81
+ /**
82
+ * Validate `data` against a Standard Schema. Returns the schema's typed
83
+ * output (validators may coerce/transform) or throws an `HttpError` 400
84
+ * carrying `validationErrors` + structured `meta.issues` — the same wire
85
+ * shape every kit's own validation errors serialize to.
86
+ */
87
+ declare function validateStandardSchema<TSchema extends StandardSchemaV1>(schema: TSchema, data: unknown): Promise<StandardSchemaV1.InferOutput<TSchema>>;
88
+ //#endregion
89
+ export { StandardSchemaV1, validateStandardSchema };