@hasna/events 0.1.9 → 0.1.11

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
@@ -207,8 +207,11 @@ events webhooks remove ops
207
207
  Field filters can match nested `data` or `metadata` values. Plain
208
208
  `--data`/`--metadata` values are strings, which keeps ids and slugs such as
209
209
  `001` intact. Use `--data-json` or `--metadata-json` for typed JSON predicates.
210
- Dot paths access nested object keys; dots inside key names are not escaped yet,
211
- and array traversal is not special-cased. Wildcard behavior stays broad for
210
+ Dot paths access nested object keys; dots inside key names are not escaped yet.
211
+ When the actual event value is an array, string filters match any primitive
212
+ array member, which is useful for tag routing such as `data.tags=auto:route`.
213
+ Use `path!=value` or `path!=json` for negative predicates such as
214
+ `metadata-json 'automation.no_auto!=true'`. Wildcard behavior stays broad for
212
215
  legacy source/type/subject filters. For field paths ending in `_path` or `.path`,
213
216
  `*` matches one path segment and `**` matches recursively.
214
217
 
@@ -218,12 +221,24 @@ events webhooks add loops \
218
221
  --transport command \
219
222
  --source todos \
220
223
  --type task.created \
224
+ --timeout-ms 15000 \
225
+ --retry-attempts 3 \
226
+ --retry-backoff-ms 500 \
221
227
  --metadata 'project_path=/home/hasna/workspace/hasna/opensource/*' \
222
228
  --metadata-json 'route_enabled=true' \
229
+ --metadata-json 'automation.no_auto!=true' \
230
+ --data 'tags=auto:route' \
223
231
  --arg events \
224
232
  --arg handle \
225
233
  --arg todos-task
226
234
 
235
+ # Command args that begin with dashes can be passed either form:
236
+ events webhooks add events --id json-route --transport command --arg --json
237
+ events webhooks add events --id json-route --transport command --arg=--json
238
+
239
+ # For nested CLIs, put child positional args and flags after an explicit delimiter.
240
+ events webhooks add events --id nested-route --transport command -- handle todos-task --json
241
+
227
242
  events webhooks match open-source-task-route \
228
243
  --source todos \
229
244
  --type task.created \
package/dist/cli/index.js CHANGED
@@ -18,6 +18,19 @@ function getPathValue(input, path) {
18
18
  return;
19
19
  }, input);
20
20
  }
21
+ function getFieldValues(input, path) {
22
+ const values = [];
23
+ const push = (value) => {
24
+ if (!values.some((item) => Object.is(item, value)))
25
+ values.push(value);
26
+ };
27
+ if (path.includes(".") && path in input)
28
+ push(input[path]);
29
+ const nestedValue = getPathValue(input, path);
30
+ if (nestedValue !== undefined || !path.includes("."))
31
+ push(nestedValue);
32
+ return values;
33
+ }
21
34
  function wildcardToRegExp(pattern, options = {}) {
22
35
  let body = "";
23
36
  for (let index = 0;index < pattern.length; index += 1) {
@@ -47,15 +60,41 @@ function matchRecord(input, matcher) {
47
60
  if (!matcher)
48
61
  return true;
49
62
  return Object.entries(matcher).every(([path, expected]) => {
50
- const actual = getPathValue(input, path);
51
- if (typeof expected === "string" || Array.isArray(expected)) {
52
- return matchString(actual === undefined ? undefined : String(actual), expected, {
53
- segmentSafe: path.endsWith("_path") || path.endsWith(".path")
54
- });
55
- }
56
- return actual === expected;
63
+ const actualValues = getFieldValues(input, path);
64
+ return matchField(actualValues, expected, path);
57
65
  });
58
66
  }
67
+ function matchField(actualValues, expected, path) {
68
+ if (isNegativeMatcher(expected)) {
69
+ return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
70
+ }
71
+ return actualValues.some((actual) => matchPositiveField(actual, expected, path));
72
+ }
73
+ function matchPositiveField(actual, expected, path) {
74
+ if (typeof expected === "string" || Array.isArray(expected)) {
75
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
76
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
77
+ }));
78
+ }
79
+ if (Array.isArray(actual)) {
80
+ return actual.some((item) => item === expected);
81
+ }
82
+ return actual === expected;
83
+ }
84
+ function stringCandidates(actual) {
85
+ if (actual === undefined)
86
+ return [];
87
+ if (Array.isArray(actual)) {
88
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
89
+ }
90
+ return [String(actual)];
91
+ }
92
+ function isPrimitiveFieldValue(value) {
93
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
94
+ }
95
+ function isNegativeMatcher(value) {
96
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
97
+ }
59
98
  function eventMatchesFilter(event, filter) {
60
99
  return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
61
100
  }
@@ -640,13 +679,12 @@ function parseFieldMatchers(values, label, typed = false) {
640
679
  return;
641
680
  const result = {};
642
681
  for (const value of values) {
643
- const separator = value.indexOf("=");
644
- if (separator <= 0)
645
- throw new Error(`Invalid ${label} filter, expected path=value: ${value}`);
646
- const path = value.slice(0, separator);
682
+ const parsed = parseMatcherExpression(value, label);
683
+ const path = parsed.path;
647
684
  if (path in result)
648
685
  throw new Error(`Duplicate ${label} filter path: ${path}`);
649
- result[path] = typed ? parseTypedMatcherValue(value.slice(separator + 1), label) : value.slice(separator + 1);
686
+ const matcherValue = typed ? parseTypedMatcherValue(parsed.rawValue, label) : parsed.rawValue;
687
+ result[path] = parsed.negated ? { not: matcherValue } : matcherValue;
650
688
  }
651
689
  return result;
652
690
  }
@@ -688,6 +726,24 @@ function parseTypedMatcherValue(value, label) {
688
726
  }
689
727
  throw new Error(`${label} filter JSON values must be string, string[], number, boolean, or null`);
690
728
  }
729
+ function parseMatcherExpression(value, label) {
730
+ const negativeSeparator = value.indexOf("!=");
731
+ if (negativeSeparator > 0) {
732
+ return {
733
+ path: value.slice(0, negativeSeparator),
734
+ rawValue: value.slice(negativeSeparator + 2),
735
+ negated: true
736
+ };
737
+ }
738
+ const separator = value.indexOf("=");
739
+ if (separator <= 0)
740
+ throw new Error(`Invalid ${label} filter, expected path=value or path!=value: ${value}`);
741
+ return {
742
+ path: value.slice(0, separator),
743
+ rawValue: value.slice(separator + 1),
744
+ negated: false
745
+ };
746
+ }
691
747
 
692
748
  // src/cli/index.ts
693
749
  function version() {
@@ -704,12 +760,23 @@ function parseGlobalArgs(argv) {
704
760
  let dir;
705
761
  for (let index = 0;index < argv.length; index += 1) {
706
762
  const arg = argv[index];
763
+ if (arg === "--") {
764
+ rest.push(...argv.slice(index + 1));
765
+ break;
766
+ }
767
+ if (!arg.startsWith("-")) {
768
+ rest.push(...argv.slice(index));
769
+ break;
770
+ }
707
771
  if (arg === "--json" || arg === "-j") {
708
772
  json = true;
773
+ } else if (arg.startsWith("--dir=")) {
774
+ dir = arg.slice("--dir=".length);
709
775
  } else if (arg === "--dir") {
710
776
  dir = argv[++index];
711
777
  } else {
712
- rest.push(arg);
778
+ rest.push(...argv.slice(index));
779
+ break;
713
780
  }
714
781
  }
715
782
  return { json, dir, rest };
@@ -827,10 +894,10 @@ Options:
827
894
  --source <source> Event source filter
828
895
  --subject <subject> Event subject filter
829
896
  --severity <severity> Event severity filter
830
- --data <path=value> Event data field filter, repeatable; string values, dot paths, * segment wildcard, ** recursive wildcard
831
- --metadata <path=value> Event metadata field filter, repeatable; string values, dot paths, * segment wildcard, ** recursive wildcard
832
- --data-json <path=json> Event data field filter with typed JSON value
833
- --metadata-json <path=json> Event metadata field filter with typed JSON value
897
+ --data <path=value|path!=value> Event data field filter, repeatable; strings, dot paths, array-member matching, * segment wildcard, ** recursive wildcard
898
+ --metadata <path=value|path!=value> Event metadata field filter, repeatable; strings, dot paths, array-member matching, * segment wildcard, ** recursive wildcard
899
+ --data-json <path=json|path!=json> Event data field filter with typed JSON value
900
+ --metadata-json <path=json|path!=json> Event metadata field filter with typed JSON value
834
901
  --honor-filters On webhooks test, skip delivery when the sample event does not match filters
835
902
  --transport <kind> webhook or command
836
903
  --secret <secret> Webhook signing secret
@@ -838,6 +905,41 @@ Options:
838
905
  --redact <path> Redaction path, repeatable
839
906
  --no-deliver Available on events emit`);
840
907
  }
908
+ function printWebhookAddHelp(options = {}) {
909
+ const name = commandName(options);
910
+ console.log(`${name} webhooks add
911
+
912
+ Usage:
913
+ ${name} [--dir <path>] [--json] webhooks add <url|command> [options]
914
+ ${name} [--dir <path>] [--json] webhooks add <command> --transport command [options] -- [command-args...]
915
+
916
+ Options:
917
+ --id <id> Channel id
918
+ --name <name> Display name
919
+ --transport <kind> webhook or command
920
+ --type <pattern> Event type filter, supports wildcards
921
+ --source <source> Event source filter
922
+ --subject <subject> Event subject filter
923
+ --severity <severity> Event severity filter
924
+ --data <path=value|path!=value> Event data field filter, repeatable
925
+ --metadata <path=value|path!=value> Event metadata field filter, repeatable
926
+ --data-json <path=json|path!=json> Event data field filter with typed JSON value
927
+ --metadata-json <path=json|path!=json> Event metadata field filter with typed JSON value
928
+ --secret <secret> Webhook signing secret
929
+ --header <name=value> Webhook header, repeatable
930
+ --arg <arg> Command argument, repeatable; values may begin with dashes
931
+ --timeout-ms <ms> Transport timeout in milliseconds
932
+ --retry-attempts <n> Maximum delivery attempts
933
+ --retry-backoff-ms <ms> Initial retry backoff in milliseconds
934
+ --redact <path> Redaction path, repeatable
935
+ --disabled Create channel disabled
936
+
937
+ Examples:
938
+ ${name} webhooks add https://example.com/webhooks/hasna --id ops --retry-attempts 3 --retry-backoff-ms 500
939
+ ${name} webhooks add bun --id command-hook --transport command --arg run --arg ./handler.ts --arg --json
940
+ ${name} webhooks add bun --id command-hook --transport command --arg=--json
941
+ ${name} webhooks add bun --id command-hook --transport command -- run ./handler.ts --json`);
942
+ }
841
943
  function printEventsHelp(options = {}) {
842
944
  const name = commandName(options);
843
945
  console.log(`${name} events
@@ -884,6 +986,10 @@ async function runEventsCli(argv = process.argv.slice(2), options = {}) {
884
986
  printWebhooksHelp(options);
885
987
  return;
886
988
  }
989
+ if (command === "add" && (tail[0] === "--help" || tail[0] === "-h")) {
990
+ printWebhookAddHelp(options);
991
+ return;
992
+ }
887
993
  if (tail.includes("--help") || tail.includes("-h")) {
888
994
  printWebhooksHelp(options);
889
995
  return;
@@ -907,7 +1013,7 @@ async function runEventsCli(argv = process.argv.slice(2), options = {}) {
907
1013
  }
908
1014
  async function handleWebhooks(client, command, tail, parsed, options) {
909
1015
  if (command === "add") {
910
- const args = [...tail];
1016
+ const { args, delimiterArgs } = splitDelimiter(tail);
911
1017
  const transport = takeOption(args, "--transport") ?? "webhook";
912
1018
  const id = takeOption(args, "--id") ?? crypto.randomUUID();
913
1019
  const name = takeOption(args, "--name");
@@ -938,7 +1044,7 @@ async function handleWebhooks(client, command, tail, parsed, options) {
938
1044
  if (transport === "webhook") {
939
1045
  channel.webhook = { url: target, secret, headers: parseHeaders(headerValues), timeoutMs };
940
1046
  } else if (transport === "command") {
941
- channel.command = { command: target, args: [...args.slice(1), ...commandArgs], timeoutMs };
1047
+ channel.command = { command: target, args: [...args.slice(1), ...commandArgs, ...delimiterArgs], timeoutMs };
942
1048
  } else {
943
1049
  throw new Error(`Transport ${transport} is reserved for future use and cannot be added yet`);
944
1050
  }
@@ -1011,6 +1117,15 @@ async function handleWebhooks(client, command, tail, parsed, options) {
1011
1117
  }
1012
1118
  throw new Error(`Unknown webhooks command: ${command ?? ""}`);
1013
1119
  }
1120
+ function splitDelimiter(values) {
1121
+ const delimiterIndex = values.indexOf("--");
1122
+ if (delimiterIndex === -1)
1123
+ return { args: [...values], delimiterArgs: [] };
1124
+ return {
1125
+ args: values.slice(0, delimiterIndex),
1126
+ delimiterArgs: values.slice(delimiterIndex + 1)
1127
+ };
1128
+ }
1014
1129
  async function handleEvents(client, command, tail, parsed, options) {
1015
1130
  if (command === "emit") {
1016
1131
  const args = [...tail];
package/dist/commander.js CHANGED
@@ -8,6 +8,19 @@ function getPathValue(input, path) {
8
8
  return;
9
9
  }, input);
10
10
  }
11
+ function getFieldValues(input, path) {
12
+ const values = [];
13
+ const push = (value) => {
14
+ if (!values.some((item) => Object.is(item, value)))
15
+ values.push(value);
16
+ };
17
+ if (path.includes(".") && path in input)
18
+ push(input[path]);
19
+ const nestedValue = getPathValue(input, path);
20
+ if (nestedValue !== undefined || !path.includes("."))
21
+ push(nestedValue);
22
+ return values;
23
+ }
11
24
  function wildcardToRegExp(pattern, options = {}) {
12
25
  let body = "";
13
26
  for (let index = 0;index < pattern.length; index += 1) {
@@ -37,15 +50,41 @@ function matchRecord(input, matcher) {
37
50
  if (!matcher)
38
51
  return true;
39
52
  return Object.entries(matcher).every(([path, expected]) => {
40
- const actual = getPathValue(input, path);
41
- if (typeof expected === "string" || Array.isArray(expected)) {
42
- return matchString(actual === undefined ? undefined : String(actual), expected, {
43
- segmentSafe: path.endsWith("_path") || path.endsWith(".path")
44
- });
45
- }
46
- return actual === expected;
53
+ const actualValues = getFieldValues(input, path);
54
+ return matchField(actualValues, expected, path);
47
55
  });
48
56
  }
57
+ function matchField(actualValues, expected, path) {
58
+ if (isNegativeMatcher(expected)) {
59
+ return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
60
+ }
61
+ return actualValues.some((actual) => matchPositiveField(actual, expected, path));
62
+ }
63
+ function matchPositiveField(actual, expected, path) {
64
+ if (typeof expected === "string" || Array.isArray(expected)) {
65
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
66
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
67
+ }));
68
+ }
69
+ if (Array.isArray(actual)) {
70
+ return actual.some((item) => item === expected);
71
+ }
72
+ return actual === expected;
73
+ }
74
+ function stringCandidates(actual) {
75
+ if (actual === undefined)
76
+ return [];
77
+ if (Array.isArray(actual)) {
78
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
79
+ }
80
+ return [String(actual)];
81
+ }
82
+ function isPrimitiveFieldValue(value) {
83
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
84
+ }
85
+ function isNegativeMatcher(value) {
86
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
87
+ }
49
88
  function eventMatchesFilter(event, filter) {
50
89
  return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
51
90
  }
@@ -651,13 +690,12 @@ function parseFieldMatchers(values, label, typed = false) {
651
690
  return;
652
691
  const result = {};
653
692
  for (const value of values) {
654
- const separator = value.indexOf("=");
655
- if (separator <= 0)
656
- throw new Error(`Invalid ${label} filter, expected path=value: ${value}`);
657
- const path = value.slice(0, separator);
693
+ const parsed = parseMatcherExpression(value, label);
694
+ const path = parsed.path;
658
695
  if (path in result)
659
696
  throw new Error(`Duplicate ${label} filter path: ${path}`);
660
- result[path] = typed ? parseTypedMatcherValue(value.slice(separator + 1), label) : value.slice(separator + 1);
697
+ const matcherValue = typed ? parseTypedMatcherValue(parsed.rawValue, label) : parsed.rawValue;
698
+ result[path] = parsed.negated ? { not: matcherValue } : matcherValue;
661
699
  }
662
700
  return result;
663
701
  }
@@ -699,6 +737,24 @@ function parseTypedMatcherValue(value, label) {
699
737
  }
700
738
  throw new Error(`${label} filter JSON values must be string, string[], number, boolean, or null`);
701
739
  }
740
+ function parseMatcherExpression(value, label) {
741
+ const negativeSeparator = value.indexOf("!=");
742
+ if (negativeSeparator > 0) {
743
+ return {
744
+ path: value.slice(0, negativeSeparator),
745
+ rawValue: value.slice(negativeSeparator + 2),
746
+ negated: true
747
+ };
748
+ }
749
+ const separator = value.indexOf("=");
750
+ if (separator <= 0)
751
+ throw new Error(`Invalid ${label} filter, expected path=value or path!=value: ${value}`);
752
+ return {
753
+ path: value.slice(0, separator),
754
+ rawValue: value.slice(separator + 1),
755
+ negated: false
756
+ };
757
+ }
702
758
 
703
759
  // src/commander.ts
704
760
  function parseJsonObject(value, fallback) {
@@ -741,7 +797,7 @@ function wantsJson(actionOptions, command) {
741
797
  }
742
798
  function registerWebhookCommands(program, options) {
743
799
  const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
744
- webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--data <path=value...>", "Event data field filter; string values, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--metadata <path=value...>", "Event metadata field filter; string values, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--data-json <path=json...>", "Event data field filter with typed JSON value", collectValues, []).option("--metadata-json <path=json...>", "Event metadata field filter with typed JSON value", collectValues, []).option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
800
+ webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--data <path=value...>", "Event data field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--metadata <path=value...>", "Event metadata field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--data-json <path=json...>", "Event data field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--metadata-json <path=json...>", "Event metadata field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
745
801
  const timestamp = new Date().toISOString();
746
802
  const channel = {
747
803
  id: actionOptions.id,
@@ -1,5 +1,7 @@
1
- import type { EventFilter, StringMatcher } from "./types.js";
2
- type MatcherValue = StringMatcher | number | boolean | null;
1
+ import type { EventFilter, FieldMatcherValue } from "./types.js";
2
+ type MatcherValue = FieldMatcherValue | {
3
+ not: FieldMatcherValue;
4
+ };
3
5
  export interface FilterOptionInput {
4
6
  source?: string;
5
7
  type?: string;
package/dist/filter.js CHANGED
@@ -8,6 +8,19 @@ function getPathValue(input, path) {
8
8
  return;
9
9
  }, input);
10
10
  }
11
+ function getFieldValues(input, path) {
12
+ const values = [];
13
+ const push = (value) => {
14
+ if (!values.some((item) => Object.is(item, value)))
15
+ values.push(value);
16
+ };
17
+ if (path.includes(".") && path in input)
18
+ push(input[path]);
19
+ const nestedValue = getPathValue(input, path);
20
+ if (nestedValue !== undefined || !path.includes("."))
21
+ push(nestedValue);
22
+ return values;
23
+ }
11
24
  function wildcardToRegExp(pattern, options = {}) {
12
25
  let body = "";
13
26
  for (let index = 0;index < pattern.length; index += 1) {
@@ -37,15 +50,41 @@ function matchRecord(input, matcher) {
37
50
  if (!matcher)
38
51
  return true;
39
52
  return Object.entries(matcher).every(([path, expected]) => {
40
- const actual = getPathValue(input, path);
41
- if (typeof expected === "string" || Array.isArray(expected)) {
42
- return matchString(actual === undefined ? undefined : String(actual), expected, {
43
- segmentSafe: path.endsWith("_path") || path.endsWith(".path")
44
- });
45
- }
46
- return actual === expected;
53
+ const actualValues = getFieldValues(input, path);
54
+ return matchField(actualValues, expected, path);
47
55
  });
48
56
  }
57
+ function matchField(actualValues, expected, path) {
58
+ if (isNegativeMatcher(expected)) {
59
+ return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
60
+ }
61
+ return actualValues.some((actual) => matchPositiveField(actual, expected, path));
62
+ }
63
+ function matchPositiveField(actual, expected, path) {
64
+ if (typeof expected === "string" || Array.isArray(expected)) {
65
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
66
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
67
+ }));
68
+ }
69
+ if (Array.isArray(actual)) {
70
+ return actual.some((item) => item === expected);
71
+ }
72
+ return actual === expected;
73
+ }
74
+ function stringCandidates(actual) {
75
+ if (actual === undefined)
76
+ return [];
77
+ if (Array.isArray(actual)) {
78
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
79
+ }
80
+ return [String(actual)];
81
+ }
82
+ function isPrimitiveFieldValue(value) {
83
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
84
+ }
85
+ function isNegativeMatcher(value) {
86
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
87
+ }
49
88
  function eventMatchesFilter(event, filter) {
50
89
  return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
51
90
  }
package/dist/index.js CHANGED
@@ -8,6 +8,19 @@ function getPathValue(input, path) {
8
8
  return;
9
9
  }, input);
10
10
  }
11
+ function getFieldValues(input, path) {
12
+ const values = [];
13
+ const push = (value) => {
14
+ if (!values.some((item) => Object.is(item, value)))
15
+ values.push(value);
16
+ };
17
+ if (path.includes(".") && path in input)
18
+ push(input[path]);
19
+ const nestedValue = getPathValue(input, path);
20
+ if (nestedValue !== undefined || !path.includes("."))
21
+ push(nestedValue);
22
+ return values;
23
+ }
11
24
  function wildcardToRegExp(pattern, options = {}) {
12
25
  let body = "";
13
26
  for (let index = 0;index < pattern.length; index += 1) {
@@ -37,15 +50,41 @@ function matchRecord(input, matcher) {
37
50
  if (!matcher)
38
51
  return true;
39
52
  return Object.entries(matcher).every(([path, expected]) => {
40
- const actual = getPathValue(input, path);
41
- if (typeof expected === "string" || Array.isArray(expected)) {
42
- return matchString(actual === undefined ? undefined : String(actual), expected, {
43
- segmentSafe: path.endsWith("_path") || path.endsWith(".path")
44
- });
45
- }
46
- return actual === expected;
53
+ const actualValues = getFieldValues(input, path);
54
+ return matchField(actualValues, expected, path);
47
55
  });
48
56
  }
57
+ function matchField(actualValues, expected, path) {
58
+ if (isNegativeMatcher(expected)) {
59
+ return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
60
+ }
61
+ return actualValues.some((actual) => matchPositiveField(actual, expected, path));
62
+ }
63
+ function matchPositiveField(actual, expected, path) {
64
+ if (typeof expected === "string" || Array.isArray(expected)) {
65
+ return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
66
+ segmentSafe: path.endsWith("_path") || path.endsWith(".path")
67
+ }));
68
+ }
69
+ if (Array.isArray(actual)) {
70
+ return actual.some((item) => item === expected);
71
+ }
72
+ return actual === expected;
73
+ }
74
+ function stringCandidates(actual) {
75
+ if (actual === undefined)
76
+ return [];
77
+ if (Array.isArray(actual)) {
78
+ return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
79
+ }
80
+ return [String(actual)];
81
+ }
82
+ function isPrimitiveFieldValue(value) {
83
+ return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
84
+ }
85
+ function isNegativeMatcher(value) {
86
+ return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
87
+ }
49
88
  function eventMatchesFilter(event, filter) {
50
89
  return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
51
90
  }
package/dist/types.d.ts CHANGED
@@ -27,13 +27,18 @@ export interface EventInput<TData extends EventData = EventData> {
27
27
  metadata?: Record<string, unknown>;
28
28
  }
29
29
  export type StringMatcher = string | string[];
30
+ export type FieldMatcherValue = StringMatcher | number | boolean | null;
31
+ export interface NegativeFieldMatcher {
32
+ not: FieldMatcherValue;
33
+ }
34
+ export type FieldMatcher = FieldMatcherValue | NegativeFieldMatcher;
30
35
  export interface EventFilter {
31
36
  source?: StringMatcher;
32
37
  type?: StringMatcher;
33
38
  subject?: StringMatcher;
34
39
  severity?: StringMatcher;
35
- data?: Record<string, StringMatcher | number | boolean | null>;
36
- metadata?: Record<string, StringMatcher | number | boolean | null>;
40
+ data?: Record<string, FieldMatcher>;
41
+ metadata?: Record<string, FieldMatcher>;
37
42
  }
38
43
  export interface RetryPolicy {
39
44
  maxAttempts?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/events",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Shared event envelopes, local subscriptions, and webhook delivery for Hasna open-source apps",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",