@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.
@@ -90,6 +90,55 @@ function writeStructuredCloudEvent(envelope, { time, data }) {
90
90
  return `${JSON.stringify(attributes).slice(0, -1)},"data":${data}}`;
91
91
  }
92
92
 
93
+ // src/internal/json-envelope-wire-types.ts
94
+ function compileWireTypes(wireTypes, { fixed }) {
95
+ if (wireTypes === void 0) {
96
+ return { wireTypes: /* @__PURE__ */ new Map(), eventTypes: /* @__PURE__ */ new Map() };
97
+ }
98
+ if (!isJsonObject(wireTypes) || fixed) {
99
+ throw new chunkJ76M7XTS_cjs.ConfigurationError(
100
+ "jsonEnvelope wireTypes must be an object of event type to wire name, with a type at { body } or { header }"
101
+ );
102
+ }
103
+ const entries = Object.entries(wireTypes);
104
+ const eventTypes = /* @__PURE__ */ new Map();
105
+ for (const [type, wire] of entries) {
106
+ chunkJ76M7XTS_cjs.assertEventType(type);
107
+ if (typeof wire !== "string" || wire.length === 0 || eventTypes.has(wire)) {
108
+ throw new chunkJ76M7XTS_cjs.ConfigurationError(
109
+ `jsonEnvelope wireTypes needs a non-empty wire name for '${type}' that no other type uses`
110
+ );
111
+ }
112
+ eventTypes.set(wire, type);
113
+ }
114
+ return { wireTypes: new Map(entries), eventTypes };
115
+ }
116
+ function wireTypeOf(envelope, { plan, failures }) {
117
+ const wire = plan.wireTypes.get(envelope.type);
118
+ if (wire !== void 0) {
119
+ return wire;
120
+ }
121
+ const other = plan.eventTypes.get(envelope.type);
122
+ if (other !== void 0) {
123
+ throw failures.unencodable(
124
+ `Event ${envelope.id} has type '${envelope.type}', which wireTypes uses as the wire name of '${other}'`
125
+ );
126
+ }
127
+ return envelope.type;
128
+ }
129
+ function eventTypeOf(wire, { plan, failures }) {
130
+ const type = plan.eventTypes.get(wire);
131
+ if (type !== void 0) {
132
+ return type;
133
+ }
134
+ try {
135
+ chunkJ76M7XTS_cjs.assertEventType(wire);
136
+ } catch (error) {
137
+ throw failures.invalid(`The event type is not a valid event type: ${error.message}`);
138
+ }
139
+ return wire;
140
+ }
141
+
93
142
  // src/internal/json-envelope-plan.ts
94
143
  var OPTIONAL_NAMES = [
95
144
  "key",
@@ -98,6 +147,7 @@ var OPTIONAL_NAMES = [
98
147
  "correlationId",
99
148
  "causationId"
100
149
  ];
150
+ var TIME_FORMATS = ["rfc3339", "epoch-millis", "epoch-seconds"];
101
151
  var ARRAY_INDEX = /^(?:0|[1-9]\d*)$/;
102
152
  function compile(mapping, transportIds) {
103
153
  if (!isJsonObject(mapping)) {
@@ -110,16 +160,72 @@ function compile(mapping, transportIds) {
110
160
  optional[name] = toLocation(field, name);
111
161
  }
112
162
  }
163
+ const payload = compilePayload(mapping.payload);
164
+ const topLevelHeaders = mapping.headers === "top-level-fields";
165
+ if (topLevelHeaders && payload === void 0) {
166
+ throw new chunkJ76M7XTS_cjs.ConfigurationError("jsonEnvelope headers 'top-level-fields' needs a body payload");
167
+ }
168
+ const type = compileType(mapping.type);
113
169
  const plan = {
114
- type: compileType(mapping.type),
170
+ type,
115
171
  id: compileId(mapping.id, transportIds),
172
+ idWhenMissing: isJsonObject(mapping.id) && "whenMissing" in mapping.id,
116
173
  optional,
117
- payload: compilePayload(mapping.payload),
118
- headers: compileHeaders(mapping.headers),
119
- mappedHeaders: /* @__PURE__ */ new Set()
174
+ payload,
175
+ headers: topLevelHeaders ? void 0 : compileHeaders(mapping.headers),
176
+ topLevelHeaders,
177
+ topLevelNames: /* @__PURE__ */ new Set(),
178
+ mappedHeaders: /* @__PURE__ */ new Set(),
179
+ timeFormat: compileTimeFormat(mapping.occurredAt),
180
+ otherFields: compileOtherFields(mapping, payload !== void 0),
181
+ ...compileWireTypes(mapping.wireTypes, { fixed: "fixed" in type }),
182
+ ...compileExtras(mapping, payload !== void 0)
120
183
  };
121
184
  return assertDisjoint(plan);
122
185
  }
186
+ function compileTimeFormat(field) {
187
+ const format = isJsonObject(field) ? field["format"] : void 0;
188
+ if (format === void 0) {
189
+ return "rfc3339";
190
+ }
191
+ if (!TIME_FORMATS.includes(format)) {
192
+ throw new chunkJ76M7XTS_cjs.ConfigurationError(
193
+ `jsonEnvelope occurredAt format must be one of ${TIME_FORMATS.map((name) => `'${name}'`).join(", ")}`
194
+ );
195
+ }
196
+ return format;
197
+ }
198
+ function compileExtras(mapping, hasBody) {
199
+ const extra = mapping.extra ?? {};
200
+ const bodyOrder = mapping.bodyOrder ?? [];
201
+ if (!hasBody && (extra.body !== void 0 || bodyOrder.length > 0)) {
202
+ throw new chunkJ76M7XTS_cjs.ConfigurationError(
203
+ "jsonEnvelope with a whole-body payload has no body for extra values or a body order"
204
+ );
205
+ }
206
+ return {
207
+ extraBody: Object.entries(extra.body ?? {}).map(([path, value]) => ({
208
+ path: toPath(path, "extra body value"),
209
+ value
210
+ })),
211
+ extraHeaders: Object.entries(extra.headers ?? {}),
212
+ bodyOrder
213
+ };
214
+ }
215
+ function compileOtherFields(mapping, hasBody) {
216
+ const otherFields = mapping.otherFields;
217
+ if (otherFields === void 0) {
218
+ return void 0;
219
+ }
220
+ if (!hasBody) {
221
+ throw new chunkJ76M7XTS_cjs.ConfigurationError("jsonEnvelope otherFields needs a body payload");
222
+ }
223
+ const field = isJsonObject(otherFields) ? otherFields["payloadField"] : void 0;
224
+ if (typeof field !== "string" || field.length === 0) {
225
+ throw new chunkJ76M7XTS_cjs.ConfigurationError("jsonEnvelope otherFields must be { payloadField: '<name>' }");
226
+ }
227
+ return field;
228
+ }
123
229
  function compileType(type) {
124
230
  if (isJsonObject(type) && typeof type["fixed"] === "string") {
125
231
  chunkJ76M7XTS_cjs.assertEventType(type["fixed"]);
@@ -134,11 +240,15 @@ function compileId(id, transportIds) {
134
240
  if (typeof id === "string" && transportIds.includes(id)) {
135
241
  return void 0;
136
242
  }
243
+ const names = transportIds.map((name) => `'${name}'`).join(", ");
137
244
  if (!isJsonObject(id)) {
138
245
  throw new chunkJ76M7XTS_cjs.ConfigurationError(
139
- `jsonEnvelope needs an id: { body }, { header } or one of ${transportIds.map((name) => `'${name}'`).join(", ")}`
246
+ `jsonEnvelope needs an id: { body }, { header } or one of ${names}`
140
247
  );
141
248
  }
249
+ if ("whenMissing" in id && !transportIds.includes(id["whenMissing"])) {
250
+ throw new chunkJ76M7XTS_cjs.ConfigurationError(`jsonEnvelope id whenMissing must be one of ${names}`);
251
+ }
142
252
  return toLocation(id, "id");
143
253
  }
144
254
  function compilePayload(payload) {
@@ -187,17 +297,25 @@ function assertDisjoint(plan) {
187
297
  const locations = [plan.type, plan.id, ...Object.values(plan.optional)].filter(
188
298
  (location) => location !== void 0 && !("fixed" in location)
189
299
  );
190
- const headers = locations.flatMap((location) => "header" in location ? [location.header] : []);
300
+ const headers = [
301
+ ...locations.flatMap((location) => "header" in location ? [location.header] : []),
302
+ ...plan.extraHeaders.map(([name]) => name)
303
+ ];
191
304
  const duplicate = headers.find((name, index) => headers.indexOf(name) !== index);
192
305
  if (duplicate !== void 0) {
193
306
  throw new chunkJ76M7XTS_cjs.ConfigurationError(`jsonEnvelope maps header '${duplicate}' twice`);
194
307
  }
195
308
  const paths = [
196
309
  ...locations.flatMap((location) => "path" in location ? [location.path] : []),
197
- ...[plan.payload, plan.headers].filter((path) => path !== void 0)
310
+ ...[plan.payload, plan.headers].filter((path) => path !== void 0),
311
+ ...plan.extraBody.map(({ path }) => path)
198
312
  ];
199
313
  assertNoNestedPaths(paths);
200
- return { ...plan, mappedHeaders: new Set(headers) };
314
+ return {
315
+ ...plan,
316
+ mappedHeaders: new Set(headers),
317
+ topLevelNames: new Set(paths.map((path) => path[0]))
318
+ };
201
319
  }
202
320
  function assertNoNestedPaths(paths) {
203
321
  paths.forEach((path, index) => {
@@ -217,8 +335,60 @@ function assertCompatiblePaths(path, other) {
217
335
  }
218
336
  }
219
337
 
338
+ // src/internal/json-envelope-other-fields.ts
339
+ function unusedFields(body, { plan }) {
340
+ return Object.entries(body).filter(
341
+ ([name, value]) => !plan.topLevelNames.has(name) && !(plan.topLevelHeaders && typeof value === "string")
342
+ );
343
+ }
344
+ function withOtherFields(payload, { body, context }) {
345
+ const field = context.plan.otherFields;
346
+ if (field === void 0) {
347
+ return payload;
348
+ }
349
+ const fields = unusedFields(body, context);
350
+ if (!isJsonObject(payload) && fields.length === 0) {
351
+ return payload;
352
+ }
353
+ if (!isJsonObject(payload) || Object.hasOwn(payload, field)) {
354
+ throw context.failures.invalid(
355
+ `otherFields keeps the unused top-level fields under the payload field '${field}', but the payload is not an object without that field`
356
+ );
357
+ }
358
+ return Object.fromEntries([...Object.entries(payload), [field, Object.fromEntries(fields)]]);
359
+ }
360
+ function splitOtherFields(envelope, context) {
361
+ const field = context.plan.otherFields;
362
+ const { payload } = envelope;
363
+ if (field === void 0 || !isJsonObject(payload) || !Object.hasOwn(payload, field)) {
364
+ return { payload, fields: [] };
365
+ }
366
+ const others = payload[field];
367
+ if (!isJsonObject(others)) {
368
+ throw context.failures.unencodable(
369
+ `Event ${envelope.id} has a payload field '${field}' that is not an object of top-level fields`
370
+ );
371
+ }
372
+ const fields = Object.entries(others);
373
+ const refused = fields.find((entry) => isRefused(entry, context));
374
+ if (refused !== void 0) {
375
+ throw context.failures.unencodable(
376
+ `Event ${envelope.id} keeps a top-level field '${refused[0]}' under '${field}' that the mapping reads as another value`
377
+ );
378
+ }
379
+ return {
380
+ payload: Object.fromEntries(Object.entries(payload).filter(([name]) => name !== field)),
381
+ fields
382
+ };
383
+ }
384
+ function isRefused([name, value], { plan }) {
385
+ return plan.topLevelNames.has(name) || plan.topLevelHeaders && typeof value === "string";
386
+ }
387
+
220
388
  // src/internal/json-envelope-read.ts
221
389
  var RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
390
+ var EPOCH_PATTERN = /^\d+$/;
391
+ var MILLISECONDS_PER_SECOND = 1e3;
222
392
  function readMessage(message, context) {
223
393
  const { plan, failures } = context;
224
394
  if (plan.payload !== void 0 && !isJsonObject(message.body)) {
@@ -226,7 +396,7 @@ function readMessage(message, context) {
226
396
  }
227
397
  const read = (location) => location === void 0 ? void 0 : readText(message, { location, failures });
228
398
  const fields = {
229
- id: plan.id === void 0 ? void 0 : requireText(read(plan.id), { location: plan.id, failures }),
399
+ id: readId(read(plan.id), context),
230
400
  type: readType(message, context),
231
401
  payload: readPayload(message.body, context),
232
402
  key: read(plan.optional.key),
@@ -234,24 +404,25 @@ function readMessage(message, context) {
234
404
  headers: readEventHeaders(message, plan),
235
405
  correlationId: read(plan.optional.correlationId) ?? null,
236
406
  causationId: read(plan.optional.causationId) ?? null,
237
- occurredAt: readTime(read(plan.optional.occurredAt), failures)
407
+ occurredAt: readTime(read(plan.optional.occurredAt), context)
238
408
  };
239
409
  return Object.fromEntries(
240
410
  Object.entries(fields).filter((entry) => entry[1] !== void 0)
241
411
  );
242
412
  }
413
+ function readId(id, { plan, failures }) {
414
+ if (plan.id === void 0 || plan.idWhenMissing) {
415
+ return id;
416
+ }
417
+ return requireText(id, { location: plan.id, failures });
418
+ }
243
419
  function readType(message, { plan, failures }) {
244
420
  if ("fixed" in plan.type) {
245
421
  return plan.type.fixed;
246
422
  }
247
423
  const location = plan.type;
248
- const type = requireText(readText(message, { location, failures }), { location, failures });
249
- try {
250
- chunkJ76M7XTS_cjs.assertEventType(type);
251
- } catch (error) {
252
- throw failures.invalid(`The event type is not a valid event type: ${error.message}`);
253
- }
254
- return type;
424
+ const wire = requireText(readText(message, { location, failures }), { location, failures });
425
+ return eventTypeOf(wire, { plan, failures });
255
426
  }
256
427
  function readPayload(body, { plan, failures }) {
257
428
  if (plan.payload === void 0) {
@@ -261,25 +432,45 @@ function readPayload(body, { plan, failures }) {
261
432
  if (payload === void 0) {
262
433
  throw failures.invalid(`The message has no payload at '${plan.payload.join(".")}'`);
263
434
  }
264
- return payload;
435
+ return withOtherFields(payload, {
436
+ body,
437
+ context: { plan, failures }
438
+ });
265
439
  }
266
440
  function readEventHeaders(message, plan) {
441
+ if (plan.topLevelHeaders) {
442
+ return textEntries(
443
+ Object.entries(message.body).filter(
444
+ ([name]) => !plan.topLevelNames.has(name)
445
+ )
446
+ );
447
+ }
267
448
  if (plan.headers === void 0) {
268
449
  return Object.fromEntries(
269
450
  Object.entries(message.headers).filter(([name]) => !plan.mappedHeaders.has(name))
270
451
  );
271
452
  }
272
453
  const headers = valueAt(message.body, plan.headers);
273
- return isJsonObject(headers) ? Object.fromEntries(
274
- Object.entries(headers).filter(
275
- (entry) => typeof entry[1] === "string"
276
- )
277
- ) : {};
454
+ return isJsonObject(headers) ? textEntries(Object.entries(headers)) : {};
278
455
  }
279
- function readTime(text, failures) {
456
+ function textEntries(entries) {
457
+ return Object.fromEntries(
458
+ entries.filter((entry) => typeof entry[1] === "string")
459
+ );
460
+ }
461
+ function readTime(text, { plan, failures }) {
280
462
  if (text === void 0) {
281
463
  return void 0;
282
464
  }
465
+ if (plan.timeFormat !== "rfc3339") {
466
+ const time2 = new Date(
467
+ Number(text) * (plan.timeFormat === "epoch-seconds" ? MILLISECONDS_PER_SECOND : 1)
468
+ );
469
+ if (!EPOCH_PATTERN.test(text) || Number.isNaN(time2.getTime())) {
470
+ throw failures.invalid(`The event time is not a whole ${plan.timeFormat} number: '${text}'`);
471
+ }
472
+ return time2;
473
+ }
283
474
  const time = new Date(text);
284
475
  if (!RFC3339_PATTERN.test(text) || Number.isNaN(time.getTime())) {
285
476
  throw failures.invalid(`The event time is not an RFC 3339 timestamp: '${text}'`);
@@ -321,63 +512,135 @@ function valueAt(body, path) {
321
512
 
322
513
  // src/internal/json-envelope-write.ts
323
514
  var MAXIMUM_RFC3339_YEAR = 9999;
515
+ var MILLISECONDS_PER_SECOND2 = 1e3;
324
516
  function writeMessage(envelope, { plan, failures }) {
325
517
  const root = {};
326
518
  const headers = [];
327
519
  const put = (location, value) => {
328
520
  if ("header" in location) {
329
- headers.push([location.header, value]);
521
+ headers.push([location.header, String(value)]);
330
522
  } else {
331
523
  setAt(root, { path: location.path, value });
332
524
  }
333
525
  };
334
- if ("fixed" in plan.type) {
335
- if (envelope.type !== plan.type.fixed) {
336
- throw failures.unencodable(
337
- `Event ${envelope.id} has type '${envelope.type}', but this route carries only '${plan.type.fixed}'`
338
- );
339
- }
340
- } else {
341
- put(plan.type, envelope.type);
526
+ const typeAt = typeLocation(envelope, { plan, failures });
527
+ if (typeAt !== void 0) {
528
+ put(typeAt, wireTypeOf(envelope, { plan, failures }));
342
529
  }
343
530
  if (plan.id !== void 0) {
344
531
  put(plan.id, envelope.id);
345
532
  }
346
- for (const [name, value] of optionalValues(envelope, failures)) {
533
+ for (const [name, value] of optionalValues(envelope, { plan, failures })) {
347
534
  const location = plan.optional[name];
348
535
  if (location !== void 0 && value !== null) {
349
536
  put(location, value);
350
537
  }
351
538
  }
352
539
  return {
353
- body: writeBody(envelope, { plan, root }),
354
- headers: Object.fromEntries([...eventHeaderEntries(envelope, { plan, failures }), ...headers])
540
+ body: writeBody(envelope, { plan, failures, root }),
541
+ headers: Object.fromEntries([
542
+ ...eventHeaderEntries(envelope, { plan, failures }),
543
+ ...headers,
544
+ ...extraHeaderEntries(envelope, plan)
545
+ ])
355
546
  };
356
547
  }
357
- function optionalValues(envelope, failures) {
548
+ function typeLocation(envelope, { plan, failures }) {
549
+ if (!("fixed" in plan.type)) {
550
+ return plan.type;
551
+ }
552
+ if (envelope.type !== plan.type.fixed) {
553
+ throw failures.unencodable(
554
+ `Event ${envelope.id} has type '${envelope.type}', but this route carries only '${plan.type.fixed}'`
555
+ );
556
+ }
557
+ return void 0;
558
+ }
559
+ function extraHeaderEntries(envelope, plan) {
560
+ return plan.extraHeaders.map(([name, value]) => [
561
+ name,
562
+ typeof value === "function" ? value(envelope) : value
563
+ ]);
564
+ }
565
+ function optionalValues(envelope, { plan, failures }) {
358
566
  return [
359
567
  ["key", envelope.key],
360
568
  ["source", envelope.source],
361
- ["occurredAt", formatTime(envelope, failures)],
569
+ [
570
+ "occurredAt",
571
+ timeValue(formatTime(envelope, failures), { envelope, format: plan.timeFormat })
572
+ ],
362
573
  ["correlationId", envelope.correlationId],
363
574
  ["causationId", envelope.causationId]
364
575
  ];
365
576
  }
366
- function writeBody(envelope, { plan, root }) {
577
+ function writeBody(envelope, { plan, failures, root }) {
578
+ const { payload, fields } = splitOtherFields(envelope, { plan, failures });
367
579
  if (plan.payload === void 0) {
368
- return envelope.payload;
580
+ return payload;
369
581
  }
370
- setAt(root, { path: plan.payload, value: envelope.payload });
582
+ setAt(root, { path: plan.payload, value: payload });
371
583
  if (plan.headers !== void 0) {
372
584
  setAt(root, {
373
585
  path: plan.headers,
374
586
  value: Object.fromEntries(Object.entries(envelope.headers))
375
587
  });
376
588
  }
377
- return root;
589
+ for (const { path, value } of plan.extraBody) {
590
+ setAt(root, { path, value: typeof value === "function" ? value(envelope) : value });
591
+ }
592
+ if (plan.topLevelHeaders) {
593
+ spreadHeaders(envelope, { plan, failures, root });
594
+ }
595
+ spreadOtherFields(envelope, { failures, fields, root });
596
+ return ordered(root, plan.bodyOrder);
597
+ }
598
+ function spreadHeaders(envelope, { plan, failures, root }) {
599
+ for (const [name, value] of Object.entries(envelope.headers)) {
600
+ if (plan.topLevelNames.has(name)) {
601
+ throw failures.unencodable(
602
+ `Event ${envelope.id} has a header '${name}', which the mapping uses for another body field`
603
+ );
604
+ }
605
+ setAt(root, { path: [name], value });
606
+ }
607
+ }
608
+ function spreadOtherFields(envelope, {
609
+ failures,
610
+ fields,
611
+ root
612
+ }) {
613
+ for (const [name, value] of fields) {
614
+ if (Object.hasOwn(root, name)) {
615
+ throw failures.unencodable(
616
+ `Event ${envelope.id} keeps a top-level field '${name}' that an event header also writes`
617
+ );
618
+ }
619
+ setAt(root, { path: [name], value });
620
+ }
621
+ }
622
+ function timeValue(text, { envelope, format }) {
623
+ const milliseconds = envelope.occurredAt.getTime();
624
+ if (format === "epoch-millis") {
625
+ return milliseconds;
626
+ }
627
+ return format === "epoch-seconds" ? Math.floor(milliseconds / MILLISECONDS_PER_SECOND2) : text;
628
+ }
629
+ function ordered(root, order) {
630
+ if (order.length === 0) {
631
+ return root;
632
+ }
633
+ const result = {};
634
+ const names = [...order.filter((name) => Object.hasOwn(root, name)), ...Object.keys(root)];
635
+ for (const name of names) {
636
+ if (!Object.hasOwn(result, name)) {
637
+ defineValue(result, { name, value: root[name] });
638
+ }
639
+ }
640
+ return result;
378
641
  }
379
642
  function eventHeaderEntries(envelope, { plan, failures }) {
380
- if (plan.headers !== void 0) {
643
+ if (plan.headers !== void 0 || plan.topLevelHeaders) {
381
644
  return [];
382
645
  }
383
646
  const entries = Object.entries(envelope.headers);