@classytic/repo-core 0.28.0 → 0.30.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 +1290 -1098
- package/dist/cache/engine.d.mts +7 -5
- package/dist/cache/engine.mjs +34 -16
- package/dist/cache/memo.d.mts +4 -0
- package/dist/cache/memo.mjs +17 -4
- package/dist/cache/memory-adapter.mjs +6 -4
- package/dist/cache/plugin/read-hooks.mjs +1 -1
- package/dist/cache/stable-stringify.d.mts +7 -8
- package/dist/cache/stable-stringify.mjs +14 -11
- package/dist/cache/tag-index.mjs +1 -2
- package/dist/cache/types.d.mts +4 -3
- package/dist/errors/codes.d.mts +117 -0
- package/dist/errors/codes.mjs +114 -0
- package/dist/errors/contract.d.mts +2 -1
- package/dist/errors/contract.mjs +9 -3
- package/dist/errors/index.d.mts +3 -2
- package/dist/errors/index.mjs +2 -2
- package/dist/errors/types.d.mts +3 -30
- package/dist/filter/builders.mjs +1 -1
- package/dist/filter/match-record.mjs +9 -3
- package/dist/filter/match.mjs +25 -9
- package/dist/idempotency/index.d.mts +462 -0
- package/dist/idempotency/index.mjs +419 -0
- package/dist/pagination/types.d.mts +6 -0
- package/dist/query-parser/coerce.d.mts +18 -11
- package/dist/query-parser/coerce.mjs +37 -69
- package/dist/query-parser/errors.d.mts +30 -0
- package/dist/query-parser/errors.mjs +32 -0
- package/dist/query-parser/grammar.d.mts +107 -0
- package/dist/query-parser/grammar.mjs +462 -0
- package/dist/query-parser/index.d.mts +5 -2
- package/dist/query-parser/index.mjs +5 -2
- package/dist/query-parser/parse-url.d.mts +1 -1
- package/dist/query-parser/parse-url.mjs +72 -205
- package/dist/query-parser/regex-safety.d.mts +31 -0
- package/dist/query-parser/regex-safety.mjs +151 -0
- package/dist/query-parser/reserved.mjs +1 -0
- package/dist/query-parser/types.d.mts +15 -25
- package/dist/repository/types.d.mts +6 -5
- package/dist/schema/field-rules.mjs +49 -6
- package/dist/schema/standard-schema.mjs +1 -1
- package/dist/sync/index.d.mts +90 -55
- package/dist/sync/index.mjs +0 -0
- package/dist/testing/change-log-conformance.d.mts +21 -0
- package/dist/testing/change-log-conformance.mjs +204 -0
- package/dist/testing/command-stream-conformance.d.mts +15 -0
- package/dist/testing/command-stream-conformance.mjs +163 -0
- package/dist/testing/idempotency-conformance.d.mts +15 -0
- package/dist/testing/idempotency-conformance.mjs +315 -0
- package/dist/testing/index.d.mts +5 -1
- package/dist/testing/index.mjs +5 -1
- package/dist/testing/query-grammar-conformance.d.mts +60 -0
- package/dist/testing/query-grammar-conformance.mjs +380 -0
- package/package.json +6 -2
- package/dist/errors/types.mjs +0 -29
package/dist/cache/engine.d.mts
CHANGED
|
@@ -55,13 +55,13 @@ declare class CacheEngine {
|
|
|
55
55
|
private readonly prefix;
|
|
56
56
|
private readonly jitter;
|
|
57
57
|
/**
|
|
58
|
-
* In-flight fetches keyed by cache-key.
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* unbounded burst).
|
|
58
|
+
* In-flight fetches, partitioned by the RESOLVED adapter, then keyed by cache-key. A scoped store
|
|
59
|
+
* (per request, per job) gets its own partition, so two scopes reading the same key never share a
|
|
60
|
+
* result — and a finished scope's partition is collected with its store. Process-local; cross-pod
|
|
61
|
+
* fanout is bounded to N pods.
|
|
63
62
|
*/
|
|
64
63
|
private readonly pending;
|
|
64
|
+
private pendingTotal;
|
|
65
65
|
constructor(adapter: CacheAdapterSource, options?: CacheEngineOptions);
|
|
66
66
|
/**
|
|
67
67
|
* Read a cache entry under SWR + TTL semantics. Returns a
|
|
@@ -91,6 +91,8 @@ declare class CacheEngine {
|
|
|
91
91
|
* pending.
|
|
92
92
|
*/
|
|
93
93
|
getPending<T = unknown>(key: string): Promise<T> | undefined;
|
|
94
|
+
/** This scope's in-flight partition; `undefined` when no store is in scope (never coalesce there). */
|
|
95
|
+
private pendingIn;
|
|
94
96
|
/**
|
|
95
97
|
* Atomically claim `key` for a fetch. Returns `'claimed'` when this
|
|
96
98
|
* caller owns the fetch (it must call `resolvePending` or
|
package/dist/cache/engine.mjs
CHANGED
|
@@ -35,13 +35,13 @@ var CacheEngine = class {
|
|
|
35
35
|
prefix;
|
|
36
36
|
jitter;
|
|
37
37
|
/**
|
|
38
|
-
* In-flight fetches keyed by cache-key.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* unbounded burst).
|
|
38
|
+
* In-flight fetches, partitioned by the RESOLVED adapter, then keyed by cache-key. A scoped store
|
|
39
|
+
* (per request, per job) gets its own partition, so two scopes reading the same key never share a
|
|
40
|
+
* result — and a finished scope's partition is collected with its store. Process-local; cross-pod
|
|
41
|
+
* fanout is bounded to N pods.
|
|
43
42
|
*/
|
|
44
|
-
pending = /* @__PURE__ */ new
|
|
43
|
+
pending = /* @__PURE__ */ new WeakMap();
|
|
44
|
+
pendingTotal = 0;
|
|
45
45
|
constructor(adapter, options = {}) {
|
|
46
46
|
this.resolveAdapter = typeof adapter === "function" ? adapter : () => adapter;
|
|
47
47
|
this.prefix = options.prefix ?? "rc";
|
|
@@ -125,7 +125,18 @@ var CacheEngine = class {
|
|
|
125
125
|
* pending.
|
|
126
126
|
*/
|
|
127
127
|
getPending(key) {
|
|
128
|
-
return this.
|
|
128
|
+
return this.pendingIn(false)?.get(key)?.promise;
|
|
129
|
+
}
|
|
130
|
+
/** This scope's in-flight partition; `undefined` when no store is in scope (never coalesce there). */
|
|
131
|
+
pendingIn(create) {
|
|
132
|
+
const adapter = this.resolveAdapter();
|
|
133
|
+
if (!adapter) return void 0;
|
|
134
|
+
let partition = this.pending.get(adapter);
|
|
135
|
+
if (!partition && create) {
|
|
136
|
+
partition = /* @__PURE__ */ new Map();
|
|
137
|
+
this.pending.set(adapter, partition);
|
|
138
|
+
}
|
|
139
|
+
return partition;
|
|
129
140
|
}
|
|
130
141
|
/**
|
|
131
142
|
* Atomically claim `key` for a fetch. Returns `'claimed'` when this
|
|
@@ -135,21 +146,26 @@ var CacheEngine = class {
|
|
|
135
146
|
* with the first claimer's result.
|
|
136
147
|
*/
|
|
137
148
|
claimPending(key) {
|
|
138
|
-
const
|
|
149
|
+
const partition = this.pendingIn(true);
|
|
150
|
+
if (!partition) return { status: "claimed" };
|
|
151
|
+
const existing = partition.get(key);
|
|
139
152
|
if (existing) return {
|
|
140
153
|
status: "wait",
|
|
141
154
|
promise: existing.promise
|
|
142
155
|
};
|
|
143
156
|
const deferred = Promise.withResolvers();
|
|
144
157
|
deferred.promise.catch(() => {});
|
|
145
|
-
|
|
158
|
+
partition.set(key, deferred);
|
|
159
|
+
this.pendingTotal += 1;
|
|
146
160
|
return { status: "claimed" };
|
|
147
161
|
}
|
|
148
162
|
/** Resolve an in-flight claim with the fresh result + clear it. */
|
|
149
163
|
resolvePending(key, value) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
164
|
+
const partition = this.pendingIn(false);
|
|
165
|
+
const deferred = partition?.get(key);
|
|
166
|
+
if (!partition || !deferred) return;
|
|
167
|
+
partition.delete(key);
|
|
168
|
+
this.pendingTotal -= 1;
|
|
153
169
|
deferred.resolve(value);
|
|
154
170
|
}
|
|
155
171
|
/**
|
|
@@ -158,14 +174,16 @@ var CacheEngine = class {
|
|
|
158
174
|
* whether to retry on a higher level.
|
|
159
175
|
*/
|
|
160
176
|
rejectPending(key, error) {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
177
|
+
const partition = this.pendingIn(false);
|
|
178
|
+
const deferred = partition?.get(key);
|
|
179
|
+
if (!partition || !deferred) return;
|
|
180
|
+
partition.delete(key);
|
|
181
|
+
this.pendingTotal -= 1;
|
|
164
182
|
deferred.reject(error);
|
|
165
183
|
}
|
|
166
184
|
/** Internal — number of in-flight fetches; observability hook. */
|
|
167
185
|
get pendingCount() {
|
|
168
|
-
return this.
|
|
186
|
+
return this.pendingTotal;
|
|
169
187
|
}
|
|
170
188
|
/**
|
|
171
189
|
* Invalidate every entry tagged with ANY of the provided tags. Reads
|
package/dist/cache/memo.d.mts
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
* only ever helped the caller after them. This is `CacheEngine.prefetch` over
|
|
8
8
|
* the memory adapter: concurrent misses share one load, a thrown load caches
|
|
9
9
|
* nothing, and a `null` result is an answer.
|
|
10
|
+
*
|
|
11
|
+
* `invalidate` is FENCED by a generation in the slot key: a load already in flight writes to the
|
|
12
|
+
* superseded slot, which nothing reads, so it can never repopulate the memo after an invalidation
|
|
13
|
+
* — and a `get` after the invalidation never joins that load either.
|
|
10
14
|
*/
|
|
11
15
|
interface TtlMemoOptions<K> {
|
|
12
16
|
/** Freshness window. `0` disables memoisation — every call loads. */
|
package/dist/cache/memo.mjs
CHANGED
|
@@ -9,6 +9,10 @@ import { createMemoryCacheAdapter } from "./memory-adapter.mjs";
|
|
|
9
9
|
* only ever helped the caller after them. This is `CacheEngine.prefetch` over
|
|
10
10
|
* the memory adapter: concurrent misses share one load, a thrown load caches
|
|
11
11
|
* nothing, and a `null` result is an answer.
|
|
12
|
+
*
|
|
13
|
+
* `invalidate` is FENCED by a generation in the slot key: a load already in flight writes to the
|
|
14
|
+
* superseded slot, which nothing reads, so it can never repopulate the memo after an invalidation
|
|
15
|
+
* — and a `get` after the invalidation never joins that load either.
|
|
12
16
|
*/
|
|
13
17
|
const PREFIX = "memo";
|
|
14
18
|
function createTtlMemo(load, options) {
|
|
@@ -23,6 +27,11 @@ function createTtlMemo(load, options) {
|
|
|
23
27
|
swr: false,
|
|
24
28
|
enabled: ttlMs > 0
|
|
25
29
|
};
|
|
30
|
+
const limit = maxEntries ?? 1e4;
|
|
31
|
+
/** Bumped by a full `invalidate()`; per-key generations exist only for keys invalidated since. */
|
|
32
|
+
let epoch = 0;
|
|
33
|
+
const generations = /* @__PURE__ */ new Map();
|
|
34
|
+
const slot = (base) => `${base}#${epoch}.${generations.get(base) ?? 0}`;
|
|
26
35
|
const toKey = (key) => {
|
|
27
36
|
if (keyOf) return `${PREFIX}:${keyOf(key)}`;
|
|
28
37
|
const t = typeof key;
|
|
@@ -31,15 +40,19 @@ function createTtlMemo(load, options) {
|
|
|
31
40
|
};
|
|
32
41
|
return {
|
|
33
42
|
get(key) {
|
|
34
|
-
|
|
35
|
-
return engine.prefetch(k, resolved, () => load(key));
|
|
43
|
+
return engine.prefetch(slot(toKey(key)), resolved, () => load(key));
|
|
36
44
|
},
|
|
37
45
|
async invalidate(key) {
|
|
38
|
-
if (key === void 0) {
|
|
46
|
+
if (key === void 0 || generations.size >= limit) {
|
|
47
|
+
epoch += 1;
|
|
48
|
+
generations.clear();
|
|
39
49
|
await engine.clear();
|
|
40
50
|
return;
|
|
41
51
|
}
|
|
42
|
-
|
|
52
|
+
const base = toKey(key);
|
|
53
|
+
const superseded = slot(base);
|
|
54
|
+
generations.set(base, (generations.get(base) ?? 0) + 1);
|
|
55
|
+
await adapter.delete(superseded);
|
|
43
56
|
}
|
|
44
57
|
};
|
|
45
58
|
}
|
|
@@ -62,13 +62,15 @@ function createMemoryCacheAdapter(options = {}) {
|
|
|
62
62
|
addToSet(key, members, ttlSeconds = 60) {
|
|
63
63
|
const existing = readUnexpired(key);
|
|
64
64
|
let set;
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
const expiry = ttlSeconds === 0 ? 0 : now() + ttlSeconds * 1e3;
|
|
66
|
+
if (existing && existing.value instanceof Set) {
|
|
67
|
+
set = existing.value;
|
|
68
|
+
if (existing.expiresAt !== 0 && (expiry === 0 || expiry > existing.expiresAt)) existing.expiresAt = expiry;
|
|
69
|
+
} else {
|
|
67
70
|
set = /* @__PURE__ */ new Set();
|
|
68
|
-
const expiresAt = existing?.expiresAt ?? (ttlSeconds === 0 ? 0 : now() + ttlSeconds * 1e3);
|
|
69
71
|
store.set(key, {
|
|
70
72
|
value: set,
|
|
71
|
-
expiresAt
|
|
73
|
+
expiresAt: expiry
|
|
72
74
|
});
|
|
73
75
|
evictIfNeeded();
|
|
74
76
|
}
|
|
@@ -42,7 +42,7 @@ function registerBefore(op, engine, hookCtx) {
|
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
hookCtx.log.onMiss?.(key, op);
|
|
45
|
-
if (resolved.bypass) return;
|
|
45
|
+
if (resolved.bypass || result.status === "disabled") return;
|
|
46
46
|
const claim = engine.claimPending(key);
|
|
47
47
|
if (claim.status === "wait") try {
|
|
48
48
|
const data = await claim.promise;
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
//#region src/cache/stable-stringify.d.ts
|
|
2
2
|
/**
|
|
3
|
-
* Deterministic JSON stringify — equivalent values produce identical output
|
|
3
|
+
* Deterministic JSON stringify — equivalent values produce identical output, different values
|
|
4
|
+
* never do. Every cache key and `contentHash` is built on it.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* every kit's cachePlugin uses the same keying rule — cross-kit caches
|
|
11
|
-
* remain bucket-compatible.
|
|
6
|
+
* WIRE semantics, so a value built in memory serialises like the same value after a JSON round
|
|
7
|
+
* trip: `toJSON` is applied (a `Date` is its ISO string, an ObjectId its hex), an `undefined`
|
|
8
|
+
* property is absent and an `undefined` array slot is `null`. A `RegExp` — which JSON would render
|
|
9
|
+
* `{}`, colliding every pattern — is `{"$regex","$options"}`. Keys sort by code unit, never by
|
|
10
|
+
* locale: a key that moves with the server's ICU data changes every digest. Arrays keep order.
|
|
12
11
|
*/
|
|
13
12
|
declare function stableStringify(value: unknown): string;
|
|
14
13
|
//#endregion
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
//#region src/cache/stable-stringify.ts
|
|
2
2
|
/**
|
|
3
|
-
* Deterministic JSON stringify — equivalent values produce identical output
|
|
3
|
+
* Deterministic JSON stringify — equivalent values produce identical output, different values
|
|
4
|
+
* never do. Every cache key and `contentHash` is built on it.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* every kit's cachePlugin uses the same keying rule — cross-kit caches
|
|
11
|
-
* remain bucket-compatible.
|
|
6
|
+
* WIRE semantics, so a value built in memory serialises like the same value after a JSON round
|
|
7
|
+
* trip: `toJSON` is applied (a `Date` is its ISO string, an ObjectId its hex), an `undefined`
|
|
8
|
+
* property is absent and an `undefined` array slot is `null`. A `RegExp` — which JSON would render
|
|
9
|
+
* `{}`, colliding every pattern — is `{"$regex","$options"}`. Keys sort by code unit, never by
|
|
10
|
+
* locale: a key that moves with the server's ICU data changes every digest. Arrays keep order.
|
|
12
11
|
*/
|
|
13
12
|
function stableStringify(value) {
|
|
14
|
-
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
15
|
-
if (
|
|
16
|
-
|
|
13
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
14
|
+
if (value instanceof RegExp) return `{"$options":${JSON.stringify(value.flags)},"$regex":${JSON.stringify(value.source)}}`;
|
|
15
|
+
const toJSON = value.toJSON;
|
|
16
|
+
if (typeof toJSON === "function") return stableStringify(toJSON.call(value));
|
|
17
|
+
if (Array.isArray(value)) return `[${value.map((v) => v === void 0 ? "null" : stableStringify(v)).join(",")}]`;
|
|
18
|
+
const record = value;
|
|
19
|
+
return `{${Object.keys(record).filter((k) => record[k] !== void 0).sort((a, b) => a < b ? -1 : a > b ? 1 : 0).map((k) => `${JSON.stringify(k)}:${stableStringify(record[k])}`).join(",")}}`;
|
|
17
20
|
}
|
|
18
21
|
//#endregion
|
|
19
22
|
export { stableStringify };
|
package/dist/cache/tag-index.mjs
CHANGED
|
@@ -52,8 +52,7 @@ async function appendKeyToTags(adapter, prefix, cacheKey, tags, ttlSeconds) {
|
|
|
52
52
|
const idxKey = tagIndexKey(prefix, tag);
|
|
53
53
|
const existing = await adapter.get(idxKey);
|
|
54
54
|
const current = Array.isArray(existing) ? existing : [];
|
|
55
|
-
|
|
56
|
-
const next = [...current, cacheKey];
|
|
55
|
+
const next = current.includes(cacheKey) ? current : [...current, cacheKey];
|
|
57
56
|
await adapter.set(idxKey, next, indexTtl);
|
|
58
57
|
}));
|
|
59
58
|
}
|
package/dist/cache/types.d.mts
CHANGED
|
@@ -66,13 +66,14 @@ interface CacheAdapter {
|
|
|
66
66
|
* difference is the 178× slowdown the in-memory benchmark surfaced.
|
|
67
67
|
*
|
|
68
68
|
* **Implementation guidance:**
|
|
69
|
-
* - Redis: `SADD key m1 m2 ...` + `EXPIRE key ttlSeconds NX`
|
|
69
|
+
* - Redis: `SADD key m1 m2 ...` + `EXPIRE key ttlSeconds GT` (Redis ≥ 7; `NX` is wrong, see below)
|
|
70
70
|
* - Memory: in-place push on the underlying array (no copy)
|
|
71
71
|
* - DynamoDB: `UpdateItem` with `ADD` action on a String Set
|
|
72
72
|
* - Cloudflare KV / pure GET-SET stores: omit; engine falls back.
|
|
73
73
|
*
|
|
74
|
-
* `ttlSeconds`
|
|
75
|
-
*
|
|
74
|
+
* `ttlSeconds` EXTENDS the set's expiry and never shortens it (Redis `GT` semantics). A set used
|
|
75
|
+
* as a tag index must outlive its newest member; keeping the creation-time expiry lets the index
|
|
76
|
+
* vanish while members live on, and invalidating the tag then misses them.
|
|
76
77
|
*/
|
|
77
78
|
addToSet?(key: string, members: readonly string[], ttlSeconds?: number): Promise<number> | number;
|
|
78
79
|
/**
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
//#region src/errors/codes.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The error-code vocabulary of the wire — every value a client may see in
|
|
4
|
+
* `ErrorContract.code`, in one place.
|
|
5
|
+
*
|
|
6
|
+
* Codes live here, in the contract package, rather than in the servers that
|
|
7
|
+
* emit them: a client SDK consumes the wire vocabulary by depending on
|
|
8
|
+
* repo-core, never on the server framework. The emitting side proves it only
|
|
9
|
+
* uses catalogued codes (arc's test suite fails on an uncatalogued `arc.*`
|
|
10
|
+
* literal); the consuming side spreads these constants instead of copying
|
|
11
|
+
* strings, so an addition reaches every client with a repo-core bump.
|
|
12
|
+
*
|
|
13
|
+
* Three families:
|
|
14
|
+
*
|
|
15
|
+
* - {@link ERROR_CODES} — cross-cutting, lowercase snake_case (RFC 7807 /
|
|
16
|
+
* Stripe style). What {@link statusToErrorCode} derives and any
|
|
17
|
+
* HTTP-emitting package may use.
|
|
18
|
+
* - {@link ARC_ERROR_CODES} — arc's `arc.`-namespaced codes: the
|
|
19
|
+
* status-derived family, request validation, auth, tenancy, idempotency,
|
|
20
|
+
* capacity, and per-feature codes. Values are wire-stable — never renamed.
|
|
21
|
+
* - {@link ARC_REASON_CODES} — UPPER_SNAKE business reasons arc's mixins put
|
|
22
|
+
* on `code` alongside the HTTP status (why a bulk write was refused, …).
|
|
23
|
+
*
|
|
24
|
+
* `ErrorContract.code` stays `string`: domain packages extend hierarchically
|
|
25
|
+
* (`'order.validation.missing_line'`) without registering here.
|
|
26
|
+
*/
|
|
27
|
+
declare const ERROR_CODES: {
|
|
28
|
+
readonly VALIDATION: 'validation_error';
|
|
29
|
+
readonly NOT_FOUND: 'not_found';
|
|
30
|
+
readonly CONFLICT: 'conflict';
|
|
31
|
+
readonly UNAUTHORIZED: 'unauthorized';
|
|
32
|
+
readonly FORBIDDEN: 'forbidden';
|
|
33
|
+
readonly RATE_LIMITED: 'rate_limited';
|
|
34
|
+
readonly IDEMPOTENCY_CONFLICT: 'idempotency_conflict';
|
|
35
|
+
readonly PRECONDITION_FAILED: 'precondition_failed';
|
|
36
|
+
readonly INTERNAL: 'internal_error';
|
|
37
|
+
readonly UNAVAILABLE: 'service_unavailable';
|
|
38
|
+
readonly TIMEOUT: 'timeout';
|
|
39
|
+
};
|
|
40
|
+
type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
|
41
|
+
declare const ARC_ERROR_CODES: {
|
|
42
|
+
readonly ERROR: 'arc.error';
|
|
43
|
+
readonly BAD_REQUEST: 'arc.bad_request';
|
|
44
|
+
readonly UNAUTHORIZED: 'arc.unauthorized';
|
|
45
|
+
readonly FORBIDDEN: 'arc.forbidden';
|
|
46
|
+
readonly NOT_FOUND: 'arc.not_found';
|
|
47
|
+
readonly CONFLICT: 'arc.conflict';
|
|
48
|
+
readonly PAYLOAD_TOO_LARGE: 'arc.payload_too_large';
|
|
49
|
+
readonly UNSUPPORTED_MEDIA_TYPE: 'arc.unsupported_media_type';
|
|
50
|
+
readonly UNPROCESSABLE_ENTITY: 'arc.unprocessable_entity';
|
|
51
|
+
readonly RATE_LIMITED: 'arc.rate_limited';
|
|
52
|
+
readonly INTERNAL_ERROR: 'arc.internal_error';
|
|
53
|
+
readonly NOT_IMPLEMENTED: 'arc.not_implemented';
|
|
54
|
+
readonly BAD_GATEWAY: 'arc.bad_gateway';
|
|
55
|
+
readonly SERVICE_UNAVAILABLE: 'arc.service_unavailable';
|
|
56
|
+
readonly GATEWAY_TIMEOUT: 'arc.gateway_timeout';
|
|
57
|
+
readonly VALIDATION_ERROR: 'arc.validation_error';
|
|
58
|
+
readonly INVALID_ID: 'arc.invalid_id';
|
|
59
|
+
readonly INVALID_ACTION: 'arc.invalid_action';
|
|
60
|
+
readonly UNSUPPORTED_API_VERSION: 'arc.unsupported_api_version';
|
|
61
|
+
/** A stale `If-Match` — re-read the document (its `ETag`) and retry. HTTP 412. */
|
|
62
|
+
readonly PRECONDITION_FAILED: 'arc.precondition_failed';
|
|
63
|
+
readonly AUTH_INVALID_TOKEN_TYPE: 'arc.auth.invalid_token_type';
|
|
64
|
+
readonly AUTH_TOKEN_REVOKED: 'arc.auth.token_revoked';
|
|
65
|
+
readonly AUTH_REVOCATION_CHECK_FAILED: 'arc.auth.revocation_check_failed';
|
|
66
|
+
readonly AUTH_MISCONFIGURED: 'arc.auth.misconfigured';
|
|
67
|
+
/** HTTP 500 — the auth provider itself failed (not a bad credential). */
|
|
68
|
+
readonly AUTH_SERVICE_ERROR: 'arc.auth.service_error';
|
|
69
|
+
/** HTTP 401 — `x-arc-scope: platform` without an authenticated user. */
|
|
70
|
+
readonly ELEVATION_AUTH_REQUIRED: 'arc.elevation.auth_required';
|
|
71
|
+
/** HTTP 403 — `x-arc-scope: platform` from a user without a platform role. */
|
|
72
|
+
readonly ELEVATION_FORBIDDEN: 'arc.elevation.forbidden';
|
|
73
|
+
readonly ORG_SELECTION_REQUIRED: 'arc.org.selection_required';
|
|
74
|
+
readonly ORG_ACCESS_DENIED: 'arc.org.access_denied';
|
|
75
|
+
/** Convention for host tier gates; arc surfaces it verbatim with `{ requiredMode, currentMode }`. */
|
|
76
|
+
readonly TIER_REQUIRED: 'arc.tier_required';
|
|
77
|
+
/** HTTP 400 — the route requires an `Idempotency-Key`. */
|
|
78
|
+
readonly IDEMPOTENCY_KEY_REQUIRED: 'arc.idempotency_key_required';
|
|
79
|
+
/** HTTP 422 — the key already named an operation with a different body. */
|
|
80
|
+
readonly IDEMPOTENCY_KEY_REUSED: 'arc.idempotency_key_reused';
|
|
81
|
+
/** HTTP 400 — the body could not be fingerprinted (too deep). */
|
|
82
|
+
readonly IDEMPOTENCY_BODY_UNFINGERPRINTABLE: 'arc.idempotency_body_unfingerprintable';
|
|
83
|
+
/** HTTP 409 — the same key is still executing; retry after `Retry-After`. */
|
|
84
|
+
readonly IDEMPOTENCY_CONFLICT: 'arc.idempotency_conflict';
|
|
85
|
+
readonly CIRCUIT_OPEN: 'arc.circuit_open';
|
|
86
|
+
readonly UNAVAILABLE: 'arc.unavailable';
|
|
87
|
+
/** HTTP 500 — a host write verb broke its contract (e.g. `create` returned nothing). */
|
|
88
|
+
readonly WRITE_VERB_CONTRACT_VIOLATION: 'arc.write_verb.contract_violation';
|
|
89
|
+
readonly ADAPTER_CAPABILITY_REQUIRED: 'arc.adapter.capability_required';
|
|
90
|
+
readonly NO_ADAPTER: 'arc.no_adapter';
|
|
91
|
+
readonly AGGREGATION_MAX_GROUPS_EXCEEDED: 'arc.aggregation.max_groups_exceeded';
|
|
92
|
+
readonly AGGREGATION_REQUIRED_FILTER_MISSING: 'arc.aggregation.required_filter_missing';
|
|
93
|
+
readonly AGGREGATION_REQUIRED_DATE_RANGE_MISSING: 'arc.aggregation.required_date_range_missing';
|
|
94
|
+
readonly AGGREGATION_DATE_RANGE_EXCEEDED: 'arc.aggregation.date_range_exceeded';
|
|
95
|
+
readonly EVENT_VALIDATION_ERROR: 'arc.event.validation_error';
|
|
96
|
+
readonly REALTIME_UNFILTERABLE: 'arc.realtime.unfilterable';
|
|
97
|
+
readonly JOBS_POLICY_UNSUPPORTED: 'arc.jobs.policy_unsupported';
|
|
98
|
+
readonly STREAMLINE_INVALID_BODY: 'arc.streamline.invalid_body';
|
|
99
|
+
readonly STREAMLINE_MISSING_INPUT_ENVELOPE: 'arc.streamline.missing_input_envelope';
|
|
100
|
+
readonly STREAMLINE_UNKNOWN_ENVELOPE_KEYS: 'arc.streamline.unknown_envelope_keys';
|
|
101
|
+
readonly PURGE_CHUNKED_REQUIRED: 'arc.purge.chunked_required';
|
|
102
|
+
readonly PURGE_UNSUPPORTED_STRATEGY: 'arc.purge.unsupported_strategy';
|
|
103
|
+
readonly PURGE_NO_BULK_OP: 'arc.purge.no_bulk_op';
|
|
104
|
+
readonly MCP_TOOL_NAME_COLLISION: 'arc.mcp.tool_name_collision';
|
|
105
|
+
};
|
|
106
|
+
type ArcErrorCode = (typeof ARC_ERROR_CODES)[keyof typeof ARC_ERROR_CODES];
|
|
107
|
+
declare const ARC_REASON_CODES: {
|
|
108
|
+
readonly ORG_CONTEXT_REQUIRED: 'ORG_CONTEXT_REQUIRED';
|
|
109
|
+
readonly MIXED_UPDATE_SHAPE: 'MIXED_UPDATE_SHAPE';
|
|
110
|
+
readonly ALL_FIELDS_STRIPPED: 'ALL_FIELDS_STRIPPED';
|
|
111
|
+
readonly BEFORE_RESTORE_HOOK_ERROR: 'BEFORE_RESTORE_HOOK_ERROR';
|
|
112
|
+
};
|
|
113
|
+
type ArcReasonCode = (typeof ARC_REASON_CODES)[keyof typeof ARC_REASON_CODES];
|
|
114
|
+
/** `details[].code` for a unique-constraint violation (see `toErrorContract`). */
|
|
115
|
+
declare const DUPLICATE_KEY_DETAIL_CODE = "duplicate_key";
|
|
116
|
+
//#endregion
|
|
117
|
+
export { ARC_ERROR_CODES, ARC_REASON_CODES, ArcErrorCode, ArcReasonCode, DUPLICATE_KEY_DETAIL_CODE, ERROR_CODES, ErrorCode };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
//#region src/errors/codes.ts
|
|
2
|
+
/**
|
|
3
|
+
* The error-code vocabulary of the wire — every value a client may see in
|
|
4
|
+
* `ErrorContract.code`, in one place.
|
|
5
|
+
*
|
|
6
|
+
* Codes live here, in the contract package, rather than in the servers that
|
|
7
|
+
* emit them: a client SDK consumes the wire vocabulary by depending on
|
|
8
|
+
* repo-core, never on the server framework. The emitting side proves it only
|
|
9
|
+
* uses catalogued codes (arc's test suite fails on an uncatalogued `arc.*`
|
|
10
|
+
* literal); the consuming side spreads these constants instead of copying
|
|
11
|
+
* strings, so an addition reaches every client with a repo-core bump.
|
|
12
|
+
*
|
|
13
|
+
* Three families:
|
|
14
|
+
*
|
|
15
|
+
* - {@link ERROR_CODES} — cross-cutting, lowercase snake_case (RFC 7807 /
|
|
16
|
+
* Stripe style). What {@link statusToErrorCode} derives and any
|
|
17
|
+
* HTTP-emitting package may use.
|
|
18
|
+
* - {@link ARC_ERROR_CODES} — arc's `arc.`-namespaced codes: the
|
|
19
|
+
* status-derived family, request validation, auth, tenancy, idempotency,
|
|
20
|
+
* capacity, and per-feature codes. Values are wire-stable — never renamed.
|
|
21
|
+
* - {@link ARC_REASON_CODES} — UPPER_SNAKE business reasons arc's mixins put
|
|
22
|
+
* on `code` alongside the HTTP status (why a bulk write was refused, …).
|
|
23
|
+
*
|
|
24
|
+
* `ErrorContract.code` stays `string`: domain packages extend hierarchically
|
|
25
|
+
* (`'order.validation.missing_line'`) without registering here.
|
|
26
|
+
*/
|
|
27
|
+
const ERROR_CODES = {
|
|
28
|
+
VALIDATION: "validation_error",
|
|
29
|
+
NOT_FOUND: "not_found",
|
|
30
|
+
CONFLICT: "conflict",
|
|
31
|
+
UNAUTHORIZED: "unauthorized",
|
|
32
|
+
FORBIDDEN: "forbidden",
|
|
33
|
+
RATE_LIMITED: "rate_limited",
|
|
34
|
+
IDEMPOTENCY_CONFLICT: "idempotency_conflict",
|
|
35
|
+
PRECONDITION_FAILED: "precondition_failed",
|
|
36
|
+
INTERNAL: "internal_error",
|
|
37
|
+
UNAVAILABLE: "service_unavailable",
|
|
38
|
+
TIMEOUT: "timeout"
|
|
39
|
+
};
|
|
40
|
+
const ARC_ERROR_CODES = {
|
|
41
|
+
ERROR: "arc.error",
|
|
42
|
+
BAD_REQUEST: "arc.bad_request",
|
|
43
|
+
UNAUTHORIZED: "arc.unauthorized",
|
|
44
|
+
FORBIDDEN: "arc.forbidden",
|
|
45
|
+
NOT_FOUND: "arc.not_found",
|
|
46
|
+
CONFLICT: "arc.conflict",
|
|
47
|
+
PAYLOAD_TOO_LARGE: "arc.payload_too_large",
|
|
48
|
+
UNSUPPORTED_MEDIA_TYPE: "arc.unsupported_media_type",
|
|
49
|
+
UNPROCESSABLE_ENTITY: "arc.unprocessable_entity",
|
|
50
|
+
RATE_LIMITED: "arc.rate_limited",
|
|
51
|
+
INTERNAL_ERROR: "arc.internal_error",
|
|
52
|
+
NOT_IMPLEMENTED: "arc.not_implemented",
|
|
53
|
+
BAD_GATEWAY: "arc.bad_gateway",
|
|
54
|
+
SERVICE_UNAVAILABLE: "arc.service_unavailable",
|
|
55
|
+
GATEWAY_TIMEOUT: "arc.gateway_timeout",
|
|
56
|
+
VALIDATION_ERROR: "arc.validation_error",
|
|
57
|
+
INVALID_ID: "arc.invalid_id",
|
|
58
|
+
INVALID_ACTION: "arc.invalid_action",
|
|
59
|
+
UNSUPPORTED_API_VERSION: "arc.unsupported_api_version",
|
|
60
|
+
/** A stale `If-Match` — re-read the document (its `ETag`) and retry. HTTP 412. */
|
|
61
|
+
PRECONDITION_FAILED: "arc.precondition_failed",
|
|
62
|
+
AUTH_INVALID_TOKEN_TYPE: "arc.auth.invalid_token_type",
|
|
63
|
+
AUTH_TOKEN_REVOKED: "arc.auth.token_revoked",
|
|
64
|
+
AUTH_REVOCATION_CHECK_FAILED: "arc.auth.revocation_check_failed",
|
|
65
|
+
AUTH_MISCONFIGURED: "arc.auth.misconfigured",
|
|
66
|
+
/** HTTP 500 — the auth provider itself failed (not a bad credential). */
|
|
67
|
+
AUTH_SERVICE_ERROR: "arc.auth.service_error",
|
|
68
|
+
/** HTTP 401 — `x-arc-scope: platform` without an authenticated user. */
|
|
69
|
+
ELEVATION_AUTH_REQUIRED: "arc.elevation.auth_required",
|
|
70
|
+
/** HTTP 403 — `x-arc-scope: platform` from a user without a platform role. */
|
|
71
|
+
ELEVATION_FORBIDDEN: "arc.elevation.forbidden",
|
|
72
|
+
ORG_SELECTION_REQUIRED: "arc.org.selection_required",
|
|
73
|
+
ORG_ACCESS_DENIED: "arc.org.access_denied",
|
|
74
|
+
/** Convention for host tier gates; arc surfaces it verbatim with `{ requiredMode, currentMode }`. */
|
|
75
|
+
TIER_REQUIRED: "arc.tier_required",
|
|
76
|
+
/** HTTP 400 — the route requires an `Idempotency-Key`. */
|
|
77
|
+
IDEMPOTENCY_KEY_REQUIRED: "arc.idempotency_key_required",
|
|
78
|
+
/** HTTP 422 — the key already named an operation with a different body. */
|
|
79
|
+
IDEMPOTENCY_KEY_REUSED: "arc.idempotency_key_reused",
|
|
80
|
+
/** HTTP 400 — the body could not be fingerprinted (too deep). */
|
|
81
|
+
IDEMPOTENCY_BODY_UNFINGERPRINTABLE: "arc.idempotency_body_unfingerprintable",
|
|
82
|
+
/** HTTP 409 — the same key is still executing; retry after `Retry-After`. */
|
|
83
|
+
IDEMPOTENCY_CONFLICT: "arc.idempotency_conflict",
|
|
84
|
+
CIRCUIT_OPEN: "arc.circuit_open",
|
|
85
|
+
UNAVAILABLE: "arc.unavailable",
|
|
86
|
+
/** HTTP 500 — a host write verb broke its contract (e.g. `create` returned nothing). */
|
|
87
|
+
WRITE_VERB_CONTRACT_VIOLATION: "arc.write_verb.contract_violation",
|
|
88
|
+
ADAPTER_CAPABILITY_REQUIRED: "arc.adapter.capability_required",
|
|
89
|
+
NO_ADAPTER: "arc.no_adapter",
|
|
90
|
+
AGGREGATION_MAX_GROUPS_EXCEEDED: "arc.aggregation.max_groups_exceeded",
|
|
91
|
+
AGGREGATION_REQUIRED_FILTER_MISSING: "arc.aggregation.required_filter_missing",
|
|
92
|
+
AGGREGATION_REQUIRED_DATE_RANGE_MISSING: "arc.aggregation.required_date_range_missing",
|
|
93
|
+
AGGREGATION_DATE_RANGE_EXCEEDED: "arc.aggregation.date_range_exceeded",
|
|
94
|
+
EVENT_VALIDATION_ERROR: "arc.event.validation_error",
|
|
95
|
+
REALTIME_UNFILTERABLE: "arc.realtime.unfilterable",
|
|
96
|
+
JOBS_POLICY_UNSUPPORTED: "arc.jobs.policy_unsupported",
|
|
97
|
+
STREAMLINE_INVALID_BODY: "arc.streamline.invalid_body",
|
|
98
|
+
STREAMLINE_MISSING_INPUT_ENVELOPE: "arc.streamline.missing_input_envelope",
|
|
99
|
+
STREAMLINE_UNKNOWN_ENVELOPE_KEYS: "arc.streamline.unknown_envelope_keys",
|
|
100
|
+
PURGE_CHUNKED_REQUIRED: "arc.purge.chunked_required",
|
|
101
|
+
PURGE_UNSUPPORTED_STRATEGY: "arc.purge.unsupported_strategy",
|
|
102
|
+
PURGE_NO_BULK_OP: "arc.purge.no_bulk_op",
|
|
103
|
+
MCP_TOOL_NAME_COLLISION: "arc.mcp.tool_name_collision"
|
|
104
|
+
};
|
|
105
|
+
const ARC_REASON_CODES = {
|
|
106
|
+
ORG_CONTEXT_REQUIRED: "ORG_CONTEXT_REQUIRED",
|
|
107
|
+
MIXED_UPDATE_SHAPE: "MIXED_UPDATE_SHAPE",
|
|
108
|
+
ALL_FIELDS_STRIPPED: "ALL_FIELDS_STRIPPED",
|
|
109
|
+
BEFORE_RESTORE_HOOK_ERROR: "BEFORE_RESTORE_HOOK_ERROR"
|
|
110
|
+
};
|
|
111
|
+
/** `details[].code` for a unique-constraint violation (see `toErrorContract`). */
|
|
112
|
+
const DUPLICATE_KEY_DETAIL_CODE = "duplicate_key";
|
|
113
|
+
//#endregion
|
|
114
|
+
export { ARC_ERROR_CODES, ARC_REASON_CODES, DUPLICATE_KEY_DETAIL_CODE, ERROR_CODES };
|
package/dist/errors/contract.mjs
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { ERROR_CODES } from "./
|
|
1
|
+
import { DUPLICATE_KEY_DETAIL_CODE, ERROR_CODES } from "./codes.mjs";
|
|
2
2
|
//#region src/errors/contract.ts
|
|
3
3
|
/**
|
|
4
|
+
* Wire-contract helpers — convert throwable {@link HttpError} into the
|
|
5
|
+
* serializable {@link ErrorContract} shape, with a `code` cascade so any
|
|
6
|
+
* thrown error produces a sensible wire envelope even when it's a plain
|
|
7
|
+
* `Error` from third-party code.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
4
10
|
* Map an HTTP status code to the canonical {@link ErrorCode}. Used as a
|
|
5
11
|
* fallback when a thrown `HttpError` lacks an explicit `code` field.
|
|
6
12
|
*
|
|
@@ -47,7 +53,7 @@ function statusToErrorCode(status) {
|
|
|
47
53
|
*/
|
|
48
54
|
function toErrorContract(error) {
|
|
49
55
|
if (!(error instanceof Error)) return {
|
|
50
|
-
code:
|
|
56
|
+
code: ERROR_CODES.INTERNAL,
|
|
51
57
|
message: typeof error === "string" ? error : "Internal error",
|
|
52
58
|
status: 500
|
|
53
59
|
};
|
|
@@ -67,7 +73,7 @@ function toErrorContract(error) {
|
|
|
67
73
|
});
|
|
68
74
|
if (e.duplicate?.fields?.length) for (const field of e.duplicate.fields) details.push({
|
|
69
75
|
path: field,
|
|
70
|
-
code:
|
|
76
|
+
code: DUPLICATE_KEY_DETAIL_CODE,
|
|
71
77
|
message: `Duplicate value for "${field}"`
|
|
72
78
|
});
|
|
73
79
|
if (details.length > 0) contract.details = details;
|
package/dist/errors/index.d.mts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { IsTransientConflictFn, VersionConflictError, conservativeMongoIsTransientConflict, isVersionConflictError, neverTransient } from "./conflict.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { ARC_ERROR_CODES, ARC_REASON_CODES, ArcErrorCode, ArcReasonCode, DUPLICATE_KEY_DETAIL_CODE, ERROR_CODES, ErrorCode } from "./codes.mjs";
|
|
3
|
+
import { DuplicateKeyMeta, ErrorContract, ErrorDetail, HttpError, ValidationErrorMeta } from "./types.mjs";
|
|
3
4
|
import { statusToErrorCode, toErrorContract } from "./contract.mjs";
|
|
4
5
|
import { createError, isHttpError } from "./create-error.mjs";
|
|
5
6
|
import { IsDuplicateKeyErrorFn, ToDuplicateKeyHttpErrorOptions, conservativeMongoIsDuplicateKey, toDuplicateKeyHttpError } from "./duplicate-key.mjs";
|
|
6
7
|
import { errorContractSchema, errorDetailSchema } from "./schema.mjs";
|
|
7
|
-
export { type DuplicateKeyMeta, ERROR_CODES, type ErrorCode, type ErrorContract, type ErrorDetail, type HttpError, type IsDuplicateKeyErrorFn, type IsTransientConflictFn, type ToDuplicateKeyHttpErrorOptions, type ValidationErrorMeta, VersionConflictError, conservativeMongoIsDuplicateKey, conservativeMongoIsTransientConflict, createError, errorContractSchema, errorDetailSchema, isHttpError, isVersionConflictError, neverTransient, statusToErrorCode, toDuplicateKeyHttpError, toErrorContract };
|
|
8
|
+
export { ARC_ERROR_CODES, ARC_REASON_CODES, type ArcErrorCode, type ArcReasonCode, DUPLICATE_KEY_DETAIL_CODE, type DuplicateKeyMeta, ERROR_CODES, type ErrorCode, type ErrorContract, type ErrorDetail, type HttpError, type IsDuplicateKeyErrorFn, type IsTransientConflictFn, type ToDuplicateKeyHttpErrorOptions, type ValidationErrorMeta, VersionConflictError, conservativeMongoIsDuplicateKey, conservativeMongoIsTransientConflict, createError, errorContractSchema, errorDetailSchema, isHttpError, isVersionConflictError, neverTransient, statusToErrorCode, toDuplicateKeyHttpError, toErrorContract };
|
package/dist/errors/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { ARC_ERROR_CODES, ARC_REASON_CODES, DUPLICATE_KEY_DETAIL_CODE, ERROR_CODES } from "./codes.mjs";
|
|
1
2
|
import { VersionConflictError, conservativeMongoIsTransientConflict, isVersionConflictError, neverTransient } from "./conflict.mjs";
|
|
2
|
-
import { ERROR_CODES } from "./types.mjs";
|
|
3
3
|
import { statusToErrorCode, toErrorContract } from "./contract.mjs";
|
|
4
4
|
import { createError, isHttpError } from "./create-error.mjs";
|
|
5
5
|
import { conservativeMongoIsDuplicateKey, toDuplicateKeyHttpError } from "./duplicate-key.mjs";
|
|
6
6
|
import { errorContractSchema, errorDetailSchema } from "./schema.mjs";
|
|
7
|
-
export { ERROR_CODES, VersionConflictError, conservativeMongoIsDuplicateKey, conservativeMongoIsTransientConflict, createError, errorContractSchema, errorDetailSchema, isHttpError, isVersionConflictError, neverTransient, statusToErrorCode, toDuplicateKeyHttpError, toErrorContract };
|
|
7
|
+
export { ARC_ERROR_CODES, ARC_REASON_CODES, DUPLICATE_KEY_DETAIL_CODE, ERROR_CODES, VersionConflictError, conservativeMongoIsDuplicateKey, conservativeMongoIsTransientConflict, createError, errorContractSchema, errorDetailSchema, isHttpError, isVersionConflictError, neverTransient, statusToErrorCode, toDuplicateKeyHttpError, toErrorContract };
|
package/dist/errors/types.d.mts
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
* `implements HttpError` and serialize to `ErrorContract` for the wire.
|
|
19
19
|
* One contract, one canonical home, every package follows the same shape.
|
|
20
20
|
*
|
|
21
|
-
* **Custom-domain escape hatch.**
|
|
22
|
-
*
|
|
21
|
+
* **Custom-domain escape hatch.** The catalogued codes live in `codes.ts`;
|
|
22
|
+
* `code: string` accepts ANY string so domain packages
|
|
23
23
|
* can extend (`'order.validation.missing_line'`, `'payment.gateway.timeout'`).
|
|
24
24
|
* The canonical codes cover cross-cutting concerns; domain extensions
|
|
25
25
|
* hierarchically narrow.
|
|
@@ -124,32 +124,5 @@ interface ErrorDetail {
|
|
|
124
124
|
message: string;
|
|
125
125
|
meta?: Readonly<Record<string, unknown>>;
|
|
126
126
|
}
|
|
127
|
-
/**
|
|
128
|
-
* Cross-cutting error codes used across the org. Every canonical code is
|
|
129
|
-
* lowercase + snake_case to match RFC 7807, Stripe, and Shopify
|
|
130
|
-
* conventions. Domain packages add their own hierarchical codes
|
|
131
|
-
* (`'order.validation.*'`, `'payment.gateway.*'`); these cover the
|
|
132
|
-
* universal cases every HTTP-emitting layer needs.
|
|
133
|
-
*
|
|
134
|
-
* **Arc compatibility note.** Arc's `ArcError` hierarchy historically
|
|
135
|
-
* uses UPPER_SNAKE codes (`'NOT_FOUND'`, `'VALIDATION_ERROR'`) for
|
|
136
|
-
* back-compat with hosts that switch on those values. New code should
|
|
137
|
-
* prefer the canonical lowercase codes; arc keeps emitting its UPPER_SNAKE
|
|
138
|
-
* codes on the wire so existing client switches keep working.
|
|
139
|
-
*/
|
|
140
|
-
declare const ERROR_CODES: {
|
|
141
|
-
readonly VALIDATION: 'validation_error';
|
|
142
|
-
readonly NOT_FOUND: 'not_found';
|
|
143
|
-
readonly CONFLICT: 'conflict';
|
|
144
|
-
readonly UNAUTHORIZED: 'unauthorized';
|
|
145
|
-
readonly FORBIDDEN: 'forbidden';
|
|
146
|
-
readonly RATE_LIMITED: 'rate_limited';
|
|
147
|
-
readonly IDEMPOTENCY_CONFLICT: 'idempotency_conflict';
|
|
148
|
-
readonly PRECONDITION_FAILED: 'precondition_failed';
|
|
149
|
-
readonly INTERNAL: 'internal_error';
|
|
150
|
-
readonly UNAVAILABLE: 'service_unavailable';
|
|
151
|
-
readonly TIMEOUT: 'timeout';
|
|
152
|
-
};
|
|
153
|
-
type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
|
154
127
|
//#endregion
|
|
155
|
-
export { DuplicateKeyMeta,
|
|
128
|
+
export { DuplicateKeyMeta, ErrorContract, ErrorDetail, HttpError, ValidationErrorMeta };
|