@relaybus/core 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/index.cjs +77 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +78 -14
- package/dist/index.js.map +1 -1
- package/dist/internal/index.cjs +306 -43
- package/dist/internal/index.cjs.map +1 -1
- package/dist/internal/index.d.cts +75 -9
- package/dist/internal/index.d.ts +75 -9
- package/dist/internal/index.js +306 -43
- package/dist/internal/index.js.map +1 -1
- package/dist/{transport-BlfzIBZD.d.cts → transport-DIFdeXQY.d.cts} +2 -1
- package/dist/{transport-BlfzIBZD.d.ts → transport-DIFdeXQY.d.ts} +2 -1
- package/package.json +1 -1
package/dist/internal/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EventHeaders, EventEnvelope, Clock, LeaseReference, ErrorInfo, ClaimRequest, ClaimedDelivery, Transport, TransportMessageHandler, TransportSubscriptionHooks, CloseOptions, Duration } from '../index.js';
|
|
2
2
|
|
|
3
3
|
/** Returns `value` when it is an integer of at least 1, else throws a {@link ConfigurationError} naming the option. */
|
|
4
4
|
declare function assertPositiveInteger(value: number, name: string): number;
|
|
@@ -82,26 +82,60 @@ type WireField = BodyField | HeaderField;
|
|
|
82
82
|
type FixedType = {
|
|
83
83
|
readonly fixed: string;
|
|
84
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* How `occurredAt` travels: RFC 3339 text (default), or whole epoch milliseconds or seconds, a
|
|
87
|
+
* number in the body and decimal text in a header. Epoch seconds drop the milliseconds.
|
|
88
|
+
*/
|
|
89
|
+
type TimeFormat = 'rfc3339' | 'epoch-millis' | 'epoch-seconds';
|
|
90
|
+
/** Where the event time lives, and in which format. */
|
|
91
|
+
type TimeField<TField extends WireField> = TField & {
|
|
92
|
+
readonly format?: TimeFormat;
|
|
93
|
+
};
|
|
94
|
+
/** A JSON value written as it is. */
|
|
95
|
+
type JsonConstant = string | number | boolean | null | readonly JsonConstant[] | {
|
|
96
|
+
readonly [name: string]: JsonConstant;
|
|
97
|
+
};
|
|
98
|
+
/** A value written with every message: a constant, or derived from the event. */
|
|
99
|
+
type ExtraValue<TValue> = TValue | ((envelope: EventEnvelope) => TValue);
|
|
100
|
+
/** Extra message headers, by name. */
|
|
101
|
+
type ExtraHeaders = Readonly<Record<string, ExtraValue<string>>>;
|
|
102
|
+
/** Extra body values, by dotted path. */
|
|
103
|
+
type ExtraBody = Readonly<Record<string, ExtraValue<JsonConstant>>>;
|
|
85
104
|
type MappingFields<TField extends WireField, TTransportId extends string> = {
|
|
86
105
|
/** Where the event type lives, or the one type of the route. Required. */
|
|
87
106
|
readonly type: TField | FixedType;
|
|
88
107
|
/**
|
|
89
108
|
* Where the event id lives, or the transport's own message identity (see the transport's
|
|
90
109
|
* mapping type). Required, so the inbox dedup key is always a deliberate choice. A message
|
|
91
|
-
* whose mapped id is missing is malformed
|
|
110
|
+
* whose mapped id is missing is malformed, unless `whenMissing` names the transport's own id
|
|
111
|
+
* to use instead: `{ body: 'id', whenMissing: 'record-position' }`.
|
|
92
112
|
*/
|
|
93
|
-
readonly id: TField | TTransportId
|
|
113
|
+
readonly id: TField | TTransportId | (TField & {
|
|
114
|
+
readonly whenMissing: TTransportId;
|
|
115
|
+
});
|
|
94
116
|
/** Ordering key. Default: the transport's own (the Kafka record key; none on RabbitMQ). */
|
|
95
117
|
readonly key?: TField;
|
|
96
118
|
/** Producing service. Default: the transport's own (Kafka topic; RabbitMQ `appId`, else the routing key). */
|
|
97
119
|
readonly source?: TField;
|
|
98
|
-
/**
|
|
99
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Event time, RFC 3339 unless `format` says otherwise. Default: the transport's own (Kafka
|
|
122
|
+
* record timestamp; RabbitMQ `timestamp`, seconds).
|
|
123
|
+
*/
|
|
124
|
+
readonly occurredAt?: TimeField<TField>;
|
|
100
125
|
readonly correlationId?: TField;
|
|
101
126
|
readonly causationId?: TField;
|
|
102
127
|
/** Media type written with the message. Default `application/json`. Not checked on read. */
|
|
103
128
|
readonly contentType?: string;
|
|
129
|
+
/**
|
|
130
|
+
* Wire names of event types, for a format whose type values are not relaybus event types or
|
|
131
|
+
* name them otherwise: `{ 'transfer.auto-dispatched.v1': 'Transfer Auto Dispatched' }`. A listed
|
|
132
|
+
* type is written as its wire name and read back from it; any other type travels as itself. One
|
|
133
|
+
* route then carries many types without a `fixed` type per transport. Not with a `fixed` type.
|
|
134
|
+
*/
|
|
135
|
+
readonly wireTypes?: WireTypes;
|
|
104
136
|
};
|
|
137
|
+
/** Event type to the name the wire uses for it; see `wireTypes` of a jsonEnvelope mapping. */
|
|
138
|
+
type WireTypes = Readonly<Record<string, string>>;
|
|
105
139
|
/**
|
|
106
140
|
* A JSON object that wraps the payload with event metadata, such as
|
|
107
141
|
* `{ "eventName": ..., "messageId": ..., "data": {...}, "meta": {...} }`.
|
|
@@ -111,10 +145,37 @@ type BodyEnvelopeMapping<TTransportId extends string> = MappingFields<WireField,
|
|
|
111
145
|
/** Body path of the payload. Required. */
|
|
112
146
|
readonly payload: BodyField;
|
|
113
147
|
/**
|
|
114
|
-
* Event headers: a body path holding an object of strings,
|
|
115
|
-
* every message header that no other field maps
|
|
148
|
+
* Event headers: a body path holding an object of strings, `'message-headers'` (default),
|
|
149
|
+
* every message header that no other field maps, or `'top-level-fields'`, spread over the top
|
|
150
|
+
* level of the body: every top-level text field that no other value uses is read as one.
|
|
151
|
+
*/
|
|
152
|
+
readonly headers?: BodyField | 'message-headers' | 'top-level-fields';
|
|
153
|
+
/**
|
|
154
|
+
* Values written with every message besides the mapped ones, such as a constant
|
|
155
|
+
* `provider: 'orders-api'` or a second copy of the time. They are never read; extra headers
|
|
156
|
+
* are left out of the event headers on read.
|
|
157
|
+
*/
|
|
158
|
+
readonly extra?: {
|
|
159
|
+
readonly body?: ExtraBody;
|
|
160
|
+
readonly headers?: ExtraHeaders;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Top-level body fields to write first, in this order; the others follow in the order they
|
|
164
|
+
* are written. Only the bytes change, for formats compared byte for byte.
|
|
165
|
+
*/
|
|
166
|
+
readonly bodyOrder?: readonly string[];
|
|
167
|
+
/**
|
|
168
|
+
* Keeps the top-level body fields that no other value uses (and that are not read as event
|
|
169
|
+
* headers) with every JSON type, in body order, as one object under `payloadField` of the
|
|
170
|
+
* payload: `{ payloadField: 'wire' }` reads `{ "data": { "a": 1 }, "attempt": 2 }` as the payload
|
|
171
|
+
* `{ "a": 1, "wire": { "attempt": 2 } }`, so the handler's schema validates them with the payload.
|
|
172
|
+
* Writing spreads that object back over the top level. Default: such fields are left out on read.
|
|
116
173
|
*/
|
|
117
|
-
readonly
|
|
174
|
+
readonly otherFields?: OtherFields;
|
|
175
|
+
};
|
|
176
|
+
/** Where {@link BodyEnvelopeMapping.otherFields} keeps the unused top-level fields. */
|
|
177
|
+
type OtherFields = {
|
|
178
|
+
readonly payloadField: string;
|
|
118
179
|
};
|
|
119
180
|
/**
|
|
120
181
|
* The body is the payload itself, as a plain JSON producer writes it; every other value lives in
|
|
@@ -124,6 +185,10 @@ type PayloadOnlyMapping<TTransportId extends string> = MappingFields<HeaderField
|
|
|
124
185
|
readonly payload: 'whole-body';
|
|
125
186
|
/** Event headers: every message header that no other field maps. */
|
|
126
187
|
readonly headers?: 'message-headers';
|
|
188
|
+
/** Message headers written with every message; never read, and left out of the event headers. */
|
|
189
|
+
readonly extra?: {
|
|
190
|
+
readonly headers?: ExtraHeaders;
|
|
191
|
+
};
|
|
127
192
|
};
|
|
128
193
|
/** Where every envelope value lives in a JSON message. `type`, `id` and `payload` are required. */
|
|
129
194
|
type JsonEnvelopeMapping<TTransportId extends string> = BodyEnvelopeMapping<TTransportId> | PayloadOnlyMapping<TTransportId>;
|
|
@@ -435,4 +500,5 @@ declare abstract class PositiveInteger {
|
|
|
435
500
|
static of(value: number, name: string): number;
|
|
436
501
|
}
|
|
437
502
|
|
|
438
|
-
export {
|
|
503
|
+
export { BoundedInteger, CompositeKey, DecodedEventReader, DeliveryLeaseIdentity, ExponentialBackoff, HandedOutLedger, LaneLeaseTokenFormat, LeaseSettlement, Milliseconds, NonEmptyText, OwnerSerializedClaims, PositiveInteger, SubscribingTransport, assertPositiveInteger, createCloudEventReaders, createJsonEnvelopeFormat, fromHeader, fromPayloadField, isJsonObject, mediaTypeOf, observeRejection, schedule, sleep, writeStructuredCloudEvent };
|
|
504
|
+
export type { BodyEnvelopeMapping, BodyField, CancelTimer, CloudEventReaders, DecodedEventFields, ExtraBody, ExtraHeaders, ExtraValue, FixedType, HandedOutEntry, HeaderField, IntegerBounds, JsonConstant, JsonEnvelopeFailures, JsonEnvelopeFields, JsonEnvelopeFormat, JsonEnvelopeMapping, JsonWireMessage, LaneLeaseTokenLimits, LaneLeaseTokenParts, LeaseChange, LeaseExtension, OtherFields, PayloadOnlyMapping, ScheduledTask, SettlementChange, TextOption, TimeField, TimeFormat, TransportSubscription, WireField, WireTypes };
|
package/dist/internal/index.js
CHANGED
|
@@ -89,6 +89,55 @@ function writeStructuredCloudEvent(envelope, { time, data }) {
|
|
|
89
89
|
return `${JSON.stringify(attributes).slice(0, -1)},"data":${data}}`;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// src/internal/json-envelope-wire-types.ts
|
|
93
|
+
function compileWireTypes(wireTypes, { fixed }) {
|
|
94
|
+
if (wireTypes === void 0) {
|
|
95
|
+
return { wireTypes: /* @__PURE__ */ new Map(), eventTypes: /* @__PURE__ */ new Map() };
|
|
96
|
+
}
|
|
97
|
+
if (!isJsonObject(wireTypes) || fixed) {
|
|
98
|
+
throw new ConfigurationError(
|
|
99
|
+
"jsonEnvelope wireTypes must be an object of event type to wire name, with a type at { body } or { header }"
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const entries = Object.entries(wireTypes);
|
|
103
|
+
const eventTypes = /* @__PURE__ */ new Map();
|
|
104
|
+
for (const [type, wire] of entries) {
|
|
105
|
+
assertEventType(type);
|
|
106
|
+
if (typeof wire !== "string" || wire.length === 0 || eventTypes.has(wire)) {
|
|
107
|
+
throw new ConfigurationError(
|
|
108
|
+
`jsonEnvelope wireTypes needs a non-empty wire name for '${type}' that no other type uses`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
eventTypes.set(wire, type);
|
|
112
|
+
}
|
|
113
|
+
return { wireTypes: new Map(entries), eventTypes };
|
|
114
|
+
}
|
|
115
|
+
function wireTypeOf(envelope, { plan, failures }) {
|
|
116
|
+
const wire = plan.wireTypes.get(envelope.type);
|
|
117
|
+
if (wire !== void 0) {
|
|
118
|
+
return wire;
|
|
119
|
+
}
|
|
120
|
+
const other = plan.eventTypes.get(envelope.type);
|
|
121
|
+
if (other !== void 0) {
|
|
122
|
+
throw failures.unencodable(
|
|
123
|
+
`Event ${envelope.id} has type '${envelope.type}', which wireTypes uses as the wire name of '${other}'`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return envelope.type;
|
|
127
|
+
}
|
|
128
|
+
function eventTypeOf(wire, { plan, failures }) {
|
|
129
|
+
const type = plan.eventTypes.get(wire);
|
|
130
|
+
if (type !== void 0) {
|
|
131
|
+
return type;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
assertEventType(wire);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
throw failures.invalid(`The event type is not a valid event type: ${error.message}`);
|
|
137
|
+
}
|
|
138
|
+
return wire;
|
|
139
|
+
}
|
|
140
|
+
|
|
92
141
|
// src/internal/json-envelope-plan.ts
|
|
93
142
|
var OPTIONAL_NAMES = [
|
|
94
143
|
"key",
|
|
@@ -97,6 +146,7 @@ var OPTIONAL_NAMES = [
|
|
|
97
146
|
"correlationId",
|
|
98
147
|
"causationId"
|
|
99
148
|
];
|
|
149
|
+
var TIME_FORMATS = ["rfc3339", "epoch-millis", "epoch-seconds"];
|
|
100
150
|
var ARRAY_INDEX = /^(?:0|[1-9]\d*)$/;
|
|
101
151
|
function compile(mapping, transportIds) {
|
|
102
152
|
if (!isJsonObject(mapping)) {
|
|
@@ -109,16 +159,72 @@ function compile(mapping, transportIds) {
|
|
|
109
159
|
optional[name] = toLocation(field, name);
|
|
110
160
|
}
|
|
111
161
|
}
|
|
162
|
+
const payload = compilePayload(mapping.payload);
|
|
163
|
+
const topLevelHeaders = mapping.headers === "top-level-fields";
|
|
164
|
+
if (topLevelHeaders && payload === void 0) {
|
|
165
|
+
throw new ConfigurationError("jsonEnvelope headers 'top-level-fields' needs a body payload");
|
|
166
|
+
}
|
|
167
|
+
const type = compileType(mapping.type);
|
|
112
168
|
const plan = {
|
|
113
|
-
type
|
|
169
|
+
type,
|
|
114
170
|
id: compileId(mapping.id, transportIds),
|
|
171
|
+
idWhenMissing: isJsonObject(mapping.id) && "whenMissing" in mapping.id,
|
|
115
172
|
optional,
|
|
116
|
-
payload
|
|
117
|
-
headers: compileHeaders(mapping.headers),
|
|
118
|
-
|
|
173
|
+
payload,
|
|
174
|
+
headers: topLevelHeaders ? void 0 : compileHeaders(mapping.headers),
|
|
175
|
+
topLevelHeaders,
|
|
176
|
+
topLevelNames: /* @__PURE__ */ new Set(),
|
|
177
|
+
mappedHeaders: /* @__PURE__ */ new Set(),
|
|
178
|
+
timeFormat: compileTimeFormat(mapping.occurredAt),
|
|
179
|
+
otherFields: compileOtherFields(mapping, payload !== void 0),
|
|
180
|
+
...compileWireTypes(mapping.wireTypes, { fixed: "fixed" in type }),
|
|
181
|
+
...compileExtras(mapping, payload !== void 0)
|
|
119
182
|
};
|
|
120
183
|
return assertDisjoint(plan);
|
|
121
184
|
}
|
|
185
|
+
function compileTimeFormat(field) {
|
|
186
|
+
const format = isJsonObject(field) ? field["format"] : void 0;
|
|
187
|
+
if (format === void 0) {
|
|
188
|
+
return "rfc3339";
|
|
189
|
+
}
|
|
190
|
+
if (!TIME_FORMATS.includes(format)) {
|
|
191
|
+
throw new ConfigurationError(
|
|
192
|
+
`jsonEnvelope occurredAt format must be one of ${TIME_FORMATS.map((name) => `'${name}'`).join(", ")}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return format;
|
|
196
|
+
}
|
|
197
|
+
function compileExtras(mapping, hasBody) {
|
|
198
|
+
const extra = mapping.extra ?? {};
|
|
199
|
+
const bodyOrder = mapping.bodyOrder ?? [];
|
|
200
|
+
if (!hasBody && (extra.body !== void 0 || bodyOrder.length > 0)) {
|
|
201
|
+
throw new ConfigurationError(
|
|
202
|
+
"jsonEnvelope with a whole-body payload has no body for extra values or a body order"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
extraBody: Object.entries(extra.body ?? {}).map(([path, value]) => ({
|
|
207
|
+
path: toPath(path, "extra body value"),
|
|
208
|
+
value
|
|
209
|
+
})),
|
|
210
|
+
extraHeaders: Object.entries(extra.headers ?? {}),
|
|
211
|
+
bodyOrder
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function compileOtherFields(mapping, hasBody) {
|
|
215
|
+
const otherFields = mapping.otherFields;
|
|
216
|
+
if (otherFields === void 0) {
|
|
217
|
+
return void 0;
|
|
218
|
+
}
|
|
219
|
+
if (!hasBody) {
|
|
220
|
+
throw new ConfigurationError("jsonEnvelope otherFields needs a body payload");
|
|
221
|
+
}
|
|
222
|
+
const field = isJsonObject(otherFields) ? otherFields["payloadField"] : void 0;
|
|
223
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
224
|
+
throw new ConfigurationError("jsonEnvelope otherFields must be { payloadField: '<name>' }");
|
|
225
|
+
}
|
|
226
|
+
return field;
|
|
227
|
+
}
|
|
122
228
|
function compileType(type) {
|
|
123
229
|
if (isJsonObject(type) && typeof type["fixed"] === "string") {
|
|
124
230
|
assertEventType(type["fixed"]);
|
|
@@ -133,11 +239,15 @@ function compileId(id, transportIds) {
|
|
|
133
239
|
if (typeof id === "string" && transportIds.includes(id)) {
|
|
134
240
|
return void 0;
|
|
135
241
|
}
|
|
242
|
+
const names = transportIds.map((name) => `'${name}'`).join(", ");
|
|
136
243
|
if (!isJsonObject(id)) {
|
|
137
244
|
throw new ConfigurationError(
|
|
138
|
-
`jsonEnvelope needs an id: { body }, { header } or one of ${
|
|
245
|
+
`jsonEnvelope needs an id: { body }, { header } or one of ${names}`
|
|
139
246
|
);
|
|
140
247
|
}
|
|
248
|
+
if ("whenMissing" in id && !transportIds.includes(id["whenMissing"])) {
|
|
249
|
+
throw new ConfigurationError(`jsonEnvelope id whenMissing must be one of ${names}`);
|
|
250
|
+
}
|
|
141
251
|
return toLocation(id, "id");
|
|
142
252
|
}
|
|
143
253
|
function compilePayload(payload) {
|
|
@@ -186,17 +296,25 @@ function assertDisjoint(plan) {
|
|
|
186
296
|
const locations = [plan.type, plan.id, ...Object.values(plan.optional)].filter(
|
|
187
297
|
(location) => location !== void 0 && !("fixed" in location)
|
|
188
298
|
);
|
|
189
|
-
const headers =
|
|
299
|
+
const headers = [
|
|
300
|
+
...locations.flatMap((location) => "header" in location ? [location.header] : []),
|
|
301
|
+
...plan.extraHeaders.map(([name]) => name)
|
|
302
|
+
];
|
|
190
303
|
const duplicate = headers.find((name, index) => headers.indexOf(name) !== index);
|
|
191
304
|
if (duplicate !== void 0) {
|
|
192
305
|
throw new ConfigurationError(`jsonEnvelope maps header '${duplicate}' twice`);
|
|
193
306
|
}
|
|
194
307
|
const paths = [
|
|
195
308
|
...locations.flatMap((location) => "path" in location ? [location.path] : []),
|
|
196
|
-
...[plan.payload, plan.headers].filter((path) => path !== void 0)
|
|
309
|
+
...[plan.payload, plan.headers].filter((path) => path !== void 0),
|
|
310
|
+
...plan.extraBody.map(({ path }) => path)
|
|
197
311
|
];
|
|
198
312
|
assertNoNestedPaths(paths);
|
|
199
|
-
return {
|
|
313
|
+
return {
|
|
314
|
+
...plan,
|
|
315
|
+
mappedHeaders: new Set(headers),
|
|
316
|
+
topLevelNames: new Set(paths.map((path) => path[0]))
|
|
317
|
+
};
|
|
200
318
|
}
|
|
201
319
|
function assertNoNestedPaths(paths) {
|
|
202
320
|
paths.forEach((path, index) => {
|
|
@@ -216,8 +334,60 @@ function assertCompatiblePaths(path, other) {
|
|
|
216
334
|
}
|
|
217
335
|
}
|
|
218
336
|
|
|
337
|
+
// src/internal/json-envelope-other-fields.ts
|
|
338
|
+
function unusedFields(body, { plan }) {
|
|
339
|
+
return Object.entries(body).filter(
|
|
340
|
+
([name, value]) => !plan.topLevelNames.has(name) && !(plan.topLevelHeaders && typeof value === "string")
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
function withOtherFields(payload, { body, context }) {
|
|
344
|
+
const field = context.plan.otherFields;
|
|
345
|
+
if (field === void 0) {
|
|
346
|
+
return payload;
|
|
347
|
+
}
|
|
348
|
+
const fields = unusedFields(body, context);
|
|
349
|
+
if (!isJsonObject(payload) && fields.length === 0) {
|
|
350
|
+
return payload;
|
|
351
|
+
}
|
|
352
|
+
if (!isJsonObject(payload) || Object.hasOwn(payload, field)) {
|
|
353
|
+
throw context.failures.invalid(
|
|
354
|
+
`otherFields keeps the unused top-level fields under the payload field '${field}', but the payload is not an object without that field`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
return Object.fromEntries([...Object.entries(payload), [field, Object.fromEntries(fields)]]);
|
|
358
|
+
}
|
|
359
|
+
function splitOtherFields(envelope, context) {
|
|
360
|
+
const field = context.plan.otherFields;
|
|
361
|
+
const { payload } = envelope;
|
|
362
|
+
if (field === void 0 || !isJsonObject(payload) || !Object.hasOwn(payload, field)) {
|
|
363
|
+
return { payload, fields: [] };
|
|
364
|
+
}
|
|
365
|
+
const others = payload[field];
|
|
366
|
+
if (!isJsonObject(others)) {
|
|
367
|
+
throw context.failures.unencodable(
|
|
368
|
+
`Event ${envelope.id} has a payload field '${field}' that is not an object of top-level fields`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
const fields = Object.entries(others);
|
|
372
|
+
const refused = fields.find((entry) => isRefused(entry, context));
|
|
373
|
+
if (refused !== void 0) {
|
|
374
|
+
throw context.failures.unencodable(
|
|
375
|
+
`Event ${envelope.id} keeps a top-level field '${refused[0]}' under '${field}' that the mapping reads as another value`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
payload: Object.fromEntries(Object.entries(payload).filter(([name]) => name !== field)),
|
|
380
|
+
fields
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function isRefused([name, value], { plan }) {
|
|
384
|
+
return plan.topLevelNames.has(name) || plan.topLevelHeaders && typeof value === "string";
|
|
385
|
+
}
|
|
386
|
+
|
|
219
387
|
// src/internal/json-envelope-read.ts
|
|
220
388
|
var RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
|
|
389
|
+
var EPOCH_PATTERN = /^\d+$/;
|
|
390
|
+
var MILLISECONDS_PER_SECOND = 1e3;
|
|
221
391
|
function readMessage(message, context) {
|
|
222
392
|
const { plan, failures } = context;
|
|
223
393
|
if (plan.payload !== void 0 && !isJsonObject(message.body)) {
|
|
@@ -225,7 +395,7 @@ function readMessage(message, context) {
|
|
|
225
395
|
}
|
|
226
396
|
const read = (location) => location === void 0 ? void 0 : readText(message, { location, failures });
|
|
227
397
|
const fields = {
|
|
228
|
-
id:
|
|
398
|
+
id: readId(read(plan.id), context),
|
|
229
399
|
type: readType(message, context),
|
|
230
400
|
payload: readPayload(message.body, context),
|
|
231
401
|
key: read(plan.optional.key),
|
|
@@ -233,24 +403,25 @@ function readMessage(message, context) {
|
|
|
233
403
|
headers: readEventHeaders(message, plan),
|
|
234
404
|
correlationId: read(plan.optional.correlationId) ?? null,
|
|
235
405
|
causationId: read(plan.optional.causationId) ?? null,
|
|
236
|
-
occurredAt: readTime(read(plan.optional.occurredAt),
|
|
406
|
+
occurredAt: readTime(read(plan.optional.occurredAt), context)
|
|
237
407
|
};
|
|
238
408
|
return Object.fromEntries(
|
|
239
409
|
Object.entries(fields).filter((entry) => entry[1] !== void 0)
|
|
240
410
|
);
|
|
241
411
|
}
|
|
412
|
+
function readId(id, { plan, failures }) {
|
|
413
|
+
if (plan.id === void 0 || plan.idWhenMissing) {
|
|
414
|
+
return id;
|
|
415
|
+
}
|
|
416
|
+
return requireText(id, { location: plan.id, failures });
|
|
417
|
+
}
|
|
242
418
|
function readType(message, { plan, failures }) {
|
|
243
419
|
if ("fixed" in plan.type) {
|
|
244
420
|
return plan.type.fixed;
|
|
245
421
|
}
|
|
246
422
|
const location = plan.type;
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
assertEventType(type);
|
|
250
|
-
} catch (error) {
|
|
251
|
-
throw failures.invalid(`The event type is not a valid event type: ${error.message}`);
|
|
252
|
-
}
|
|
253
|
-
return type;
|
|
423
|
+
const wire = requireText(readText(message, { location, failures }), { location, failures });
|
|
424
|
+
return eventTypeOf(wire, { plan, failures });
|
|
254
425
|
}
|
|
255
426
|
function readPayload(body, { plan, failures }) {
|
|
256
427
|
if (plan.payload === void 0) {
|
|
@@ -260,25 +431,45 @@ function readPayload(body, { plan, failures }) {
|
|
|
260
431
|
if (payload === void 0) {
|
|
261
432
|
throw failures.invalid(`The message has no payload at '${plan.payload.join(".")}'`);
|
|
262
433
|
}
|
|
263
|
-
return payload
|
|
434
|
+
return withOtherFields(payload, {
|
|
435
|
+
body,
|
|
436
|
+
context: { plan, failures }
|
|
437
|
+
});
|
|
264
438
|
}
|
|
265
439
|
function readEventHeaders(message, plan) {
|
|
440
|
+
if (plan.topLevelHeaders) {
|
|
441
|
+
return textEntries(
|
|
442
|
+
Object.entries(message.body).filter(
|
|
443
|
+
([name]) => !plan.topLevelNames.has(name)
|
|
444
|
+
)
|
|
445
|
+
);
|
|
446
|
+
}
|
|
266
447
|
if (plan.headers === void 0) {
|
|
267
448
|
return Object.fromEntries(
|
|
268
449
|
Object.entries(message.headers).filter(([name]) => !plan.mappedHeaders.has(name))
|
|
269
450
|
);
|
|
270
451
|
}
|
|
271
452
|
const headers = valueAt(message.body, plan.headers);
|
|
272
|
-
return isJsonObject(headers) ? Object.
|
|
273
|
-
Object.entries(headers).filter(
|
|
274
|
-
(entry) => typeof entry[1] === "string"
|
|
275
|
-
)
|
|
276
|
-
) : {};
|
|
453
|
+
return isJsonObject(headers) ? textEntries(Object.entries(headers)) : {};
|
|
277
454
|
}
|
|
278
|
-
function
|
|
455
|
+
function textEntries(entries) {
|
|
456
|
+
return Object.fromEntries(
|
|
457
|
+
entries.filter((entry) => typeof entry[1] === "string")
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
function readTime(text, { plan, failures }) {
|
|
279
461
|
if (text === void 0) {
|
|
280
462
|
return void 0;
|
|
281
463
|
}
|
|
464
|
+
if (plan.timeFormat !== "rfc3339") {
|
|
465
|
+
const time2 = new Date(
|
|
466
|
+
Number(text) * (plan.timeFormat === "epoch-seconds" ? MILLISECONDS_PER_SECOND : 1)
|
|
467
|
+
);
|
|
468
|
+
if (!EPOCH_PATTERN.test(text) || Number.isNaN(time2.getTime())) {
|
|
469
|
+
throw failures.invalid(`The event time is not a whole ${plan.timeFormat} number: '${text}'`);
|
|
470
|
+
}
|
|
471
|
+
return time2;
|
|
472
|
+
}
|
|
282
473
|
const time = new Date(text);
|
|
283
474
|
if (!RFC3339_PATTERN.test(text) || Number.isNaN(time.getTime())) {
|
|
284
475
|
throw failures.invalid(`The event time is not an RFC 3339 timestamp: '${text}'`);
|
|
@@ -320,63 +511,135 @@ function valueAt(body, path) {
|
|
|
320
511
|
|
|
321
512
|
// src/internal/json-envelope-write.ts
|
|
322
513
|
var MAXIMUM_RFC3339_YEAR = 9999;
|
|
514
|
+
var MILLISECONDS_PER_SECOND2 = 1e3;
|
|
323
515
|
function writeMessage(envelope, { plan, failures }) {
|
|
324
516
|
const root = {};
|
|
325
517
|
const headers = [];
|
|
326
518
|
const put = (location, value) => {
|
|
327
519
|
if ("header" in location) {
|
|
328
|
-
headers.push([location.header, value]);
|
|
520
|
+
headers.push([location.header, String(value)]);
|
|
329
521
|
} else {
|
|
330
522
|
setAt(root, { path: location.path, value });
|
|
331
523
|
}
|
|
332
524
|
};
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
`Event ${envelope.id} has type '${envelope.type}', but this route carries only '${plan.type.fixed}'`
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
} else {
|
|
340
|
-
put(plan.type, envelope.type);
|
|
525
|
+
const typeAt = typeLocation(envelope, { plan, failures });
|
|
526
|
+
if (typeAt !== void 0) {
|
|
527
|
+
put(typeAt, wireTypeOf(envelope, { plan, failures }));
|
|
341
528
|
}
|
|
342
529
|
if (plan.id !== void 0) {
|
|
343
530
|
put(plan.id, envelope.id);
|
|
344
531
|
}
|
|
345
|
-
for (const [name, value] of optionalValues(envelope, failures)) {
|
|
532
|
+
for (const [name, value] of optionalValues(envelope, { plan, failures })) {
|
|
346
533
|
const location = plan.optional[name];
|
|
347
534
|
if (location !== void 0 && value !== null) {
|
|
348
535
|
put(location, value);
|
|
349
536
|
}
|
|
350
537
|
}
|
|
351
538
|
return {
|
|
352
|
-
body: writeBody(envelope, { plan, root }),
|
|
353
|
-
headers: Object.fromEntries([
|
|
539
|
+
body: writeBody(envelope, { plan, failures, root }),
|
|
540
|
+
headers: Object.fromEntries([
|
|
541
|
+
...eventHeaderEntries(envelope, { plan, failures }),
|
|
542
|
+
...headers,
|
|
543
|
+
...extraHeaderEntries(envelope, plan)
|
|
544
|
+
])
|
|
354
545
|
};
|
|
355
546
|
}
|
|
356
|
-
function
|
|
547
|
+
function typeLocation(envelope, { plan, failures }) {
|
|
548
|
+
if (!("fixed" in plan.type)) {
|
|
549
|
+
return plan.type;
|
|
550
|
+
}
|
|
551
|
+
if (envelope.type !== plan.type.fixed) {
|
|
552
|
+
throw failures.unencodable(
|
|
553
|
+
`Event ${envelope.id} has type '${envelope.type}', but this route carries only '${plan.type.fixed}'`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
return void 0;
|
|
557
|
+
}
|
|
558
|
+
function extraHeaderEntries(envelope, plan) {
|
|
559
|
+
return plan.extraHeaders.map(([name, value]) => [
|
|
560
|
+
name,
|
|
561
|
+
typeof value === "function" ? value(envelope) : value
|
|
562
|
+
]);
|
|
563
|
+
}
|
|
564
|
+
function optionalValues(envelope, { plan, failures }) {
|
|
357
565
|
return [
|
|
358
566
|
["key", envelope.key],
|
|
359
567
|
["source", envelope.source],
|
|
360
|
-
[
|
|
568
|
+
[
|
|
569
|
+
"occurredAt",
|
|
570
|
+
timeValue(formatTime(envelope, failures), { envelope, format: plan.timeFormat })
|
|
571
|
+
],
|
|
361
572
|
["correlationId", envelope.correlationId],
|
|
362
573
|
["causationId", envelope.causationId]
|
|
363
574
|
];
|
|
364
575
|
}
|
|
365
|
-
function writeBody(envelope, { plan, root }) {
|
|
576
|
+
function writeBody(envelope, { plan, failures, root }) {
|
|
577
|
+
const { payload, fields } = splitOtherFields(envelope, { plan, failures });
|
|
366
578
|
if (plan.payload === void 0) {
|
|
367
|
-
return
|
|
579
|
+
return payload;
|
|
368
580
|
}
|
|
369
|
-
setAt(root, { path: plan.payload, value:
|
|
581
|
+
setAt(root, { path: plan.payload, value: payload });
|
|
370
582
|
if (plan.headers !== void 0) {
|
|
371
583
|
setAt(root, {
|
|
372
584
|
path: plan.headers,
|
|
373
585
|
value: Object.fromEntries(Object.entries(envelope.headers))
|
|
374
586
|
});
|
|
375
587
|
}
|
|
376
|
-
|
|
588
|
+
for (const { path, value } of plan.extraBody) {
|
|
589
|
+
setAt(root, { path, value: typeof value === "function" ? value(envelope) : value });
|
|
590
|
+
}
|
|
591
|
+
if (plan.topLevelHeaders) {
|
|
592
|
+
spreadHeaders(envelope, { plan, failures, root });
|
|
593
|
+
}
|
|
594
|
+
spreadOtherFields(envelope, { failures, fields, root });
|
|
595
|
+
return ordered(root, plan.bodyOrder);
|
|
596
|
+
}
|
|
597
|
+
function spreadHeaders(envelope, { plan, failures, root }) {
|
|
598
|
+
for (const [name, value] of Object.entries(envelope.headers)) {
|
|
599
|
+
if (plan.topLevelNames.has(name)) {
|
|
600
|
+
throw failures.unencodable(
|
|
601
|
+
`Event ${envelope.id} has a header '${name}', which the mapping uses for another body field`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
setAt(root, { path: [name], value });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function spreadOtherFields(envelope, {
|
|
608
|
+
failures,
|
|
609
|
+
fields,
|
|
610
|
+
root
|
|
611
|
+
}) {
|
|
612
|
+
for (const [name, value] of fields) {
|
|
613
|
+
if (Object.hasOwn(root, name)) {
|
|
614
|
+
throw failures.unencodable(
|
|
615
|
+
`Event ${envelope.id} keeps a top-level field '${name}' that an event header also writes`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
setAt(root, { path: [name], value });
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function timeValue(text, { envelope, format }) {
|
|
622
|
+
const milliseconds = envelope.occurredAt.getTime();
|
|
623
|
+
if (format === "epoch-millis") {
|
|
624
|
+
return milliseconds;
|
|
625
|
+
}
|
|
626
|
+
return format === "epoch-seconds" ? Math.floor(milliseconds / MILLISECONDS_PER_SECOND2) : text;
|
|
627
|
+
}
|
|
628
|
+
function ordered(root, order) {
|
|
629
|
+
if (order.length === 0) {
|
|
630
|
+
return root;
|
|
631
|
+
}
|
|
632
|
+
const result = {};
|
|
633
|
+
const names = [...order.filter((name) => Object.hasOwn(root, name)), ...Object.keys(root)];
|
|
634
|
+
for (const name of names) {
|
|
635
|
+
if (!Object.hasOwn(result, name)) {
|
|
636
|
+
defineValue(result, { name, value: root[name] });
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return result;
|
|
377
640
|
}
|
|
378
641
|
function eventHeaderEntries(envelope, { plan, failures }) {
|
|
379
|
-
if (plan.headers !== void 0) {
|
|
642
|
+
if (plan.headers !== void 0 || plan.topLevelHeaders) {
|
|
380
643
|
return [];
|
|
381
644
|
}
|
|
382
645
|
const entries = Object.entries(envelope.headers);
|