@relaybus/core 0.3.1 → 0.4.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.
@@ -98,6 +98,7 @@ var OPTIONAL_NAMES = [
98
98
  "correlationId",
99
99
  "causationId"
100
100
  ];
101
+ var TIME_FORMATS = ["rfc3339", "epoch-millis", "epoch-seconds"];
101
102
  var ARRAY_INDEX = /^(?:0|[1-9]\d*)$/;
102
103
  function compile(mapping, transportIds) {
103
104
  if (!isJsonObject(mapping)) {
@@ -110,16 +111,55 @@ function compile(mapping, transportIds) {
110
111
  optional[name] = toLocation(field, name);
111
112
  }
112
113
  }
114
+ const payload = compilePayload(mapping.payload);
115
+ const topLevelHeaders = mapping.headers === "top-level-fields";
116
+ if (topLevelHeaders && payload === void 0) {
117
+ throw new chunkJ76M7XTS_cjs.ConfigurationError("jsonEnvelope headers 'top-level-fields' needs a body payload");
118
+ }
113
119
  const plan = {
114
120
  type: compileType(mapping.type),
115
121
  id: compileId(mapping.id, transportIds),
122
+ idWhenMissing: isJsonObject(mapping.id) && "whenMissing" in mapping.id,
116
123
  optional,
117
- payload: compilePayload(mapping.payload),
118
- headers: compileHeaders(mapping.headers),
119
- mappedHeaders: /* @__PURE__ */ new Set()
124
+ payload,
125
+ headers: topLevelHeaders ? void 0 : compileHeaders(mapping.headers),
126
+ topLevelHeaders,
127
+ topLevelNames: /* @__PURE__ */ new Set(),
128
+ mappedHeaders: /* @__PURE__ */ new Set(),
129
+ timeFormat: compileTimeFormat(mapping.occurredAt),
130
+ ...compileExtras(mapping, payload !== void 0)
120
131
  };
121
132
  return assertDisjoint(plan);
122
133
  }
134
+ function compileTimeFormat(field) {
135
+ const format = isJsonObject(field) ? field["format"] : void 0;
136
+ if (format === void 0) {
137
+ return "rfc3339";
138
+ }
139
+ if (!TIME_FORMATS.includes(format)) {
140
+ throw new chunkJ76M7XTS_cjs.ConfigurationError(
141
+ `jsonEnvelope occurredAt format must be one of ${TIME_FORMATS.map((name) => `'${name}'`).join(", ")}`
142
+ );
143
+ }
144
+ return format;
145
+ }
146
+ function compileExtras(mapping, hasBody) {
147
+ const extra = mapping.extra ?? {};
148
+ const bodyOrder = mapping.bodyOrder ?? [];
149
+ if (!hasBody && (extra.body !== void 0 || bodyOrder.length > 0)) {
150
+ throw new chunkJ76M7XTS_cjs.ConfigurationError(
151
+ "jsonEnvelope with a whole-body payload has no body for extra values or a body order"
152
+ );
153
+ }
154
+ return {
155
+ extraBody: Object.entries(extra.body ?? {}).map(([path, value]) => ({
156
+ path: toPath(path, "extra body value"),
157
+ value
158
+ })),
159
+ extraHeaders: Object.entries(extra.headers ?? {}),
160
+ bodyOrder
161
+ };
162
+ }
123
163
  function compileType(type) {
124
164
  if (isJsonObject(type) && typeof type["fixed"] === "string") {
125
165
  chunkJ76M7XTS_cjs.assertEventType(type["fixed"]);
@@ -134,11 +174,15 @@ function compileId(id, transportIds) {
134
174
  if (typeof id === "string" && transportIds.includes(id)) {
135
175
  return void 0;
136
176
  }
177
+ const names = transportIds.map((name) => `'${name}'`).join(", ");
137
178
  if (!isJsonObject(id)) {
138
179
  throw new chunkJ76M7XTS_cjs.ConfigurationError(
139
- `jsonEnvelope needs an id: { body }, { header } or one of ${transportIds.map((name) => `'${name}'`).join(", ")}`
180
+ `jsonEnvelope needs an id: { body }, { header } or one of ${names}`
140
181
  );
141
182
  }
183
+ if ("whenMissing" in id && !transportIds.includes(id["whenMissing"])) {
184
+ throw new chunkJ76M7XTS_cjs.ConfigurationError(`jsonEnvelope id whenMissing must be one of ${names}`);
185
+ }
142
186
  return toLocation(id, "id");
143
187
  }
144
188
  function compilePayload(payload) {
@@ -187,17 +231,25 @@ function assertDisjoint(plan) {
187
231
  const locations = [plan.type, plan.id, ...Object.values(plan.optional)].filter(
188
232
  (location) => location !== void 0 && !("fixed" in location)
189
233
  );
190
- const headers = locations.flatMap((location) => "header" in location ? [location.header] : []);
234
+ const headers = [
235
+ ...locations.flatMap((location) => "header" in location ? [location.header] : []),
236
+ ...plan.extraHeaders.map(([name]) => name)
237
+ ];
191
238
  const duplicate = headers.find((name, index) => headers.indexOf(name) !== index);
192
239
  if (duplicate !== void 0) {
193
240
  throw new chunkJ76M7XTS_cjs.ConfigurationError(`jsonEnvelope maps header '${duplicate}' twice`);
194
241
  }
195
242
  const paths = [
196
243
  ...locations.flatMap((location) => "path" in location ? [location.path] : []),
197
- ...[plan.payload, plan.headers].filter((path) => path !== void 0)
244
+ ...[plan.payload, plan.headers].filter((path) => path !== void 0),
245
+ ...plan.extraBody.map(({ path }) => path)
198
246
  ];
199
247
  assertNoNestedPaths(paths);
200
- return { ...plan, mappedHeaders: new Set(headers) };
248
+ return {
249
+ ...plan,
250
+ mappedHeaders: new Set(headers),
251
+ topLevelNames: new Set(paths.map((path) => path[0]))
252
+ };
201
253
  }
202
254
  function assertNoNestedPaths(paths) {
203
255
  paths.forEach((path, index) => {
@@ -219,6 +271,8 @@ function assertCompatiblePaths(path, other) {
219
271
 
220
272
  // src/internal/json-envelope-read.ts
221
273
  var RFC3339_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/i;
274
+ var EPOCH_PATTERN = /^\d+$/;
275
+ var MILLISECONDS_PER_SECOND = 1e3;
222
276
  function readMessage(message, context) {
223
277
  const { plan, failures } = context;
224
278
  if (plan.payload !== void 0 && !isJsonObject(message.body)) {
@@ -226,7 +280,7 @@ function readMessage(message, context) {
226
280
  }
227
281
  const read = (location) => location === void 0 ? void 0 : readText(message, { location, failures });
228
282
  const fields = {
229
- id: plan.id === void 0 ? void 0 : requireText(read(plan.id), { location: plan.id, failures }),
283
+ id: readId(read(plan.id), context),
230
284
  type: readType(message, context),
231
285
  payload: readPayload(message.body, context),
232
286
  key: read(plan.optional.key),
@@ -234,12 +288,18 @@ function readMessage(message, context) {
234
288
  headers: readEventHeaders(message, plan),
235
289
  correlationId: read(plan.optional.correlationId) ?? null,
236
290
  causationId: read(plan.optional.causationId) ?? null,
237
- occurredAt: readTime(read(plan.optional.occurredAt), failures)
291
+ occurredAt: readTime(read(plan.optional.occurredAt), context)
238
292
  };
239
293
  return Object.fromEntries(
240
294
  Object.entries(fields).filter((entry) => entry[1] !== void 0)
241
295
  );
242
296
  }
297
+ function readId(id, { plan, failures }) {
298
+ if (plan.id === void 0 || plan.idWhenMissing) {
299
+ return id;
300
+ }
301
+ return requireText(id, { location: plan.id, failures });
302
+ }
243
303
  function readType(message, { plan, failures }) {
244
304
  if ("fixed" in plan.type) {
245
305
  return plan.type.fixed;
@@ -264,22 +324,39 @@ function readPayload(body, { plan, failures }) {
264
324
  return payload;
265
325
  }
266
326
  function readEventHeaders(message, plan) {
327
+ if (plan.topLevelHeaders) {
328
+ return textEntries(
329
+ Object.entries(message.body).filter(
330
+ ([name]) => !plan.topLevelNames.has(name)
331
+ )
332
+ );
333
+ }
267
334
  if (plan.headers === void 0) {
268
335
  return Object.fromEntries(
269
336
  Object.entries(message.headers).filter(([name]) => !plan.mappedHeaders.has(name))
270
337
  );
271
338
  }
272
339
  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
- ) : {};
340
+ return isJsonObject(headers) ? textEntries(Object.entries(headers)) : {};
341
+ }
342
+ function textEntries(entries) {
343
+ return Object.fromEntries(
344
+ entries.filter((entry) => typeof entry[1] === "string")
345
+ );
278
346
  }
279
- function readTime(text, failures) {
347
+ function readTime(text, { plan, failures }) {
280
348
  if (text === void 0) {
281
349
  return void 0;
282
350
  }
351
+ if (plan.timeFormat !== "rfc3339") {
352
+ const time2 = new Date(
353
+ Number(text) * (plan.timeFormat === "epoch-seconds" ? MILLISECONDS_PER_SECOND : 1)
354
+ );
355
+ if (!EPOCH_PATTERN.test(text) || Number.isNaN(time2.getTime())) {
356
+ throw failures.invalid(`The event time is not a whole ${plan.timeFormat} number: '${text}'`);
357
+ }
358
+ return time2;
359
+ }
283
360
  const time = new Date(text);
284
361
  if (!RFC3339_PATTERN.test(text) || Number.isNaN(time.getTime())) {
285
362
  throw failures.invalid(`The event time is not an RFC 3339 timestamp: '${text}'`);
@@ -321,44 +398,68 @@ function valueAt(body, path) {
321
398
 
322
399
  // src/internal/json-envelope-write.ts
323
400
  var MAXIMUM_RFC3339_YEAR = 9999;
401
+ var MILLISECONDS_PER_SECOND2 = 1e3;
324
402
  function writeMessage(envelope, { plan, failures }) {
325
403
  const root = {};
326
404
  const headers = [];
327
405
  const put = (location, value) => {
328
406
  if ("header" in location) {
329
- headers.push([location.header, value]);
407
+ headers.push([location.header, String(value)]);
330
408
  } else {
331
409
  setAt(root, { path: location.path, value });
332
410
  }
333
411
  };
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);
412
+ const typeAt = typeLocation(envelope, { plan, failures });
413
+ if (typeAt !== void 0) {
414
+ put(typeAt, envelope.type);
342
415
  }
343
416
  if (plan.id !== void 0) {
344
417
  put(plan.id, envelope.id);
345
418
  }
346
- for (const [name, value] of optionalValues(envelope, failures)) {
419
+ for (const [name, value] of optionalValues(envelope, { plan, failures })) {
347
420
  const location = plan.optional[name];
348
421
  if (location !== void 0 && value !== null) {
349
422
  put(location, value);
350
423
  }
351
424
  }
425
+ const body = writeBody(envelope, { plan, root });
426
+ if (plan.topLevelHeaders) {
427
+ spreadHeaders(envelope, { plan, failures, root: body });
428
+ }
352
429
  return {
353
- body: writeBody(envelope, { plan, root }),
354
- headers: Object.fromEntries([...eventHeaderEntries(envelope, { plan, failures }), ...headers])
430
+ body,
431
+ headers: Object.fromEntries([
432
+ ...eventHeaderEntries(envelope, { plan, failures }),
433
+ ...headers,
434
+ ...extraHeaderEntries(envelope, plan)
435
+ ])
355
436
  };
356
437
  }
357
- function optionalValues(envelope, failures) {
438
+ function typeLocation(envelope, { plan, failures }) {
439
+ if (!("fixed" in plan.type)) {
440
+ return plan.type;
441
+ }
442
+ if (envelope.type !== plan.type.fixed) {
443
+ throw failures.unencodable(
444
+ `Event ${envelope.id} has type '${envelope.type}', but this route carries only '${plan.type.fixed}'`
445
+ );
446
+ }
447
+ return void 0;
448
+ }
449
+ function extraHeaderEntries(envelope, plan) {
450
+ return plan.extraHeaders.map(([name, value]) => [
451
+ name,
452
+ typeof value === "function" ? value(envelope) : value
453
+ ]);
454
+ }
455
+ function optionalValues(envelope, { plan, failures }) {
358
456
  return [
359
457
  ["key", envelope.key],
360
458
  ["source", envelope.source],
361
- ["occurredAt", formatTime(envelope, failures)],
459
+ [
460
+ "occurredAt",
461
+ timeValue(formatTime(envelope, failures), { envelope, format: plan.timeFormat })
462
+ ],
362
463
  ["correlationId", envelope.correlationId],
363
464
  ["causationId", envelope.causationId]
364
465
  ];
@@ -374,10 +475,43 @@ function writeBody(envelope, { plan, root }) {
374
475
  value: Object.fromEntries(Object.entries(envelope.headers))
375
476
  });
376
477
  }
377
- return root;
478
+ for (const { path, value } of plan.extraBody) {
479
+ setAt(root, { path, value: typeof value === "function" ? value(envelope) : value });
480
+ }
481
+ return ordered(root, plan.bodyOrder);
482
+ }
483
+ function spreadHeaders(envelope, { plan, failures, root }) {
484
+ for (const [name, value] of Object.entries(envelope.headers)) {
485
+ if (plan.topLevelNames.has(name)) {
486
+ throw failures.unencodable(
487
+ `Event ${envelope.id} has a header '${name}', which the mapping uses for another body field`
488
+ );
489
+ }
490
+ setAt(root, { path: [name], value });
491
+ }
492
+ }
493
+ function timeValue(text, { envelope, format }) {
494
+ const milliseconds = envelope.occurredAt.getTime();
495
+ if (format === "epoch-millis") {
496
+ return milliseconds;
497
+ }
498
+ return format === "epoch-seconds" ? Math.floor(milliseconds / MILLISECONDS_PER_SECOND2) : text;
499
+ }
500
+ function ordered(root, order) {
501
+ if (order.length === 0) {
502
+ return root;
503
+ }
504
+ const result = {};
505
+ const names = [...order.filter((name) => Object.hasOwn(root, name)), ...Object.keys(root)];
506
+ for (const name of names) {
507
+ if (!Object.hasOwn(result, name)) {
508
+ defineValue(result, { name, value: root[name] });
509
+ }
510
+ }
511
+ return result;
378
512
  }
379
513
  function eventHeaderEntries(envelope, { plan, failures }) {
380
- if (plan.headers !== void 0) {
514
+ if (plan.headers !== void 0 || plan.topLevelHeaders) {
381
515
  return [];
382
516
  }
383
517
  const entries = Object.entries(envelope.headers);