@relaybus/core 0.4.0 → 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 CHANGED
@@ -394,6 +394,7 @@ never renamed or reused once released, so branch on them in alerts and retry rul
394
394
  | `RELAYBUS_RABBITMQ_UNROUTABLE_MESSAGE` | `UnroutableMessageError` | `@relaybus/rabbitmq` |
395
395
  | `RELAYBUS_RABBITMQ_MALFORMED_MESSAGE` | `MalformedMessageError` | `@relaybus/rabbitmq` |
396
396
  | `RELAYBUS_RABBITMQ_UNSERIALIZABLE_PAYLOAD` | `UnserializablePayloadError` | `@relaybus/rabbitmq` |
397
+ | `RELAYBUS_TESTING_CODEC_CONTRACT` | `CodecContractError` | `@relaybus/testing` |
397
398
  | `RELAYBUS_RABBITMQ_UNENCODABLE_EVENT` | `RabbitMqUnencodableEventError` | `@relaybus/rabbitmq` |
398
399
  | `RELAYBUS_RABBITMQ_AMQP_LIMIT_EXCEEDED` | `AmqpLimitExceededError` | `@relaybus/rabbitmq` |
399
400
 
@@ -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",
@@ -116,8 +165,9 @@ function compile(mapping, transportIds) {
116
165
  if (topLevelHeaders && payload === void 0) {
117
166
  throw new chunkJ76M7XTS_cjs.ConfigurationError("jsonEnvelope headers 'top-level-fields' needs a body payload");
118
167
  }
168
+ const type = compileType(mapping.type);
119
169
  const plan = {
120
- type: compileType(mapping.type),
170
+ type,
121
171
  id: compileId(mapping.id, transportIds),
122
172
  idWhenMissing: isJsonObject(mapping.id) && "whenMissing" in mapping.id,
123
173
  optional,
@@ -127,6 +177,8 @@ function compile(mapping, transportIds) {
127
177
  topLevelNames: /* @__PURE__ */ new Set(),
128
178
  mappedHeaders: /* @__PURE__ */ new Set(),
129
179
  timeFormat: compileTimeFormat(mapping.occurredAt),
180
+ otherFields: compileOtherFields(mapping, payload !== void 0),
181
+ ...compileWireTypes(mapping.wireTypes, { fixed: "fixed" in type }),
130
182
  ...compileExtras(mapping, payload !== void 0)
131
183
  };
132
184
  return assertDisjoint(plan);
@@ -160,6 +212,20 @@ function compileExtras(mapping, hasBody) {
160
212
  bodyOrder
161
213
  };
162
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
+ }
163
229
  function compileType(type) {
164
230
  if (isJsonObject(type) && typeof type["fixed"] === "string") {
165
231
  chunkJ76M7XTS_cjs.assertEventType(type["fixed"]);
@@ -269,6 +335,56 @@ function assertCompatiblePaths(path, other) {
269
335
  }
270
336
  }
271
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
+
272
388
  // src/internal/json-envelope-read.ts
273
389
  var RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
274
390
  var EPOCH_PATTERN = /^\d+$/;
@@ -305,13 +421,8 @@ function readType(message, { plan, failures }) {
305
421
  return plan.type.fixed;
306
422
  }
307
423
  const location = plan.type;
308
- const type = requireText(readText(message, { location, failures }), { location, failures });
309
- try {
310
- chunkJ76M7XTS_cjs.assertEventType(type);
311
- } catch (error) {
312
- throw failures.invalid(`The event type is not a valid event type: ${error.message}`);
313
- }
314
- return type;
424
+ const wire = requireText(readText(message, { location, failures }), { location, failures });
425
+ return eventTypeOf(wire, { plan, failures });
315
426
  }
316
427
  function readPayload(body, { plan, failures }) {
317
428
  if (plan.payload === void 0) {
@@ -321,7 +432,10 @@ function readPayload(body, { plan, failures }) {
321
432
  if (payload === void 0) {
322
433
  throw failures.invalid(`The message has no payload at '${plan.payload.join(".")}'`);
323
434
  }
324
- return payload;
435
+ return withOtherFields(payload, {
436
+ body,
437
+ context: { plan, failures }
438
+ });
325
439
  }
326
440
  function readEventHeaders(message, plan) {
327
441
  if (plan.topLevelHeaders) {
@@ -411,7 +525,7 @@ function writeMessage(envelope, { plan, failures }) {
411
525
  };
412
526
  const typeAt = typeLocation(envelope, { plan, failures });
413
527
  if (typeAt !== void 0) {
414
- put(typeAt, envelope.type);
528
+ put(typeAt, wireTypeOf(envelope, { plan, failures }));
415
529
  }
416
530
  if (plan.id !== void 0) {
417
531
  put(plan.id, envelope.id);
@@ -422,12 +536,8 @@ function writeMessage(envelope, { plan, failures }) {
422
536
  put(location, value);
423
537
  }
424
538
  }
425
- const body = writeBody(envelope, { plan, root });
426
- if (plan.topLevelHeaders) {
427
- spreadHeaders(envelope, { plan, failures, root: body });
428
- }
429
539
  return {
430
- body,
540
+ body: writeBody(envelope, { plan, failures, root }),
431
541
  headers: Object.fromEntries([
432
542
  ...eventHeaderEntries(envelope, { plan, failures }),
433
543
  ...headers,
@@ -464,11 +574,12 @@ function optionalValues(envelope, { plan, failures }) {
464
574
  ["causationId", envelope.causationId]
465
575
  ];
466
576
  }
467
- function writeBody(envelope, { plan, root }) {
577
+ function writeBody(envelope, { plan, failures, root }) {
578
+ const { payload, fields } = splitOtherFields(envelope, { plan, failures });
468
579
  if (plan.payload === void 0) {
469
- return envelope.payload;
580
+ return payload;
470
581
  }
471
- setAt(root, { path: plan.payload, value: envelope.payload });
582
+ setAt(root, { path: plan.payload, value: payload });
472
583
  if (plan.headers !== void 0) {
473
584
  setAt(root, {
474
585
  path: plan.headers,
@@ -478,6 +589,10 @@ function writeBody(envelope, { plan, root }) {
478
589
  for (const { path, value } of plan.extraBody) {
479
590
  setAt(root, { path, value: typeof value === "function" ? value(envelope) : value });
480
591
  }
592
+ if (plan.topLevelHeaders) {
593
+ spreadHeaders(envelope, { plan, failures, root });
594
+ }
595
+ spreadOtherFields(envelope, { failures, fields, root });
481
596
  return ordered(root, plan.bodyOrder);
482
597
  }
483
598
  function spreadHeaders(envelope, { plan, failures, root }) {
@@ -490,6 +605,20 @@ function spreadHeaders(envelope, { plan, failures, root }) {
490
605
  setAt(root, { path: [name], value });
491
606
  }
492
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
+ }
493
622
  function timeValue(text, { envelope, format }) {
494
623
  const milliseconds = envelope.occurredAt.getTime();
495
624
  if (format === "epoch-millis") {