@classytic/repo-core 0.5.0 → 0.6.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/CHANGELOG.md +20 -1
- package/README.md +65 -3
- package/dist/events/emit.d.mts +22 -0
- package/dist/events/emit.mjs +105 -0
- package/dist/events/index.d.mts +3 -0
- package/dist/events/index.mjs +2 -0
- package/dist/events/types.d.mts +73 -0
- package/dist/filter/from-record.d.mts +13 -0
- package/dist/filter/from-record.mjs +135 -0
- package/dist/filter/index.d.mts +2 -1
- package/dist/filter/index.mjs +2 -1
- package/dist/hooks/priority.d.mts +7 -1
- package/dist/hooks/priority.mjs +1 -0
- package/dist/repository/base.d.mts +32 -0
- package/dist/repository/base.mjs +25 -0
- package/dist/repository/capabilities.d.mts +159 -0
- package/dist/repository/index.d.mts +4 -2
- package/dist/repository/index.mjs +2 -1
- package/dist/repository/options.d.mts +5 -1
- package/dist/repository/options.mjs +6 -1
- package/dist/repository/purge.mjs +21 -21
- package/dist/repository/resilience.d.mts +55 -0
- package/dist/repository/resilience.mjs +39 -0
- package/dist/repository/types.d.mts +73 -14
- package/dist/schema/index.d.mts +2 -1
- package/dist/schema/index.mjs +2 -1
- package/dist/schema/standard-schema.d.mts +89 -0
- package/dist/schema/standard-schema.mjs +59 -0
- package/dist/testing/index.d.mts +2 -1
- package/dist/testing/types.d.mts +12 -112
- package/package.json +5 -1
|
@@ -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 {
|
|
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
|
|
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
|
|
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?:
|
|
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,
|
|
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 };
|
package/dist/schema/index.d.mts
CHANGED
|
@@ -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 };
|
package/dist/schema/index.mjs
CHANGED
|
@@ -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 };
|