@hasna/events 0.1.12 → 0.1.14
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 +149 -17
- package/dist/catalog.d.ts +136 -0
- package/dist/catalog.js +191 -0
- package/dist/cli/index.js +301 -63
- package/dist/commander.d.ts +16 -2
- package/dist/commander.js +449 -62
- package/dist/index.d.ts +21 -6
- package/dist/index.js +406 -26
- package/dist/storage.d.ts +16 -3
- package/dist/storage.js +140 -4
- package/dist/types.d.ts +55 -2
- package/package.json +8 -4
package/dist/cli/index.js
CHANGED
|
@@ -108,11 +108,15 @@ function channelMatchesEvent(channel, event) {
|
|
|
108
108
|
|
|
109
109
|
// src/storage.ts
|
|
110
110
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
111
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
111
112
|
import { existsSync } from "fs";
|
|
112
113
|
import { homedir } from "os";
|
|
113
114
|
import { join } from "path";
|
|
114
115
|
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
115
116
|
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
117
|
+
var LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:";
|
|
118
|
+
var DEFAULT_EVENT_PAGE_LIMIT = 100;
|
|
119
|
+
var MAX_EVENT_PAGE_LIMIT = 1000;
|
|
116
120
|
function getEventsDataDir(override) {
|
|
117
121
|
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
|
|
118
122
|
}
|
|
@@ -126,11 +130,13 @@ function getActiveEventsDirEnv() {
|
|
|
126
130
|
|
|
127
131
|
class JsonEventsStore {
|
|
128
132
|
dataDir;
|
|
133
|
+
runtime;
|
|
129
134
|
channelsPath;
|
|
130
135
|
eventsPath;
|
|
131
136
|
deliveriesPath;
|
|
132
137
|
constructor(dataDir = getEventsDataDir()) {
|
|
133
138
|
this.dataDir = dataDir;
|
|
139
|
+
this.runtime = localJsonRuntime(dataDir);
|
|
134
140
|
this.channelsPath = join(dataDir, "channels.json");
|
|
135
141
|
this.eventsPath = join(dataDir, "events.json");
|
|
136
142
|
this.deliveriesPath = join(dataDir, "deliveries.json");
|
|
@@ -178,13 +184,58 @@ class JsonEventsStore {
|
|
|
178
184
|
await this.writeJson(this.eventsPath, events);
|
|
179
185
|
return event;
|
|
180
186
|
}
|
|
181
|
-
async
|
|
187
|
+
async appendEventOnce(event, options = {}) {
|
|
182
188
|
await this.init();
|
|
183
|
-
|
|
189
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
190
|
+
const dedupe = options.dedupe !== false;
|
|
191
|
+
if (dedupe) {
|
|
192
|
+
const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
|
|
193
|
+
if (existing) {
|
|
194
|
+
return {
|
|
195
|
+
event: existing,
|
|
196
|
+
stored: false,
|
|
197
|
+
deduped: true,
|
|
198
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
events.push(event);
|
|
203
|
+
await this.writeJson(this.eventsPath, events);
|
|
204
|
+
return {
|
|
205
|
+
event,
|
|
206
|
+
stored: true,
|
|
207
|
+
deduped: false,
|
|
208
|
+
identity: { id: event.id, dedupeKey: event.dedupeKey }
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async listEvents(options = {}) {
|
|
212
|
+
await this.init();
|
|
213
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
214
|
+
return queryEvents(events, options);
|
|
215
|
+
}
|
|
216
|
+
async listEventsPage(options = {}) {
|
|
217
|
+
await this.init();
|
|
218
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
219
|
+
const queried = queryEvents(events, {
|
|
220
|
+
eventId: options.eventId,
|
|
221
|
+
source: options.source,
|
|
222
|
+
type: options.type
|
|
223
|
+
});
|
|
224
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
225
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
226
|
+
const pageEvents = queried.slice(offset, offset + limit);
|
|
227
|
+
const nextOffset = offset + pageEvents.length;
|
|
228
|
+
const hasMore = nextOffset < queried.length;
|
|
229
|
+
return {
|
|
230
|
+
events: pageEvents,
|
|
231
|
+
cursor: options.cursor,
|
|
232
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
233
|
+
hasMore
|
|
234
|
+
};
|
|
184
235
|
}
|
|
185
236
|
async findEventByIdentity(identity) {
|
|
186
237
|
const events = await this.listEvents();
|
|
187
|
-
return events
|
|
238
|
+
return findEventByIdentity(events, identity);
|
|
188
239
|
}
|
|
189
240
|
async appendDelivery(result) {
|
|
190
241
|
await this.init();
|
|
@@ -235,6 +286,83 @@ class JsonEventsStore {
|
|
|
235
286
|
});
|
|
236
287
|
}
|
|
237
288
|
}
|
|
289
|
+
function localJsonRuntime(dataDir = getEventsDataDir()) {
|
|
290
|
+
return {
|
|
291
|
+
mode: "local-files",
|
|
292
|
+
name: "json-events-store",
|
|
293
|
+
remote: false,
|
|
294
|
+
localFiles: true,
|
|
295
|
+
localSqlite: false,
|
|
296
|
+
postgres: false,
|
|
297
|
+
s3: false,
|
|
298
|
+
aws: false,
|
|
299
|
+
durable: true,
|
|
300
|
+
idempotency: "best-effort-local",
|
|
301
|
+
replayCursors: true,
|
|
302
|
+
description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function encodeLocalJsonEventCursor(offset, options = {}) {
|
|
306
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
307
|
+
throw new Error(`Invalid event cursor offset: ${offset}`);
|
|
308
|
+
const payload = {
|
|
309
|
+
offset,
|
|
310
|
+
eventId: options.eventId,
|
|
311
|
+
source: options.source,
|
|
312
|
+
type: options.type
|
|
313
|
+
};
|
|
314
|
+
return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
|
|
315
|
+
}
|
|
316
|
+
function decodeLocalJsonEventCursor(cursor, options = {}) {
|
|
317
|
+
if (!cursor)
|
|
318
|
+
return 0;
|
|
319
|
+
if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
|
|
320
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
321
|
+
const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
|
|
322
|
+
let payload;
|
|
323
|
+
try {
|
|
324
|
+
payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
|
|
325
|
+
} catch {
|
|
326
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
327
|
+
}
|
|
328
|
+
const offset = payload.offset;
|
|
329
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
330
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
331
|
+
assertCursorFilter("eventId", payload.eventId, options.eventId);
|
|
332
|
+
assertCursorFilter("source", payload.source, options.source);
|
|
333
|
+
assertCursorFilter("type", payload.type, options.type);
|
|
334
|
+
return offset;
|
|
335
|
+
}
|
|
336
|
+
function normalizeEventPageLimit(limit) {
|
|
337
|
+
if (limit === undefined)
|
|
338
|
+
return DEFAULT_EVENT_PAGE_LIMIT;
|
|
339
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
340
|
+
throw new Error(`Event page limit must be a positive integer, got ${limit}`);
|
|
341
|
+
return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
|
|
342
|
+
}
|
|
343
|
+
function queryEvents(events, options) {
|
|
344
|
+
let rows = events;
|
|
345
|
+
if (options.eventId)
|
|
346
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
347
|
+
if (options.source)
|
|
348
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
349
|
+
if (options.type)
|
|
350
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
351
|
+
if (options.cursor) {
|
|
352
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
353
|
+
rows = rows.slice(offset);
|
|
354
|
+
}
|
|
355
|
+
if (options.limit !== undefined)
|
|
356
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
357
|
+
return rows;
|
|
358
|
+
}
|
|
359
|
+
function assertCursorFilter(name, cursorValue, optionValue) {
|
|
360
|
+
if (cursorValue !== optionValue)
|
|
361
|
+
throw new Error(`Local JSON event cursor ${name} filter mismatch`);
|
|
362
|
+
}
|
|
363
|
+
function findEventByIdentity(events, identity) {
|
|
364
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
365
|
+
}
|
|
238
366
|
async function getEventsStatus(dataDir) {
|
|
239
367
|
const store = new JsonEventsStore(dataDir);
|
|
240
368
|
await store.init();
|
|
@@ -251,6 +379,7 @@ async function getEventsStatus(dataDir) {
|
|
|
251
379
|
service: "events",
|
|
252
380
|
schemaVersion: "1.0",
|
|
253
381
|
dataDir: store.dataDir,
|
|
382
|
+
storage: store.runtime,
|
|
254
383
|
env: {
|
|
255
384
|
primary: HASNA_EVENTS_DIR_ENV,
|
|
256
385
|
fallback: HASNA_EVENTS_HOME_ENV,
|
|
@@ -447,6 +576,52 @@ function createDeliveryResult(event, channel, attempts) {
|
|
|
447
576
|
};
|
|
448
577
|
}
|
|
449
578
|
|
|
579
|
+
// src/catalog.ts
|
|
580
|
+
class EventValidationError extends Error {
|
|
581
|
+
eventType;
|
|
582
|
+
issues;
|
|
583
|
+
constructor(eventType, issues) {
|
|
584
|
+
const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
|
|
585
|
+
super(`Event validation failed for type "${eventType}": ${detail}`);
|
|
586
|
+
this.name = "EventValidationError";
|
|
587
|
+
this.eventType = eventType;
|
|
588
|
+
this.issues = issues;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
class EventTypeCatalog {
|
|
593
|
+
definitions = new Map;
|
|
594
|
+
register(definition) {
|
|
595
|
+
this.definitions.set(definition.type, definition);
|
|
596
|
+
return this;
|
|
597
|
+
}
|
|
598
|
+
unregister(type) {
|
|
599
|
+
return this.definitions.delete(type);
|
|
600
|
+
}
|
|
601
|
+
has(type) {
|
|
602
|
+
return this.definitions.has(type);
|
|
603
|
+
}
|
|
604
|
+
get(type) {
|
|
605
|
+
return this.definitions.get(type);
|
|
606
|
+
}
|
|
607
|
+
list() {
|
|
608
|
+
return [...this.definitions.values()];
|
|
609
|
+
}
|
|
610
|
+
validateEvent(event) {
|
|
611
|
+
const definition = this.definitions.get(event.type);
|
|
612
|
+
if (!definition)
|
|
613
|
+
return { ok: true };
|
|
614
|
+
return definition.validate(event.data, event);
|
|
615
|
+
}
|
|
616
|
+
assertEventValid(event) {
|
|
617
|
+
const result = this.validateEvent(event);
|
|
618
|
+
if (!result.ok) {
|
|
619
|
+
throw new EventValidationError(event.type, result.issues);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
var defaultEventTypeCatalog = new EventTypeCatalog;
|
|
624
|
+
|
|
450
625
|
// src/index.ts
|
|
451
626
|
function createEvent(input) {
|
|
452
627
|
return {
|
|
@@ -468,10 +643,14 @@ class EventsClient {
|
|
|
468
643
|
store;
|
|
469
644
|
redactors;
|
|
470
645
|
transportOptions;
|
|
646
|
+
catalog;
|
|
647
|
+
validateCatalogTypes;
|
|
471
648
|
constructor(options = {}) {
|
|
472
649
|
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
473
650
|
this.redactors = options.redactors ?? [];
|
|
474
651
|
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
652
|
+
this.catalog = options.catalog ?? defaultEventTypeCatalog;
|
|
653
|
+
this.validateCatalogTypes = options.validateCatalogTypes ?? false;
|
|
475
654
|
}
|
|
476
655
|
async addChannel(input) {
|
|
477
656
|
const timestamp = new Date().toISOString();
|
|
@@ -489,18 +668,40 @@ class EventsClient {
|
|
|
489
668
|
}
|
|
490
669
|
async emit(input, options = {}) {
|
|
491
670
|
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
492
|
-
if (options.
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
671
|
+
if (options.validate ?? this.validateCatalogTypes) {
|
|
672
|
+
this.catalog.assertEventValid(event);
|
|
673
|
+
}
|
|
674
|
+
const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
|
|
675
|
+
if (append.deduped) {
|
|
676
|
+
return { event: append.event, deliveries: [], deduped: true };
|
|
497
677
|
}
|
|
498
|
-
await this.
|
|
499
|
-
|
|
500
|
-
return { event, deliveries, deduped: false };
|
|
678
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
|
|
679
|
+
return { event: append.event, deliveries, deduped: false };
|
|
501
680
|
}
|
|
502
|
-
async listEvents() {
|
|
503
|
-
|
|
681
|
+
async listEvents(options = {}) {
|
|
682
|
+
if (Object.keys(options).length === 0)
|
|
683
|
+
return this.store.listEvents();
|
|
684
|
+
return queryClientEvents(await this.store.listEvents(), options);
|
|
685
|
+
}
|
|
686
|
+
async listEventsPage(options = {}) {
|
|
687
|
+
if (this.store.listEventsPage)
|
|
688
|
+
return this.store.listEventsPage(options);
|
|
689
|
+
const events = queryClientEvents(await this.store.listEvents(), {
|
|
690
|
+
eventId: options.eventId,
|
|
691
|
+
source: options.source,
|
|
692
|
+
type: options.type
|
|
693
|
+
});
|
|
694
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
695
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
696
|
+
const pageEvents = events.slice(offset, offset + limit);
|
|
697
|
+
const nextOffset = offset + pageEvents.length;
|
|
698
|
+
const hasMore = nextOffset < events.length;
|
|
699
|
+
return {
|
|
700
|
+
events: pageEvents,
|
|
701
|
+
cursor: options.cursor,
|
|
702
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
703
|
+
hasMore
|
|
704
|
+
};
|
|
504
705
|
}
|
|
505
706
|
async listDeliveries() {
|
|
506
707
|
return this.store.listDeliveries();
|
|
@@ -568,22 +769,37 @@ class EventsClient {
|
|
|
568
769
|
return result;
|
|
569
770
|
}
|
|
570
771
|
async replay(options = {}) {
|
|
571
|
-
const
|
|
572
|
-
if (options.eventId && event.id !== options.eventId)
|
|
573
|
-
return false;
|
|
574
|
-
if (options.source && event.source !== options.source)
|
|
575
|
-
return false;
|
|
576
|
-
if (options.type && event.type !== options.type)
|
|
577
|
-
return false;
|
|
578
|
-
return true;
|
|
579
|
-
});
|
|
772
|
+
const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
|
|
580
773
|
if (options.dryRun)
|
|
581
|
-
return { events, deliveries: [] };
|
|
774
|
+
return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
582
775
|
const deliveries = [];
|
|
583
|
-
for (const event of events) {
|
|
776
|
+
for (const event of page.events) {
|
|
584
777
|
deliveries.push(...await this.deliver(event));
|
|
585
778
|
}
|
|
586
|
-
return { events, deliveries };
|
|
779
|
+
return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
780
|
+
}
|
|
781
|
+
async appendEvent(event, options) {
|
|
782
|
+
if (this.store.appendEventOnce) {
|
|
783
|
+
return this.store.appendEventOnce(event, { dedupe: options.dedupe });
|
|
784
|
+
}
|
|
785
|
+
if (options.dedupe) {
|
|
786
|
+
const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
|
|
787
|
+
if (existing) {
|
|
788
|
+
return {
|
|
789
|
+
event: existing,
|
|
790
|
+
stored: false,
|
|
791
|
+
deduped: true,
|
|
792
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const stored = await this.store.appendEvent(event);
|
|
797
|
+
return {
|
|
798
|
+
event: stored,
|
|
799
|
+
stored: true,
|
|
800
|
+
deduped: false,
|
|
801
|
+
identity: { id: stored.id, dedupeKey: stored.dedupeKey }
|
|
802
|
+
};
|
|
587
803
|
}
|
|
588
804
|
async applyRedaction(event, channel) {
|
|
589
805
|
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
@@ -660,6 +876,20 @@ function setPath(input, path, replacement) {
|
|
|
660
876
|
if (last && last in cursor)
|
|
661
877
|
cursor[last] = replacement;
|
|
662
878
|
}
|
|
879
|
+
function queryClientEvents(events, options) {
|
|
880
|
+
let rows = events;
|
|
881
|
+
if (options.eventId)
|
|
882
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
883
|
+
if (options.source)
|
|
884
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
885
|
+
if (options.type)
|
|
886
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
887
|
+
if (options.cursor)
|
|
888
|
+
rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
|
|
889
|
+
if (options.limit !== undefined)
|
|
890
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
891
|
+
return rows;
|
|
892
|
+
}
|
|
663
893
|
function normalizeTime(value) {
|
|
664
894
|
if (!value)
|
|
665
895
|
return new Date().toISOString();
|
|
@@ -862,31 +1092,31 @@ function printHelp(options = {}) {
|
|
|
862
1092
|
console.log(`${name} ${version()}
|
|
863
1093
|
|
|
864
1094
|
Usage:
|
|
865
|
-
${name} [--dir <path>] [--json]
|
|
866
|
-
${name} [--dir <path>] [--json]
|
|
867
|
-
${name} [--dir <path>] [--json]
|
|
868
|
-
${name} [--dir <path>] [--json]
|
|
869
|
-
${name} [--dir <path>] [--json]
|
|
870
|
-
${name} [--dir <path>] [--json]
|
|
1095
|
+
${name} [--dir <path>] [--json] channels add <url|command> [options]
|
|
1096
|
+
${name} [--dir <path>] [--json] channels list
|
|
1097
|
+
${name} [--dir <path>] [--json] channels remove <id>
|
|
1098
|
+
${name} [--dir <path>] [--json] channels test <id>
|
|
1099
|
+
${name} [--dir <path>] [--json] channels match <id>
|
|
1100
|
+
${name} [--dir <path>] [--json] channels status
|
|
871
1101
|
${name} [--dir <path>] [--json] status
|
|
872
1102
|
${name} [--dir <path>] [--json] events emit <type>${options.source ? "" : " --source <source>"} [options]
|
|
873
1103
|
${name} [--dir <path>] [--json] events list [--limit <n>]
|
|
874
|
-
${name} [--dir <path>] [--json] events replay [--id <event-id>] [--dry-run]
|
|
1104
|
+
${name} [--dir <path>] [--json] events replay [--id <event-id>] [--cursor <cursor>] [--limit <n>] [--dry-run]
|
|
875
1105
|
|
|
876
1106
|
Environment:
|
|
877
1107
|
HASNA_EVENTS_DIR or HASNA_EVENTS_HOME overrides the default ${getEventsDataDir()}`);
|
|
878
1108
|
}
|
|
879
|
-
function
|
|
1109
|
+
function printChannelsHelp(options = {}) {
|
|
880
1110
|
const name = commandName(options);
|
|
881
|
-
console.log(`${name}
|
|
1111
|
+
console.log(`${name} channels
|
|
882
1112
|
|
|
883
1113
|
Usage:
|
|
884
|
-
${name} [--dir <path>] [--json]
|
|
885
|
-
${name} [--dir <path>] [--json]
|
|
886
|
-
${name} [--dir <path>] [--json]
|
|
887
|
-
${name} [--dir <path>] [--json]
|
|
888
|
-
${name} [--dir <path>] [--json]
|
|
889
|
-
${name} [--dir <path>] [--json]
|
|
1114
|
+
${name} [--dir <path>] [--json] channels add <url|command> [options]
|
|
1115
|
+
${name} [--dir <path>] [--json] channels list
|
|
1116
|
+
${name} [--dir <path>] [--json] channels remove <id>
|
|
1117
|
+
${name} [--dir <path>] [--json] channels test <id>
|
|
1118
|
+
${name} [--dir <path>] [--json] channels match <id>
|
|
1119
|
+
${name} [--dir <path>] [--json] channels status
|
|
890
1120
|
|
|
891
1121
|
Options:
|
|
892
1122
|
--id <id> Channel id for add
|
|
@@ -898,20 +1128,20 @@ Options:
|
|
|
898
1128
|
--metadata <path=value|path!=value> Event metadata field filter, repeatable; strings, dot paths, array-member matching, * segment wildcard, ** recursive wildcard
|
|
899
1129
|
--data-json <path=json|path!=json> Event data field filter with typed JSON value
|
|
900
1130
|
--metadata-json <path=json|path!=json> Event metadata field filter with typed JSON value
|
|
901
|
-
--honor-filters On
|
|
1131
|
+
--honor-filters On channels test, skip delivery when the sample event does not match filters
|
|
902
1132
|
--transport <kind> webhook or command
|
|
903
1133
|
--secret <secret> Webhook signing secret
|
|
904
1134
|
--header <name=value> Webhook header, repeatable
|
|
905
1135
|
--redact <path> Redaction path, repeatable
|
|
906
1136
|
--no-deliver Available on events emit`);
|
|
907
1137
|
}
|
|
908
|
-
function
|
|
1138
|
+
function printChannelAddHelp(options = {}) {
|
|
909
1139
|
const name = commandName(options);
|
|
910
|
-
console.log(`${name}
|
|
1140
|
+
console.log(`${name} channels add
|
|
911
1141
|
|
|
912
1142
|
Usage:
|
|
913
|
-
${name} [--dir <path>] [--json]
|
|
914
|
-
${name} [--dir <path>] [--json]
|
|
1143
|
+
${name} [--dir <path>] [--json] channels add <url|command> [options]
|
|
1144
|
+
${name} [--dir <path>] [--json] channels add <command> --transport command [options] -- [command-args...]
|
|
915
1145
|
|
|
916
1146
|
Options:
|
|
917
1147
|
--id <id> Channel id
|
|
@@ -935,10 +1165,10 @@ Options:
|
|
|
935
1165
|
--disabled Create channel disabled
|
|
936
1166
|
|
|
937
1167
|
Examples:
|
|
938
|
-
${name}
|
|
939
|
-
${name}
|
|
940
|
-
${name}
|
|
941
|
-
${name}
|
|
1168
|
+
${name} channels add https://example.com/channels/hasna --id ops --retry-attempts 3 --retry-backoff-ms 500
|
|
1169
|
+
${name} channels add bun --id command-hook --transport command --arg run --arg ./handler.ts --arg --json
|
|
1170
|
+
${name} channels add bun --id command-hook --transport command --arg=--json
|
|
1171
|
+
${name} channels add bun --id command-hook --transport command -- run ./handler.ts --json`);
|
|
942
1172
|
}
|
|
943
1173
|
function printEventsHelp(options = {}) {
|
|
944
1174
|
const name = commandName(options);
|
|
@@ -947,7 +1177,7 @@ function printEventsHelp(options = {}) {
|
|
|
947
1177
|
Usage:
|
|
948
1178
|
${name} [--dir <path>] [--json] events emit <type>${options.source ? "" : " --source <source>"} [options]
|
|
949
1179
|
${name} [--dir <path>] [--json] events list [--limit <n>]
|
|
950
|
-
${name} [--dir <path>] [--json] events replay [--id <event-id>] [--dry-run]
|
|
1180
|
+
${name} [--dir <path>] [--json] events replay [--id <event-id>] [--cursor <cursor>] [--limit <n>] [--dry-run]
|
|
951
1181
|
|
|
952
1182
|
Options:
|
|
953
1183
|
--source <source> Event source${options.source ? ` (default: ${options.source})` : ""}
|
|
@@ -957,7 +1187,9 @@ Options:
|
|
|
957
1187
|
--dedupe-key <key> Deduplicate repeated events
|
|
958
1188
|
--data <json> JSON object payload
|
|
959
1189
|
--metadata <json> JSON object metadata
|
|
960
|
-
--no-deliver Record without delivering
|
|
1190
|
+
--no-deliver Record without delivering channels
|
|
1191
|
+
--cursor <cursor> Opaque cursor returned by a previous replay page
|
|
1192
|
+
--limit <n> Maximum events to replay
|
|
961
1193
|
--dry-run Preview replay matches without delivery`);
|
|
962
1194
|
}
|
|
963
1195
|
async function runEventsCli(argv = process.argv.slice(2), options = {}) {
|
|
@@ -981,20 +1213,20 @@ async function runEventsCli(argv = process.argv.slice(2), options = {}) {
|
|
|
981
1213
|
}
|
|
982
1214
|
const store = new JsonEventsStore(parsed.dir);
|
|
983
1215
|
const client = new EventsClient({ store });
|
|
984
|
-
if (group === "
|
|
1216
|
+
if (group === "channels") {
|
|
985
1217
|
if (!command || command === "--help" || command === "-h") {
|
|
986
|
-
|
|
1218
|
+
printChannelsHelp(options);
|
|
987
1219
|
return;
|
|
988
1220
|
}
|
|
989
1221
|
if (command === "add" && (tail[0] === "--help" || tail[0] === "-h")) {
|
|
990
|
-
|
|
1222
|
+
printChannelAddHelp(options);
|
|
991
1223
|
return;
|
|
992
1224
|
}
|
|
993
1225
|
if (tail.includes("--help") || tail.includes("-h")) {
|
|
994
|
-
|
|
1226
|
+
printChannelsHelp(options);
|
|
995
1227
|
return;
|
|
996
1228
|
}
|
|
997
|
-
await
|
|
1229
|
+
await handleChannels(client, command, tail, parsed, options);
|
|
998
1230
|
return;
|
|
999
1231
|
}
|
|
1000
1232
|
if (group === "events") {
|
|
@@ -1011,7 +1243,7 @@ async function runEventsCli(argv = process.argv.slice(2), options = {}) {
|
|
|
1011
1243
|
}
|
|
1012
1244
|
throw new Error(`Unknown command group: ${group}`);
|
|
1013
1245
|
}
|
|
1014
|
-
async function
|
|
1246
|
+
async function handleChannels(client, command, tail, parsed, options) {
|
|
1015
1247
|
if (command === "add") {
|
|
1016
1248
|
const { args, delimiterArgs } = splitDelimiter(tail);
|
|
1017
1249
|
const transport = takeOption(args, "--transport") ?? "webhook";
|
|
@@ -1028,7 +1260,7 @@ async function handleWebhooks(client, command, tail, parsed, options) {
|
|
|
1028
1260
|
const filters = parseFilter(args);
|
|
1029
1261
|
const target = args[0];
|
|
1030
1262
|
if (!target)
|
|
1031
|
-
throw new Error("
|
|
1263
|
+
throw new Error("channels add requires a URL or command target");
|
|
1032
1264
|
const now2 = new Date().toISOString();
|
|
1033
1265
|
const channel = {
|
|
1034
1266
|
id,
|
|
@@ -1077,7 +1309,7 @@ async function handleWebhooks(client, command, tail, parsed, options) {
|
|
|
1077
1309
|
if (command === "remove") {
|
|
1078
1310
|
const id = tail[0];
|
|
1079
1311
|
if (!id)
|
|
1080
|
-
throw new Error("
|
|
1312
|
+
throw new Error("channels remove requires a channel id");
|
|
1081
1313
|
const removed = await client.removeChannel(id);
|
|
1082
1314
|
output(parsed, { removed }, () => console.log(removed ? `Removed ${id}` : `Channel not found: ${id}`));
|
|
1083
1315
|
return;
|
|
@@ -1086,7 +1318,7 @@ async function handleWebhooks(client, command, tail, parsed, options) {
|
|
|
1086
1318
|
const args = [...tail];
|
|
1087
1319
|
const id = args.shift();
|
|
1088
1320
|
if (!id)
|
|
1089
|
-
throw new Error("
|
|
1321
|
+
throw new Error("channels test requires a channel id");
|
|
1090
1322
|
const honorFilters = takeFlag(args, "--honor-filters");
|
|
1091
1323
|
const result = await client.testChannel(id, {
|
|
1092
1324
|
source: takeOption(args, "--source") ?? options.source ?? "hasna.events",
|
|
@@ -1103,7 +1335,7 @@ async function handleWebhooks(client, command, tail, parsed, options) {
|
|
|
1103
1335
|
const args = [...tail];
|
|
1104
1336
|
const id = args.shift();
|
|
1105
1337
|
if (!id)
|
|
1106
|
-
throw new Error("
|
|
1338
|
+
throw new Error("channels match requires a channel id");
|
|
1107
1339
|
const result = await client.matchChannel(id, {
|
|
1108
1340
|
source: takeOption(args, "--source") ?? options.source ?? "hasna.events",
|
|
1109
1341
|
type: takeOption(args, "--type") ?? "events.test",
|
|
@@ -1115,7 +1347,7 @@ async function handleWebhooks(client, command, tail, parsed, options) {
|
|
|
1115
1347
|
output(parsed, result, () => console.log(`${result.matched ? "matched" : "skipped"}: ${result.channelId}`));
|
|
1116
1348
|
return;
|
|
1117
1349
|
}
|
|
1118
|
-
throw new Error(`Unknown
|
|
1350
|
+
throw new Error(`Unknown channels command: ${command ?? ""}`);
|
|
1119
1351
|
}
|
|
1120
1352
|
function splitDelimiter(values) {
|
|
1121
1353
|
const delimiterIndex = values.indexOf("--");
|
|
@@ -1178,9 +1410,11 @@ async function handleEvents(client, command, tail, parsed, options) {
|
|
|
1178
1410
|
eventId: takeOption(args, "--id"),
|
|
1179
1411
|
source: takeOption(args, "--source"),
|
|
1180
1412
|
type: takeOption(args, "--type"),
|
|
1413
|
+
cursor: takeOption(args, "--cursor"),
|
|
1414
|
+
limit: numberOption(takeOption(args, "--limit")),
|
|
1181
1415
|
dryRun: takeFlag(args, "--dry-run")
|
|
1182
1416
|
});
|
|
1183
|
-
output(parsed, result, () => console.log(
|
|
1417
|
+
output(parsed, result, () => console.log(replaySummary(result.events.length, result.deliveries.length, result.nextCursor)));
|
|
1184
1418
|
return;
|
|
1185
1419
|
}
|
|
1186
1420
|
throw new Error(`Unknown events command: ${command ?? ""}`);
|
|
@@ -1201,6 +1435,10 @@ function severityOption(value) {
|
|
|
1201
1435
|
throw new Error(`Invalid severity: ${value}`);
|
|
1202
1436
|
return value;
|
|
1203
1437
|
}
|
|
1438
|
+
function replaySummary(events, deliveries, nextCursor) {
|
|
1439
|
+
const suffix = nextCursor ? `, next cursor: ${nextCursor}` : "";
|
|
1440
|
+
return `Replayed ${events} event(s), ${deliveries} delivery result(s)${suffix}`;
|
|
1441
|
+
}
|
|
1204
1442
|
if (import.meta.main) {
|
|
1205
1443
|
runEventsCli().catch((error) => {
|
|
1206
1444
|
const parsed = parseGlobalArgs(process.argv.slice(2));
|
package/dist/commander.d.ts
CHANGED
|
@@ -5,10 +5,24 @@ export interface RegisterEventsCommandsOptions {
|
|
|
5
5
|
source: string;
|
|
6
6
|
dataDir?: string;
|
|
7
7
|
createClient?: () => EventsClient;
|
|
8
|
-
|
|
8
|
+
channelsCommandName?: string;
|
|
9
9
|
eventsCommandName?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Default row cap applied to `events list` when the caller does not pass an
|
|
12
|
+
* explicit `--limit`. Guards against dumping the entire event store (a
|
|
13
|
+
* usability/performance hazard for hosts with large stores). Pass `--limit 0`
|
|
14
|
+
* to opt out and list every recorded event. Defaults to
|
|
15
|
+
* {@link DEFAULT_EVENT_LIST_LIMIT}.
|
|
16
|
+
*/
|
|
17
|
+
defaultEventListLimit?: number;
|
|
10
18
|
}
|
|
11
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Sane default number of most-recent events returned by `events list` when the
|
|
21
|
+
* host does not configure {@link RegisterEventsCommandsOptions.defaultEventListLimit}
|
|
22
|
+
* and the user does not pass an explicit `--limit`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const DEFAULT_EVENT_LIST_LIMIT = 100;
|
|
25
|
+
export declare function registerChannelCommands(program: CommanderLike, options: RegisterEventsCommandsOptions): CommanderCommandLike;
|
|
12
26
|
export declare function registerEventCommands(program: CommanderLike, options: RegisterEventsCommandsOptions): CommanderCommandLike;
|
|
13
27
|
export declare function registerEventsCommands(program: CommanderLike, options: RegisterEventsCommandsOptions): void;
|
|
14
28
|
export {};
|