@voltro/protocol 0.51.0 → 0.53.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 +356 -0
- package/dist/index.d.ts +245 -4
- package/dist/index.js +544 -441
- package/dist/rest.d.ts +99 -3
- package/dist/rest.js +81 -36
- package/package.json +3 -3
package/dist/rest.d.ts
CHANGED
|
@@ -74,6 +74,12 @@ declare interface AnyApprovalPolicy {
|
|
|
74
74
|
/** A guard is either a scope check or a relationship check. */
|
|
75
75
|
declare type AnyGuardSpec<Input = unknown> = GuardSpec<Input> | PolicyGuardSpec<Input>;
|
|
76
76
|
|
|
77
|
+
export declare const bytes: (stream: ReadableStream<Uint8Array> | (() => ReadableStream<Uint8Array>), options?: {
|
|
78
|
+
readonly contentType?: string;
|
|
79
|
+
readonly contentLength?: number;
|
|
80
|
+
readonly contentDisposition?: string;
|
|
81
|
+
}) => RestByteResponse;
|
|
82
|
+
|
|
77
83
|
/**
|
|
78
84
|
* Project every descriptor carrying a `publicApi` annotation into a REST
|
|
79
85
|
* route, in stable (path) order. The boot layer feeds these straight into
|
|
@@ -224,6 +230,8 @@ declare interface IdempotencyStore {
|
|
|
224
230
|
declare interface InsertTarget<Input = unknown, Row = unknown, Item = Record<string, unknown>> extends NestedTargetFields<Input> {
|
|
225
231
|
readonly table: string;
|
|
226
232
|
readonly op: 'insert';
|
|
233
|
+
/** Declared junction relations — see {@link TargetRelations}. */
|
|
234
|
+
readonly relations?: TargetRelations | undefined;
|
|
227
235
|
readonly order?: 'prepend' | 'append' | undefined;
|
|
228
236
|
/**
|
|
229
237
|
* Build the optimistic row from the mutation input. The framework
|
|
@@ -248,6 +256,8 @@ declare interface InsertTarget<Input = unknown, Row = unknown, Item = Record<str
|
|
|
248
256
|
readonly shapeItem?: ((input: Input, optimisticId: string) => Item) | undefined;
|
|
249
257
|
}
|
|
250
258
|
|
|
259
|
+
export declare const isRestByteResponse: (v: unknown) => v is RestByteResponse;
|
|
260
|
+
|
|
251
261
|
export declare const isRestStreamResponse: (v: unknown) => v is RestStreamResponse;
|
|
252
262
|
|
|
253
263
|
/** Extract `:name` path params by aligning the route PATTERN with the request
|
|
@@ -349,12 +359,19 @@ declare interface OpenAccessSpec {
|
|
|
349
359
|
|
|
350
360
|
/** A public raw-HTTP route a plugin serves on the framework listener. */
|
|
351
361
|
declare interface PluginHttpRoute {
|
|
352
|
-
/** HTTP method, or `'*'` for any (the handler decides).
|
|
353
|
-
|
|
362
|
+
/** HTTP method, or `'*'` for any (the handler decides). PATCH/HEAD/OPTIONS
|
|
363
|
+
* are first-class — the REST desugar used to mount `'*'` partly BECAUSE
|
|
364
|
+
* this union lacked PATCH; that reason is gone (the `'*'` mount remains
|
|
365
|
+
* for its other job: one dispatcher per shared path + a precise 405). */
|
|
366
|
+
readonly method: '*' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
354
367
|
/** Absolute path prefix, e.g. `/_voltro/storage`. Matches the path AND
|
|
355
368
|
* any sub-path (`/_voltro/storage/abc123`). */
|
|
356
369
|
readonly path: string;
|
|
357
370
|
readonly handle: (req: PluginHttpRouteRequest) => Promise<PluginHttpRouteResult>;
|
|
371
|
+
/** Per-route body cap override (bytes) — wins over the listener's shared
|
|
372
|
+
* `maxBodyBytes`. NOTE: routes SHARING a path share one body read, so the
|
|
373
|
+
* widest override on the path's group applies to the whole group. */
|
|
374
|
+
readonly maxBodyBytes?: number;
|
|
358
375
|
/**
|
|
359
376
|
* Opt this route's path OUT of the listener's cross-site origin check.
|
|
360
377
|
*
|
|
@@ -393,6 +410,21 @@ declare interface PluginHttpRoute {
|
|
|
393
410
|
readonly originGuard?: 'exempt';
|
|
394
411
|
}
|
|
395
412
|
|
|
413
|
+
/**
|
|
414
|
+
* A binary streaming body — the download/export shape. The serve layer pipes
|
|
415
|
+
* the Web ReadableStream to the socket without buffering, so a response
|
|
416
|
+
* larger than the heap is fine; the LAZY thunk form defers opening the
|
|
417
|
+
* source (a provider connection, a file handle) until the response actually
|
|
418
|
+
* streams.
|
|
419
|
+
*/
|
|
420
|
+
declare interface PluginHttpRouteByteStream {
|
|
421
|
+
readonly stream: ReadableStream<Uint8Array> | (() => ReadableStream<Uint8Array>);
|
|
422
|
+
/** Declared up front when known — lets the client render progress. */
|
|
423
|
+
readonly contentLength?: number;
|
|
424
|
+
/** e.g. `attachment; filename="export.zip"`. */
|
|
425
|
+
readonly contentDisposition?: string;
|
|
426
|
+
}
|
|
427
|
+
|
|
396
428
|
declare interface PluginHttpRouteRequest {
|
|
397
429
|
readonly method: string;
|
|
398
430
|
/** Path WITHOUT query string. */
|
|
@@ -504,6 +536,12 @@ declare interface PluginHttpRouteResult {
|
|
|
504
536
|
/** Stream the response (SSE) instead of sending `body`. See
|
|
505
537
|
* {@link PluginHttpRouteStream}. */
|
|
506
538
|
readonly stream?: PluginHttpRouteStream;
|
|
539
|
+
/** Stream a BINARY response (a download, an export) instead of sending
|
|
540
|
+
* `body` — see {@link PluginHttpRouteByteStream}. Never buffered by the
|
|
541
|
+
* serve layer; never compressed (flush timing + Content-Length are the
|
|
542
|
+
* contract). Takes precedence over `body`; do not set both `stream` and
|
|
543
|
+
* `byteStream`. */
|
|
544
|
+
readonly byteStream?: PluginHttpRouteByteStream;
|
|
507
545
|
}
|
|
508
546
|
|
|
509
547
|
/**
|
|
@@ -739,6 +777,19 @@ export declare const requireAnyScope: (scopes: ReadonlyArray<string>) => RestGua
|
|
|
739
777
|
*/
|
|
740
778
|
export declare const requireScope: (scope: string) => RestGuard;
|
|
741
779
|
|
|
780
|
+
/** A BINARY streaming response (download/export). The serve layer pipes the
|
|
781
|
+
* ReadableStream without buffering — a body larger than the heap is fine.
|
|
782
|
+
* The LAZY thunk form defers opening the source until the response streams. */
|
|
783
|
+
export declare interface RestByteResponse {
|
|
784
|
+
readonly __voltroRestBytes: true;
|
|
785
|
+
readonly byteStream: {
|
|
786
|
+
readonly stream: ReadableStream<Uint8Array> | (() => ReadableStream<Uint8Array>);
|
|
787
|
+
readonly contentLength?: number;
|
|
788
|
+
readonly contentDisposition?: string;
|
|
789
|
+
};
|
|
790
|
+
readonly contentType?: string;
|
|
791
|
+
}
|
|
792
|
+
|
|
742
793
|
export declare type RestGuard = (ctx: RestRouteContext) => RestGuardRejection | undefined | Promise<RestGuardRejection | undefined>;
|
|
743
794
|
|
|
744
795
|
/**
|
|
@@ -751,7 +802,7 @@ export declare interface RestGuardRejection {
|
|
|
751
802
|
readonly message: string;
|
|
752
803
|
}
|
|
753
804
|
|
|
754
|
-
export declare type RestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
805
|
+
export declare type RestMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
755
806
|
|
|
756
807
|
/**
|
|
757
808
|
* Per-call context handed to a REST route handler. Resolved by the serve
|
|
@@ -791,6 +842,26 @@ export declare interface RestRouteDescriptor<I, O> {
|
|
|
791
842
|
/** ISO date. Sets a `Sunset:` header; past the date the route returns
|
|
792
843
|
* `410 Gone` with a replacement pointer. */
|
|
793
844
|
readonly sunset?: string;
|
|
845
|
+
/**
|
|
846
|
+
* Opt-in API version. `version: 'v2'` + `path: '/customers'` mounts the route
|
|
847
|
+
* at `/v2/customers` — the same `/vN/` convention the `publicApi:` projection
|
|
848
|
+
* has always used (`derivePublicPath`) and the built-in `/v1/api-keys`
|
|
849
|
+
* surface follows.
|
|
850
|
+
*
|
|
851
|
+
* OPT-IN on purpose: a route without `version` keeps its literal `path`
|
|
852
|
+
* untouched. An automatic prefix would silently move every deployed route —
|
|
853
|
+
* a second breaking change hiding inside a naming feature.
|
|
854
|
+
*
|
|
855
|
+
* Two versions of one resource are TWO descriptors: the old version is
|
|
856
|
+
* ordinary code — visible, testable, deletable — carrying `deprecated` (the
|
|
857
|
+
* replacement pointer) and `sunset` (the date it starts answering `410`,
|
|
858
|
+
* whose body then also names this `version`). There is no transformation
|
|
859
|
+
* DSL, and the rpc SOCKET is deliberately outside this: the generated client
|
|
860
|
+
* is versioned with the server it was generated from (a stale browser tab
|
|
861
|
+
* runs the previous client until reload — that skew window exists and is
|
|
862
|
+
* documented, it is not solved by URL versioning).
|
|
863
|
+
*/
|
|
864
|
+
readonly version?: `v${number}`;
|
|
794
865
|
readonly guards?: ReadonlyArray<RestGuard>;
|
|
795
866
|
/**
|
|
796
867
|
* This route STREAMS (Server-Sent Events) rather than resolving one value — its
|
|
@@ -801,6 +872,13 @@ export declare interface RestRouteDescriptor<I, O> {
|
|
|
801
872
|
* no spec, because clients are generated from it.
|
|
802
873
|
*/
|
|
803
874
|
readonly streaming?: boolean;
|
|
875
|
+
/** Per-route body cap override (bytes) — see PluginHttpRoute.maxBodyBytes. */
|
|
876
|
+
readonly maxBodyBytes?: number;
|
|
877
|
+
/** GET only: derive a weak ETag from the encoded response and answer a
|
|
878
|
+
* matching `If-None-Match` with 304. The tag is content-derived (an md5
|
|
879
|
+
* of the JSON), so it is correct across content-encodings — the
|
|
880
|
+
* transport's compression varies the bytes, not the representation. */
|
|
881
|
+
readonly etag?: boolean;
|
|
804
882
|
}
|
|
805
883
|
|
|
806
884
|
export declare interface RestRouteExample {
|
|
@@ -922,11 +1000,29 @@ declare type Subject = typeof Subject.Type;
|
|
|
922
1000
|
|
|
923
1001
|
declare type Target<Input = unknown, Row = unknown> = TargetSpec<Input, Row> | ReadonlyArray<TargetSpec<Input, Row>>;
|
|
924
1002
|
|
|
1003
|
+
/**
|
|
1004
|
+
* Declared many-to-many RELATIONS of a write target: `{ inputField:
|
|
1005
|
+
* junctionTable }`. After the executor succeeds — inside the SAME
|
|
1006
|
+
* transaction — the framework reconciles the junction's links for the
|
|
1007
|
+
* written row against `input[inputField]` (an array of target ids) via the
|
|
1008
|
+
* diff-based `store.relationLinks`, so a form's multi-reference field saves
|
|
1009
|
+
* in one mutation with no hand-written junction code. The anchor column is
|
|
1010
|
+
* derived from the junction's `reference()` targets (a self-junction is
|
|
1011
|
+
* refused, never guessed).
|
|
1012
|
+
*
|
|
1013
|
+
* An ABSENT input field leaves the links untouched (absent ≠ empty — an
|
|
1014
|
+
* empty array is the explicit "clear them all"). The row id is the
|
|
1015
|
+
* executor's `output.id`, falling back to `input.id`.
|
|
1016
|
+
*/
|
|
1017
|
+
declare type TargetRelations = Readonly<Record<string, string>>;
|
|
1018
|
+
|
|
925
1019
|
declare type TargetSpec<Input = unknown, Row = unknown> = InsertTarget<Input, Row> | UpdateTarget<Input, Row> | DeleteTarget<Input>;
|
|
926
1020
|
|
|
927
1021
|
declare interface UpdateTarget<Input = unknown, Row = unknown, Item = Record<string, unknown>> extends NestedTargetFields<Input> {
|
|
928
1022
|
readonly table: string;
|
|
929
1023
|
readonly op: 'update';
|
|
1024
|
+
/** Declared junction relations — see {@link TargetRelations}. */
|
|
1025
|
+
readonly relations?: TargetRelations | undefined;
|
|
930
1026
|
/** Identify the row(s) to patch. Default: `input.id`. Return an ARRAY to patch
|
|
931
1027
|
* MANY rows/items in one mutation (a bulk edit — where the per-item
|
|
932
1028
|
* parallel-write race lived). */
|
package/dist/rest.js
CHANGED
|
@@ -33,26 +33,45 @@ var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.
|
|
|
33
33
|
status: 403,
|
|
34
34
|
message: `Missing required scope: one of ${e.join(", ")}`
|
|
35
35
|
};
|
|
36
|
-
}, m = (e) =>
|
|
36
|
+
}, m = (e) => {
|
|
37
|
+
if (e.version === void 0) return e;
|
|
38
|
+
if (!/^v\d+$/.test(e.version)) throw Error(`defineRestRoute(${e.method} ${e.path}): version "${e.version}" must be 'v' followed by digits — 'v1', 'v2', …`);
|
|
39
|
+
if ((/* @__PURE__ */ RegExp("^/v\\d+(/|$)")).test(e.path)) throw Error(`defineRestRoute(${e.method} ${e.path}): the path already starts with a version segment AND declares version: '${e.version}'. Pick one spelling — either bake the version into the path, or declare version: and keep the path bare ('/customers'). Both would mount '/${e.version}${e.path}', which is never what the author meant.`);
|
|
40
|
+
return {
|
|
41
|
+
...e,
|
|
42
|
+
path: `/${e.version}${e.path}`
|
|
43
|
+
};
|
|
44
|
+
}, h = (e, t = {}) => ({
|
|
37
45
|
__voltroRestStream: !0,
|
|
38
46
|
stream: {
|
|
39
47
|
subscribe: e,
|
|
40
48
|
...t.keepAliveMs === void 0 ? {} : { keepAliveMs: t.keepAliveMs }
|
|
41
49
|
}
|
|
42
|
-
}), g = (e, t) => `event: ${e}\n${(typeof t == "string" ? t : JSON.stringify(t)).split("\n").map((e) => `data: ${e}`).join("\n")}\n\n`, _ = (e) => typeof e == "object" && !!e && e.__voltroRestStream === !0, v = (e, t
|
|
50
|
+
}), g = (e, t) => `event: ${e}\n${(typeof t == "string" ? t : JSON.stringify(t)).split("\n").map((e) => `data: ${e}`).join("\n")}\n\n`, _ = (e) => typeof e == "object" && !!e && e.__voltroRestStream === !0, v = (e, t = {}) => ({
|
|
51
|
+
__voltroRestBytes: !0,
|
|
52
|
+
byteStream: {
|
|
53
|
+
stream: e,
|
|
54
|
+
...t.contentLength === void 0 ? {} : { contentLength: t.contentLength },
|
|
55
|
+
...t.contentDisposition === void 0 ? {} : { contentDisposition: t.contentDisposition }
|
|
56
|
+
},
|
|
57
|
+
...t.contentType === void 0 ? {} : { contentType: t.contentType }
|
|
58
|
+
}), y = (e) => typeof e == "object" && !!e && e.__voltroRestBytes === !0, b = async (e) => {
|
|
59
|
+
let t = await globalThis.crypto.subtle.digest("SHA-1", new TextEncoder().encode(e));
|
|
60
|
+
return [...new Uint8Array(t)].map((e) => e.toString(16).padStart(2, "0")).join("");
|
|
61
|
+
}, x = (e, t, n) => ({
|
|
43
62
|
status: e,
|
|
44
63
|
contentType: "application/json; charset=utf-8",
|
|
45
64
|
body: JSON.stringify(t),
|
|
46
65
|
...n ? { headers: n } : {}
|
|
47
|
-
}),
|
|
66
|
+
}), S = (e) => {
|
|
48
67
|
if (e.length === 0) return;
|
|
49
68
|
let t = new TextDecoder().decode(e);
|
|
50
69
|
if (t.trim() !== "") return JSON.parse(t);
|
|
51
|
-
},
|
|
70
|
+
}, C = (e) => {
|
|
52
71
|
let t = {};
|
|
53
72
|
for (let [n, r] of new URLSearchParams(e)) t[n] = r;
|
|
54
73
|
return t;
|
|
55
|
-
},
|
|
74
|
+
}, w = (e, t) => {
|
|
56
75
|
let n = e.split("/").filter((e) => e.length > 0), r = t.split("/").filter((e) => e.length > 0), i = {};
|
|
57
76
|
for (let e = 0; e < n.length; e++) {
|
|
58
77
|
let t = n[e];
|
|
@@ -66,22 +85,24 @@ var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.
|
|
|
66
85
|
}
|
|
67
86
|
}
|
|
68
87
|
return i;
|
|
69
|
-
},
|
|
70
|
-
query:
|
|
88
|
+
}, T = (e, t) => ({
|
|
89
|
+
query: C(e.query),
|
|
71
90
|
params: t,
|
|
72
|
-
body:
|
|
73
|
-
}),
|
|
91
|
+
body: S(e.rawBody)
|
|
92
|
+
}), E = /* @__PURE__ */ new Set([
|
|
74
93
|
"POST",
|
|
75
94
|
"PUT",
|
|
76
95
|
"PATCH",
|
|
77
96
|
"DELETE"
|
|
78
|
-
]),
|
|
97
|
+
]), D = (c, l) => {
|
|
79
98
|
let u = c.input ? s.decodeUnknown(c.input) : void 0, d = s.encode(c.output), f = {};
|
|
80
99
|
return c.deprecated !== void 0 && (f.Deprecation = "true"), c.sunset !== void 0 && (f.Sunset = c.sunset), {
|
|
81
100
|
method: "*",
|
|
82
101
|
path: c.path,
|
|
102
|
+
...c.maxBodyBytes === void 0 ? {} : { maxBodyBytes: c.maxBodyBytes },
|
|
83
103
|
handle: async (s) => {
|
|
84
|
-
|
|
104
|
+
let p = s.method.toUpperCase();
|
|
105
|
+
if (!(p === c.method || p === "HEAD" && c.method === "GET")) return x(405, {
|
|
85
106
|
error: "Method Not Allowed",
|
|
86
107
|
allow: c.method
|
|
87
108
|
}, {
|
|
@@ -90,74 +111,98 @@ var c = (e, t) => t.method ?? (e === "query" ? "GET" : "POST"), l = (e, t) => t.
|
|
|
90
111
|
});
|
|
91
112
|
if (c.sunset !== void 0) {
|
|
92
113
|
let e = Date.parse(c.sunset);
|
|
93
|
-
if (!Number.isNaN(e) && Date.now() >= e) return
|
|
114
|
+
if (!Number.isNaN(e) && Date.now() >= e) return x(410, {
|
|
94
115
|
error: "Gone",
|
|
116
|
+
...c.version === void 0 ? {} : { version: c.version },
|
|
95
117
|
...c.deprecated === void 0 ? {} : { replacement: c.deprecated }
|
|
96
118
|
}, f);
|
|
97
119
|
}
|
|
98
|
-
let
|
|
120
|
+
let m;
|
|
99
121
|
if (u) {
|
|
100
|
-
let e =
|
|
122
|
+
let e = T(s, w(c.path, s.path)), t = await o.runPromise(u(e).pipe(o.map((e) => ({
|
|
101
123
|
ok: !0,
|
|
102
124
|
value: e
|
|
103
125
|
})), o.catchAll((e) => o.succeed({
|
|
104
126
|
ok: !1,
|
|
105
127
|
cause: e
|
|
106
128
|
}))));
|
|
107
|
-
if (!t.ok) return
|
|
129
|
+
if (!t.ok) return x(400, {
|
|
108
130
|
error: "Invalid request",
|
|
109
131
|
detail: String(t.cause)
|
|
110
132
|
}, f);
|
|
111
|
-
|
|
133
|
+
m = t.value;
|
|
112
134
|
}
|
|
113
|
-
let
|
|
114
|
-
subject:
|
|
135
|
+
let h = l.resolveSubject ? await l.resolveSubject(s.headers) : e(s.headers["x-tenant"] ?? null), g = {
|
|
136
|
+
subject: h,
|
|
115
137
|
headers: s.headers,
|
|
116
138
|
store: l.store
|
|
117
139
|
};
|
|
118
140
|
if (c.guards) for (let e of c.guards) {
|
|
119
|
-
let t = await e(
|
|
120
|
-
if (t) return
|
|
141
|
+
let t = await e(g);
|
|
142
|
+
if (t) return x(t.status, { error: t.message }, f);
|
|
121
143
|
}
|
|
122
|
-
let
|
|
123
|
-
if (
|
|
124
|
-
let e = await i(
|
|
125
|
-
if (e.kind === "replay") return
|
|
144
|
+
let v = l.idempotency, S = v && E.has(c.method) ? s.headers[v.header.toLowerCase()] : void 0, C = v && S ? r(h.tenantId, c.method, c.path) : "";
|
|
145
|
+
if (v && S) {
|
|
146
|
+
let e = await i(v.store, C, S, v.ttlMs, Date.now());
|
|
147
|
+
if (e.kind === "replay") return x(e.response.status, e.response.body, {
|
|
126
148
|
...f,
|
|
127
149
|
"Idempotency-Replayed": "true"
|
|
128
150
|
});
|
|
129
|
-
if (e.kind === "conflict") return
|
|
151
|
+
if (e.kind === "conflict") return x(409, { error: "A request with this Idempotency-Key is already being processed" }, f);
|
|
130
152
|
}
|
|
131
153
|
try {
|
|
132
|
-
let e = await c.handler(
|
|
133
|
-
if (_(e)) return {
|
|
154
|
+
let e = await c.handler(m, g);
|
|
155
|
+
if (_(e) || y(e)) return v && S && await n(v.store, C, S), y(e) ? {
|
|
156
|
+
status: 200,
|
|
157
|
+
headers: f,
|
|
158
|
+
byteStream: e.byteStream,
|
|
159
|
+
...e.contentType === void 0 ? {} : { contentType: e.contentType }
|
|
160
|
+
} : {
|
|
134
161
|
status: 200,
|
|
135
162
|
headers: f,
|
|
136
163
|
stream: e.stream
|
|
137
164
|
};
|
|
138
|
-
let
|
|
139
|
-
|
|
165
|
+
let r = await o.runPromise(d(e));
|
|
166
|
+
if (v && S && await t(v.store, C, S, {
|
|
140
167
|
status: 200,
|
|
141
|
-
body:
|
|
142
|
-
}, Date.now()),
|
|
168
|
+
body: r
|
|
169
|
+
}, Date.now()), c.etag === !0 && c.method === "GET") {
|
|
170
|
+
let e = `W/"${await b(JSON.stringify(r))}"`, t = s.headers["if-none-match"];
|
|
171
|
+
return t !== void 0 && t.split(",").some((t) => t.trim() === e) ? {
|
|
172
|
+
status: 304,
|
|
173
|
+
headers: {
|
|
174
|
+
...f,
|
|
175
|
+
ETag: e
|
|
176
|
+
}
|
|
177
|
+
} : x(200, r, {
|
|
178
|
+
...f,
|
|
179
|
+
ETag: e
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return x(200, r, f);
|
|
143
183
|
} catch (e) {
|
|
144
|
-
if (
|
|
184
|
+
if (v && S && await n(v.store, C, S), typeof e == "object" && e && typeof e.status == "number") {
|
|
145
185
|
let t = e;
|
|
146
|
-
return
|
|
186
|
+
return x(t.status, { error: t.message ?? "Error" }, f);
|
|
147
187
|
}
|
|
148
188
|
return a({
|
|
149
189
|
error: e,
|
|
150
190
|
source: "rest",
|
|
151
191
|
name: `${c.method} ${c.path}`,
|
|
152
192
|
fields: { status: 500 }
|
|
153
|
-
}),
|
|
193
|
+
}), x(500, { error: "Internal Server Error" }, f);
|
|
154
194
|
}
|
|
155
195
|
}
|
|
156
196
|
};
|
|
157
|
-
},
|
|
197
|
+
}, O = (e, t = {}) => {
|
|
198
|
+
if (t.idempotency !== void 0) {
|
|
199
|
+
for (let t of e) if (t.streaming === !0 && E.has(t.method)) throw Error(`REST route ${t.method} ${t.path} declares \`streaming: true\` on a method the idempotency binding claims (${[...E].join("/")}). A stream cannot complete an idempotency claim — there is no replayable body to cache — so a retried request would 409 until the claim's TTL. Either serve the stream on GET, or scope the idempotency binding away from this app's streaming routes.`);
|
|
200
|
+
}
|
|
201
|
+
return e.map((e) => D(e, t));
|
|
202
|
+
}, k = async (e, t) => {
|
|
158
203
|
let n = await e[0].handle(t);
|
|
159
204
|
for (let r = 1; r < e.length && n.status === 405; r++) n = await e[r].handle(t);
|
|
160
205
|
return n;
|
|
161
206
|
};
|
|
162
207
|
//#endregion
|
|
163
|
-
export { d as collectPublicApiRoutes, m as defineRestRoute, c as derivePublicMethod, l as derivePublicPath,
|
|
208
|
+
export { v as bytes, d as collectPublicApiRoutes, m as defineRestRoute, c as derivePublicMethod, l as derivePublicPath, k as dispatchSharedPath, y as isRestByteResponse, _ as isRestStreamResponse, w as matchPathParams, u as publicApiRoute, p as requireAnyScope, f as requireScope, O as restRoutesToHttpRoutes, h as sse, g as sseFrame };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"description": "The Voltro wire + plugin contract — defineQuery/Mutation/Action/Stream, definePlugin, sessions / JWT / API-keys, and the RPC protocol.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@effect/sql": "^0.52.0",
|
|
57
|
-
"@voltro/database": "0.
|
|
58
|
-
"@voltro/logger": "0.
|
|
57
|
+
"@voltro/database": "0.53.0",
|
|
58
|
+
"@voltro/logger": "0.53.0",
|
|
59
59
|
"jose": "^6.2.8"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|