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