@openparachute/vault 0.6.4-rc.2 → 0.6.4-rc.4

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.
@@ -95,6 +95,39 @@ describe("buildPredicate", () => {
95
95
  expect(pred(makeNote({ tags: ["reader"] }))).toBe(false);
96
96
  expect(pred(makeNote({ tags: ["reader", "important"] }))).toBe(true);
97
97
  });
98
+
99
+ // Value-matched metadata grammar (vault#299 Part B).
100
+ it("metadata eq: fires only when the field equals the value", () => {
101
+ const pred = buildPredicate({ metadata: { state: { eq: "published" } } }, "publish_hook");
102
+ expect(pred(makeNote({ metadata: { state: "published" } }))).toBe(true);
103
+ expect(pred(makeNote({ metadata: { state: "drafted" } }))).toBe(false);
104
+ expect(pred(makeNote({ metadata: {} }))).toBe(false); // absent → no fire
105
+ });
106
+
107
+ it("metadata in: fires on membership", () => {
108
+ const pred = buildPredicate({ metadata: { state: { in: ["produced", "published"] } } }, "h");
109
+ expect(pred(makeNote({ metadata: { state: "produced" } }))).toBe(true);
110
+ expect(pred(makeNote({ metadata: { state: "idea" } }))).toBe(false);
111
+ });
112
+
113
+ it("metadata combines with tag + presence filters (all must hold)", () => {
114
+ const pred = buildPredicate(
115
+ { tags: ["piece"], metadata: { state: { eq: "published" } }, missing_metadata: ["notified_at"] },
116
+ "h",
117
+ );
118
+ expect(pred(makeNote({ tags: ["piece"], metadata: { state: "published" } }))).toBe(true);
119
+ // right state, wrong tag.
120
+ expect(pred(makeNote({ tags: ["other"], metadata: { state: "published" } }))).toBe(false);
121
+ // right tag + state but already notified.
122
+ expect(
123
+ pred(makeNote({ tags: ["piece"], metadata: { state: "published", notified_at: "x" } })),
124
+ ).toBe(false);
125
+ });
126
+
127
+ it("a malformed operator never throws from the predicate (treated as no-match)", () => {
128
+ const pred = buildPredicate({ metadata: { state: { bogus: "x" } as any } }, "h");
129
+ expect(pred(makeNote({ metadata: { state: "published" } }))).toBe(false);
130
+ });
98
131
  });
99
132
 
100
133
  describe("registerTriggers — dispatch modes", async () => {
package/src/triggers.ts CHANGED
@@ -30,6 +30,7 @@ import { mkdirSync, readFileSync, writeFileSync, existsSync } from "fs";
30
30
  import crypto from "node:crypto";
31
31
  import type { Note, Store, Attachment } from "../core/src/types.ts";
32
32
  import type { HookRegistry, HookEvent, NoteHookPayload } from "../core/src/hooks.ts";
33
+ import { matchesOperator } from "../core/src/query-operators.ts";
33
34
  import type { TriggerConfig, TriggerWhen } from "./config.ts";
34
35
  import type { StoredTrigger } from "../core/src/triggers-store.ts";
35
36
  import { getVaultNameForStore } from "./vault-store.ts";
@@ -106,6 +107,22 @@ export function buildPredicate(when: TriggerWhen, triggerName: string): (note: N
106
107
  }
107
108
  }
108
109
 
110
+ // Value-matched metadata filter (vault#299 Part B). Each field's
111
+ // operator-object is evaluated against the note's live metadata with the
112
+ // SAME engine query-notes uses. ALL fields must match (AND). A malformed
113
+ // operator throws at registration-time validation, not here — by the time
114
+ // the predicate runs the shape is known-good, but we guard defensively so
115
+ // a bad config can never crash the hook dispatch loop (treat as no-match).
116
+ if (when.metadata) {
117
+ for (const [field, opObj] of Object.entries(when.metadata)) {
118
+ try {
119
+ if (!matchesOperator(field, meta?.[field], opObj)) return false;
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
124
+ }
125
+
109
126
  return true;
110
127
  };
111
128
  }