@memberjunction/global 5.42.0 → 5.43.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.
Files changed (42) hide show
  1. package/README.md +89 -0
  2. package/dist/KeyedSerialTaskQueue.d.ts +60 -0
  3. package/dist/KeyedSerialTaskQueue.d.ts.map +1 -0
  4. package/dist/KeyedSerialTaskQueue.js +76 -0
  5. package/dist/KeyedSerialTaskQueue.js.map +1 -0
  6. package/dist/__tests__/FieldRulesEvaluator.test.d.ts +2 -0
  7. package/dist/__tests__/FieldRulesEvaluator.test.d.ts.map +1 -0
  8. package/dist/__tests__/FieldRulesEvaluator.test.js +167 -0
  9. package/dist/__tests__/FieldRulesEvaluator.test.js.map +1 -0
  10. package/dist/__tests__/KeyedSerialTaskQueue.test.d.ts +2 -0
  11. package/dist/__tests__/KeyedSerialTaskQueue.test.d.ts.map +1 -0
  12. package/dist/__tests__/KeyedSerialTaskQueue.test.js +91 -0
  13. package/dist/__tests__/KeyedSerialTaskQueue.test.js.map +1 -0
  14. package/dist/fieldRules/FieldRulesEvaluator.d.ts +34 -0
  15. package/dist/fieldRules/FieldRulesEvaluator.d.ts.map +1 -0
  16. package/dist/fieldRules/FieldRulesEvaluator.js +130 -0
  17. package/dist/fieldRules/FieldRulesEvaluator.js.map +1 -0
  18. package/dist/fieldRules/FieldTransformEngine.d.ts +44 -0
  19. package/dist/fieldRules/FieldTransformEngine.d.ts.map +1 -0
  20. package/dist/fieldRules/FieldTransformEngine.js +189 -0
  21. package/dist/fieldRules/FieldTransformEngine.js.map +1 -0
  22. package/dist/fieldRules/index.d.ts +6 -0
  23. package/dist/fieldRules/index.d.ts.map +1 -0
  24. package/dist/fieldRules/index.js +6 -0
  25. package/dist/fieldRules/index.js.map +1 -0
  26. package/dist/fieldRules/rules.d.ts +135 -0
  27. package/dist/fieldRules/rules.d.ts.map +1 -0
  28. package/dist/fieldRules/rules.js +2 -0
  29. package/dist/fieldRules/rules.js.map +1 -0
  30. package/dist/fieldRules/transformRegistry.d.ts +22 -0
  31. package/dist/fieldRules/transformRegistry.d.ts.map +1 -0
  32. package/dist/fieldRules/transformRegistry.js +33 -0
  33. package/dist/fieldRules/transformRegistry.js.map +1 -0
  34. package/dist/fieldRules/transforms.d.ts +102 -0
  35. package/dist/fieldRules/transforms.d.ts.map +1 -0
  36. package/dist/fieldRules/transforms.js +8 -0
  37. package/dist/fieldRules/transforms.js.map +1 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +2 -0
  41. package/dist/index.js.map +1 -1
  42. package/package.json +1 -1
package/README.md CHANGED
@@ -456,6 +456,26 @@ cache.Remove('user-prefs');
456
456
  cache.Clear();
457
457
  ```
458
458
 
459
+ ### KeyedSerialTaskQueue
460
+
461
+ An entity-agnostic primitive for **fire-and-forget work that must serialize per key**. Tasks enqueued under the same key (compared by object identity) run strictly in order — the next can't start until the prior settles — while tasks under different keys run concurrently. Failures are tallied for a later `flush()` and never propagate outward, so the enqueue site is never blocked, nor broken, by a background failure.
462
+
463
+ It is **self-bounding**: only in-flight tasks are retained (they drop out as they settle) and failures accumulate into counters, so a long-lived queue that never flushes does not grow without bound.
464
+
465
+ ```typescript
466
+ import { KeyedSerialTaskQueue } from '@memberjunction/global';
467
+
468
+ const queue = new KeyedSerialTaskQueue({ onError: (err, label) => console.error(label, err) });
469
+
470
+ // Same key (the `record` object) → these run in order; different keys → concurrent.
471
+ queue.enqueue(record, () => insert(record));
472
+ queue.enqueue(record, () => update(record), { label: 'update', isOk: (ok) => ok === true });
473
+
474
+ const { failures, rejections } = await queue.flush(); // await in-flight tasks + read/reset the failure tally
475
+ ```
476
+
477
+ The canonical consumer is `BaseEntitySaveQueue` in `@memberjunction/core`, which builds the fire-and-forget entity-save pattern (INSERT then chained UPDATE, with the "mutate-after-insert" race made structurally impossible) on top of this primitive.
478
+
459
479
  ### WarningManager
460
480
 
461
481
  A singleton warning system with session-level deduplication, debounced output, and tree-structured formatting. Tracks deprecation warnings, field-not-found warnings, and redundant load warnings.
@@ -675,6 +695,75 @@ function RegisterClass(
675
695
  | `lodash` | Deep comparison, type checking, object utilities |
676
696
  | `uuid` | UUID v4 generation |
677
697
 
698
+ ## Field Rules Engine
699
+
700
+ A **framework-agnostic** engine for computing field values from declarative rules and a composable
701
+ transform pipeline. It is the shared substrate behind two MemberJunction features — and is designed so
702
+ *any* code can use it, because it lives here in `global` with zero dependencies beyond this package:
703
+
704
+ | Layer | Package | Use it when |
705
+ |---|---|---|
706
+ | **Pure engine** (this package) | `@memberjunction/global` | You have a **plain record** (`Record<string, unknown>`) — from anywhere — and want to compute/transform field values. No entity, no DB required. |
707
+ | **`EntityFieldRules`** | `@memberjunction/core` | The **target is an MJ entity** — you want metadata validation, automatic type coercion, RunView-backed lookups, and write-back with Record Changes versioning. Builds on this engine. |
708
+ | **`FieldMappingEngine`** | `@memberjunction/integration` | The **other side is a live external system** (its own protocol, auth, match resolution, sync direction). Uses this engine for the per-field transforms. |
709
+
710
+ > One engine, three purpose-built layers. Pick the lowest one that fits: if you only have a dict, use this; if you're updating an entity, use core; if you're syncing an external system, that's integration's job.
711
+
712
+ ### Two pieces
713
+
714
+ **1. `FieldTransformEngine` — a transform pipeline over `(value, fields)`**
715
+
716
+ Runs an ordered list of `TransformStep`s. Step types: `direct`, `regex`, `split`, `combine`, `lookup`
717
+ (in-memory map), `format`, `coerce`, `substring`, `custom` (a `(value, fields) => result` expression,
718
+ compiled once and LRU-cached). Per-step `OnError` is `Skip` | `Null` | `Fail` (default `Null` — grace).
719
+
720
+ ```ts
721
+ import { FieldTransformEngine } from '@memberjunction/global';
722
+
723
+ const e = new FieldTransformEngine();
724
+ e.ExecutePipeline('19.99', {}, [{ Type: 'coerce', Config: { TargetType: 'number' } }]).Value; // 19.99
725
+ e.Evaluate("fields.First + ' ' + fields.Last", undefined, { First: 'Ada', Last: 'Lovelace' }); // 'Ada Lovelace'
726
+ ```
727
+
728
+ **2. `FieldRulesEvaluator` — rules → a per-field diff (no mutation)**
729
+
730
+ A `FieldRule` sets a `TargetField` from a `Source` — `static` | `field` | `formula` | `lookup` —
731
+ optionally through a `Transforms` pipeline, gated by an optional `Condition` (a **safe** boolean
732
+ expression via this package's `SafeExpressionEvaluator`). `ComputeChanges` returns a `FieldChange[]`
733
+ (old → new per rule) **without applying anything** — which is exactly what makes a **dry-run preview**
734
+ possible: the caller decides whether to write.
735
+
736
+ ```ts
737
+ import { FieldRulesEvaluator, type FieldRuleSet } from '@memberjunction/global';
738
+
739
+ const ruleSet: FieldRuleSet = {
740
+ Rules: [
741
+ { TargetField: 'FullName', Source: { Kind: 'formula', Expression: "fields.FirstName + ' ' + fields.LastName" } },
742
+ { TargetField: 'Tier', Source: { Kind: 'static', Value: 'Gold' }, Condition: 'Revenue > 1000' },
743
+ ],
744
+ };
745
+
746
+ // LookupResolver is optional — only needed for `lookup` sources. core's EntityFieldRules supplies a
747
+ // RunView-backed one; here you inject your own (or omit it).
748
+ const changes = await new FieldRulesEvaluator().ComputeChanges(
749
+ { FirstName: 'Ada', LastName: 'Lovelace', FullName: '', Revenue: 5000, Tier: 'Bronze' },
750
+ ruleSet,
751
+ );
752
+ // → [{ Field: 'FullName', OldValue: '', NewValue: 'Ada Lovelace', Changed: true, Applied: true }, …]
753
+ ```
754
+
755
+ ### Safety
756
+
757
+ - `Condition` expressions use the **safe** evaluator (blocklisted patterns, no statements).
758
+ - `formula` / `custom` value expressions compile with `new Function` (the same model as the established
759
+ integration `custom` transform) — author them from privileged users/agents, not untrusted input.
760
+ - The engine performs **no I/O**; `lookup` reaches a database only through the resolver you inject.
761
+
762
+ ### See also
763
+
764
+ - **`EntityFieldRules`** in [`@memberjunction/core`](../MJCore/README.md#entity-field-rules) — the metadata-aware layer for updating entities (validation, coercion, lookups, apply).
765
+ - **`FieldMappingEngine`** in [`@memberjunction/integration`](../Integration/engine/README.md#field-mapping--the-shared-transform-engine) — external-system field mapping built on this engine.
766
+
678
767
  ## Related Packages
679
768
 
680
769
  | Package | Relationship |
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @fileoverview Entity-agnostic per-key serial task queue. The lowest-layer primitive behind the
3
+ * fire-and-forget save pattern: tasks enqueued under the same key run strictly in order (the next
4
+ * cannot start until the prior settles), tasks under different keys run concurrently, and failures
5
+ * are captured for a later `flush()` rather than propagated outward. MJGlobal cannot reference
6
+ * `BaseEntity`, so this knows nothing about entities — see `BaseEntitySaveQueue` for the entity façade.
7
+ *
8
+ * Self-bounding: only *in-flight* tasks are retained (they drop out as they settle), and failures are
9
+ * tallied into counters — so a long-lived queue that never flushes does not grow without bound.
10
+ * @module @memberjunction/global
11
+ */
12
+ /** Aggregate outcome of a {@link KeyedSerialTaskQueue.flush}. */
13
+ export interface SerialTaskFlushResult {
14
+ /** Tasks that rejected (threw) OR resolved a falsy "not ok" value, since the last flush. */
15
+ failures: number;
16
+ /** Tasks that rejected (threw), since the last flush. */
17
+ rejections: number;
18
+ }
19
+ /**
20
+ * A per-key serial task chain. Tasks for the **same key** (compared by object identity) run one at a
21
+ * time in enqueue order; tasks for **different keys** run concurrently. Enqueuing is fire-and-forget —
22
+ * the returned promise never rejects, and failures are tallied for {@link flush}.
23
+ *
24
+ * This is what makes "mutate after the prior task lands" structural: a task enqueued after another on
25
+ * the same key physically cannot begin until the prior one resolves.
26
+ */
27
+ export declare class KeyedSerialTaskQueue {
28
+ /** Latest tail promise per key (object identity) — never rejects, so the chain can't break. */
29
+ private tails;
30
+ /** Currently-running tasks (each removes itself on settle), for `flush()` to await. */
31
+ private inFlight;
32
+ /** Failures/rejections since the last flush (counters, not a growing list — keeps memory bounded). */
33
+ private failures;
34
+ private rejections;
35
+ private readonly onError?;
36
+ constructor(opts?: {
37
+ onError?: (err: unknown, label?: string) => void;
38
+ });
39
+ /**
40
+ * Enqueues `task` to run after all prior tasks for `key` have settled. Fire-and-forget: returns
41
+ * the task's result promise, but the caller need not await it — it resolves to the task's value on
42
+ * success or `undefined` on failure, and **never rejects**. Failures (a thrown error, or a resolved
43
+ * value `opts.isOk` deems falsy) are counted for {@link flush} and routed to `onError`.
44
+ *
45
+ * @param key Serialization key (object identity). Same key → serialized; different keys → concurrent.
46
+ * @param task The work to run.
47
+ * @param opts.label Diagnostic label passed to `onError`.
48
+ * @param opts.isOk Treats a resolved value as a failure when it returns false (e.g. `Save()` → `false`).
49
+ */
50
+ enqueue<T>(key: object, task: () => Promise<T>, opts?: {
51
+ label?: string;
52
+ isOk?: (v: T) => boolean;
53
+ }): Promise<T | undefined>;
54
+ /**
55
+ * Awaits the currently in-flight tasks, then reports and resets the failure tally accumulated since
56
+ * the last flush. Tasks enqueued after this call begins are not awaited here.
57
+ */
58
+ flush(): Promise<SerialTaskFlushResult>;
59
+ }
60
+ //# sourceMappingURL=KeyedSerialTaskQueue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"KeyedSerialTaskQueue.d.ts","sourceRoot":"","sources":["../src/KeyedSerialTaskQueue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,iEAAiE;AACjE,MAAM,WAAW,qBAAqB;IAClC,4FAA4F;IAC5F,QAAQ,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,qBAAa,oBAAoB;IAC7B,+FAA+F;IAC/F,OAAO,CAAC,KAAK,CAAwC;IACrD,uFAAuF;IACvF,OAAO,CAAC,QAAQ,CAA4B;IAC5C,sGAAsG;IACtG,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAyC;gBAEtD,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE;IAIvE;;;;;;;;;;OAUG;IACI,OAAO,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;IA8BnI;;;OAGG;IACU,KAAK,IAAI,OAAO,CAAC,qBAAqB,CAAC;CAOvD"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @fileoverview Entity-agnostic per-key serial task queue. The lowest-layer primitive behind the
3
+ * fire-and-forget save pattern: tasks enqueued under the same key run strictly in order (the next
4
+ * cannot start until the prior settles), tasks under different keys run concurrently, and failures
5
+ * are captured for a later `flush()` rather than propagated outward. MJGlobal cannot reference
6
+ * `BaseEntity`, so this knows nothing about entities — see `BaseEntitySaveQueue` for the entity façade.
7
+ *
8
+ * Self-bounding: only *in-flight* tasks are retained (they drop out as they settle), and failures are
9
+ * tallied into counters — so a long-lived queue that never flushes does not grow without bound.
10
+ * @module @memberjunction/global
11
+ */
12
+ /**
13
+ * A per-key serial task chain. Tasks for the **same key** (compared by object identity) run one at a
14
+ * time in enqueue order; tasks for **different keys** run concurrently. Enqueuing is fire-and-forget —
15
+ * the returned promise never rejects, and failures are tallied for {@link flush}.
16
+ *
17
+ * This is what makes "mutate after the prior task lands" structural: a task enqueued after another on
18
+ * the same key physically cannot begin until the prior one resolves.
19
+ */
20
+ export class KeyedSerialTaskQueue {
21
+ constructor(opts) {
22
+ /** Latest tail promise per key (object identity) — never rejects, so the chain can't break. */
23
+ this.tails = new WeakMap();
24
+ /** Currently-running tasks (each removes itself on settle), for `flush()` to await. */
25
+ this.inFlight = new Set();
26
+ /** Failures/rejections since the last flush (counters, not a growing list — keeps memory bounded). */
27
+ this.failures = 0;
28
+ this.rejections = 0;
29
+ this.onError = opts?.onError;
30
+ }
31
+ /**
32
+ * Enqueues `task` to run after all prior tasks for `key` have settled. Fire-and-forget: returns
33
+ * the task's result promise, but the caller need not await it — it resolves to the task's value on
34
+ * success or `undefined` on failure, and **never rejects**. Failures (a thrown error, or a resolved
35
+ * value `opts.isOk` deems falsy) are counted for {@link flush} and routed to `onError`.
36
+ *
37
+ * @param key Serialization key (object identity). Same key → serialized; different keys → concurrent.
38
+ * @param task The work to run.
39
+ * @param opts.label Diagnostic label passed to `onError`.
40
+ * @param opts.isOk Treats a resolved value as a failure when it returns false (e.g. `Save()` → `false`).
41
+ */
42
+ enqueue(key, task, opts) {
43
+ const prior = this.tails.get(key) ?? Promise.resolve();
44
+ // Run the task exactly once, after `prior` settles, collapsing to a never-rejecting outcome.
45
+ const settled = prior.then(() => task(), () => task()).then((value) => ({ value: value, ok: opts?.isOk ? !!opts.isOk(value) : true, rejected: false, err: undefined }), (err) => ({ value: undefined, ok: false, rejected: true, err }));
46
+ // Never-rejecting tail: tally failures + route to onError. Serves as the per-key chain link.
47
+ const tail = settled.then((o) => {
48
+ if (!o.ok) {
49
+ this.failures++;
50
+ if (o.rejected) {
51
+ this.rejections++;
52
+ }
53
+ const err = o.rejected ? o.err : new Error(`task resolved not-ok${opts?.label ? `: ${opts.label}` : ''}`);
54
+ this.onError?.(err, opts?.label);
55
+ }
56
+ });
57
+ this.tails.set(key, tail);
58
+ // Track only while in flight — self-remove on settle so the set stays bounded by concurrency.
59
+ let tracked;
60
+ tracked = tail.finally(() => this.inFlight.delete(tracked));
61
+ this.inFlight.add(tracked);
62
+ return settled.then((o) => (o.ok ? o.value : undefined));
63
+ }
64
+ /**
65
+ * Awaits the currently in-flight tasks, then reports and resets the failure tally accumulated since
66
+ * the last flush. Tasks enqueued after this call begins are not awaited here.
67
+ */
68
+ async flush() {
69
+ await Promise.all([...this.inFlight]);
70
+ const result = { failures: this.failures, rejections: this.rejections };
71
+ this.failures = 0;
72
+ this.rejections = 0;
73
+ return result;
74
+ }
75
+ }
76
+ //# sourceMappingURL=KeyedSerialTaskQueue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"KeyedSerialTaskQueue.js","sourceRoot":"","sources":["../src/KeyedSerialTaskQueue.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=FieldRulesEvaluator.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldRulesEvaluator.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/FieldRulesEvaluator.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,167 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { FieldRulesEvaluator } from '../fieldRules/FieldRulesEvaluator.js';
3
+ import { FieldTransformEngine } from '../fieldRules/FieldTransformEngine.js';
4
+ import { RegisterFieldTransform, GetFieldTransform } from '../fieldRules/transformRegistry.js';
5
+ describe('FieldTransformEngine — plugin registry (keeps libs out of global)', () => {
6
+ it('dispatches a non-built-in transform to a registered plugin', () => {
7
+ const seen = [];
8
+ RegisterFieldTransform('jsonpath', (value, _fields, config) => {
9
+ seen.push({ value, config });
10
+ return 'plugin-result';
11
+ });
12
+ expect(GetFieldTransform('JSONPath')).toBeTypeOf('function'); // case-insensitive
13
+ const out = new FieldTransformEngine().ExecutePipeline('{"a":1}', {}, [{ Type: 'jsonpath', Config: { Path: '$.a' } }]);
14
+ expect(out.Value).toBe('plugin-result');
15
+ expect(seen[0]).toEqual({ value: '{"a":1}', config: { Path: '$.a' } });
16
+ });
17
+ it('errors clearly for an unregistered plugin transform (OnError=Fail)', () => {
18
+ expect(() => new FieldTransformEngine().ExecutePipeline('x', {}, [{ Type: 'xpath', Config: { Path: '/a' }, OnError: 'Fail' }]))
19
+ .toThrow(/Unknown transform type 'xpath'/);
20
+ });
21
+ });
22
+ describe('FieldRulesEvaluator — entityDocument source (injected render infra)', () => {
23
+ it('renders the record via the injected EntityDocumentResolver', async () => {
24
+ const seen = [];
25
+ const evaluator = new FieldRulesEvaluator({ EntityDocumentResolver: async (d) => { seen.push(d); return 'RENDERED DOC'; } });
26
+ const [c] = await evaluator.ComputeChanges({ Name: 'Acme', Doc: '' }, { Rules: [{ TargetField: 'Doc', Source: { Kind: 'entityDocument', EntityDocument: { EntityDocumentID: 'ed-1' } } }] });
27
+ expect(c.NewValue).toBe('RENDERED DOC');
28
+ expect(seen[0]).toEqual({ EntityDocumentID: 'ed-1', Record: { Name: 'Acme', Doc: '' } });
29
+ });
30
+ it('errors clearly when no EntityDocumentResolver is provided', async () => {
31
+ const [c] = await new FieldRulesEvaluator().ComputeChanges({ X: '' }, { Rules: [{ TargetField: 'X', Source: { Kind: 'entityDocument' } }] });
32
+ expect(c.Applied).toBe(false);
33
+ expect(c.Error).toMatch(/no EntityDocumentResolver/);
34
+ });
35
+ });
36
+ describe('FieldRulesEvaluator — prompt sources', () => {
37
+ it('passes the WHOLE record to the prompt by default and uses its output', async () => {
38
+ const seen = [];
39
+ const evaluator = new FieldRulesEvaluator({ PromptResolver: async (p) => { seen.push(p); return 'A concise summary.'; } });
40
+ const [c] = await evaluator.ComputeChanges({ Name: 'Acme', Notes: 'long notes…', Summary: '' }, { Rules: [{ TargetField: 'Summary', Source: { Kind: 'prompt', Prompt: { PromptID: 'p-1' } } }] });
41
+ expect(c.NewValue).toBe('A concise summary.');
42
+ expect(seen[0].PromptID).toBe('p-1');
43
+ expect(seen[0].Data).toEqual({ Name: 'Acme', Notes: 'long notes…', Summary: '' }); // whole record
44
+ });
45
+ it('shapes the prompt data when Data is provided (field/formula/static resolved per entry)', async () => {
46
+ let captured = {};
47
+ const evaluator = new FieldRulesEvaluator({ PromptResolver: async (p) => { captured = p.Data; return 'ok'; } });
48
+ await evaluator.ComputeChanges({ First: 'Ada', Last: 'Lovelace', Bio: '' }, { Rules: [{ TargetField: 'Bio', Source: { Kind: 'prompt', Prompt: { PromptID: 'p-2', Data: {
49
+ fullName: { Kind: 'formula', Expression: "fields.First + ' ' + fields.Last" },
50
+ role: { Kind: 'static', Value: 'mathematician' },
51
+ } } } }] });
52
+ expect(captured).toEqual({ fullName: 'Ada Lovelace', role: 'mathematician' });
53
+ });
54
+ it('errors clearly when a prompt rule is used with no PromptResolver', async () => {
55
+ const [c] = await new FieldRulesEvaluator().ComputeChanges({ X: '' }, { Rules: [{ TargetField: 'X', Source: { Kind: 'prompt', Prompt: { PromptID: 'p' } } }] });
56
+ expect(c.Applied).toBe(false);
57
+ expect(c.Error).toMatch(/no PromptResolver/);
58
+ });
59
+ });
60
+ describe('FieldRulesEvaluator.ComputeChanges', () => {
61
+ const run = async (record, ruleSet, resolver) => new FieldRulesEvaluator({ LookupResolver: resolver }).ComputeChanges(record, ruleSet);
62
+ describe('value sources', () => {
63
+ it('static: sets a literal value and flags the change', async () => {
64
+ const [c] = await run({ Status: 'Active' }, { Rules: [{ TargetField: 'Status', Source: { Kind: 'static', Value: 'Inactive' } }] });
65
+ expect(c).toMatchObject({ Field: 'Status', OldValue: 'Active', NewValue: 'Inactive', Changed: true, Applied: true });
66
+ });
67
+ it('static: equal value is Applied but not Changed', async () => {
68
+ const [c] = await run({ Status: 'Active' }, { Rules: [{ TargetField: 'Status', Source: { Kind: 'static', Value: 'Active' } }] });
69
+ expect(c).toMatchObject({ Changed: false, Applied: true });
70
+ });
71
+ it('field: copies another field', async () => {
72
+ const [c] = await run({ A: 'x', B: 'y' }, { Rules: [{ TargetField: 'A', Source: { Kind: 'field', Field: 'B' } }] });
73
+ expect(c.NewValue).toBe('y');
74
+ });
75
+ it('formula: computes from record fields', async () => {
76
+ const [c] = await run({ FirstName: 'Ada', LastName: 'Lovelace', FullName: '' }, { Rules: [{ TargetField: 'FullName', Source: { Kind: 'formula', Expression: "fields.FirstName + ' ' + fields.LastName" } }] });
77
+ expect(c.NewValue).toBe('Ada Lovelace');
78
+ });
79
+ it('lookup: resolves via the injected resolver and passes through the matched value', async () => {
80
+ const seen = [];
81
+ const [c] = await run({ CompanyName: 'Acme', CompanyID: null }, { Rules: [{ TargetField: 'CompanyID', Source: { Kind: 'lookup', Lookup: { Entity: 'Accounts', MatchField: 'Name', MatchValue: { Kind: 'field', Field: 'CompanyName' }, ReturnField: 'ID' } } }] }, async (l) => { seen.push(l); return 'acct-123'; });
82
+ expect(c.NewValue).toBe('acct-123');
83
+ expect(seen[0]).toEqual({ Entity: 'Accounts', MatchField: 'Name', MatchValue: 'Acme', ReturnField: 'ID' });
84
+ });
85
+ it('lookup: falls back to Default when the resolver returns undefined', async () => {
86
+ const [c] = await run({ Name: 'x', RegionID: null }, { Rules: [{ TargetField: 'RegionID', Source: { Kind: 'lookup', Lookup: { Entity: 'Regions', MatchField: 'Name', MatchValue: { Kind: 'static', Value: 'Nowhere' }, ReturnField: 'ID', Default: 'unknown' } } }] }, async () => undefined);
87
+ expect(c.NewValue).toBe('unknown');
88
+ });
89
+ it('lookup: errors clearly when no resolver is provided', async () => {
90
+ const [c] = await run({ X: null }, { Rules: [{ TargetField: 'X', Source: { Kind: 'lookup', Lookup: { Entity: 'E', MatchField: 'F', MatchValue: { Kind: 'static', Value: 1 }, ReturnField: 'ID' } } }] });
91
+ expect(c.Applied).toBe(false);
92
+ expect(c.Error).toMatch(/no LookupResolver/);
93
+ });
94
+ });
95
+ describe('conditions', () => {
96
+ it('applies the rule only when the condition is true', async () => {
97
+ const ruleSet = { Rules: [{ TargetField: 'Tier', Source: { Kind: 'static', Value: 'Gold' }, Condition: 'Revenue > 1000' }] };
98
+ const [hot] = await run({ Revenue: 5000, Tier: 'Bronze' }, ruleSet);
99
+ const [cold] = await run({ Revenue: 10, Tier: 'Bronze' }, ruleSet);
100
+ expect(hot).toMatchObject({ Applied: true, NewValue: 'Gold' });
101
+ expect(cold).toMatchObject({ Applied: false, SkipReason: expect.stringMatching(/condition/) });
102
+ });
103
+ it('reports a condition expression error without throwing', async () => {
104
+ const [c] = await run({ X: 1 }, { Rules: [{ TargetField: 'X', Source: { Kind: 'static', Value: 2 }, Condition: 'this is not valid ===' }] });
105
+ expect(c.Applied).toBe(false);
106
+ expect(c.Error).toMatch(/condition/);
107
+ });
108
+ });
109
+ describe('transform pipeline on the computed value', () => {
110
+ it('chains transforms (formula → coerce → custom)', async () => {
111
+ const [c] = await run({ Price: '19.99', Total: 0 }, { Rules: [{
112
+ TargetField: 'Total',
113
+ Source: { Kind: 'field', Field: 'Price' },
114
+ Transforms: [
115
+ { Type: 'coerce', Config: { TargetType: 'number' } },
116
+ { Type: 'custom', Config: { Expression: 'value * 2' } },
117
+ ],
118
+ }] });
119
+ expect(c.NewValue).toBeCloseTo(39.98);
120
+ });
121
+ it('OnError=Null nulls the field when a transform throws', async () => {
122
+ const [c] = await run({ Amount: 'not-a-number', Out: 'x' }, { Rules: [{ TargetField: 'Out', Source: { Kind: 'field', Field: 'Amount' }, Transforms: [{ Type: 'coerce', Config: { TargetType: 'number' }, OnError: 'Null' }] }] });
123
+ expect(c.NewValue).toBeNull();
124
+ });
125
+ });
126
+ it('computes ALL rules independently (multi-rule preview)', async () => {
127
+ const changes = await run({ A: 1, B: 2, C: 3 }, { Rules: [
128
+ { TargetField: 'A', Source: { Kind: 'static', Value: 10 } },
129
+ { TargetField: 'B', Source: { Kind: 'static', Value: 2 } }, // unchanged
130
+ { TargetField: 'C', Source: { Kind: 'static', Value: 30 }, Condition: 'A == 1' },
131
+ ] });
132
+ expect(changes.map((c) => [c.Field, c.Changed, c.Applied])).toEqual([
133
+ ['A', true, true],
134
+ ['B', false, true],
135
+ ['C', true, true],
136
+ ]);
137
+ });
138
+ });
139
+ describe('FieldTransformEngine (each transform type)', () => {
140
+ const e = new FieldTransformEngine();
141
+ const step = (value, ...steps) => e.ExecutePipeline(value, {}, steps).Value;
142
+ it('regex / split / substring', () => {
143
+ expect(step('a-b-c', { Type: 'regex', Config: { Pattern: '-', Replacement: '_', Flags: 'g' } })).toBe('a_b_c');
144
+ expect(step('a,b,c', { Type: 'split', Config: { Delimiter: ',', Index: 1 } })).toBe('b');
145
+ expect(step('hello', { Type: 'substring', Config: { Start: 1, Length: 3 } })).toBe('ell');
146
+ });
147
+ it('combine pulls from fields', () => {
148
+ expect(e.ExecutePipeline(null, { A: 'x', B: 'y' }, [{ Type: 'combine', Config: { SourceFields: ['A', 'B'], Separator: '-' } }]).Value).toBe('x-y');
149
+ });
150
+ it('lookup (in-memory map, case-insensitive) + default', () => {
151
+ expect(step('ACTIVE', { Type: 'lookup', Config: { Map: { active: 1 }, Default: 0 } })).toBe(1);
152
+ expect(step('other', { Type: 'lookup', Config: { Map: { active: 1 }, Default: 0 } })).toBe(0);
153
+ });
154
+ it('coerce number/boolean/string', () => {
155
+ expect(step('42', { Type: 'coerce', Config: { TargetType: 'number' } })).toBe(42);
156
+ expect(step('yes', { Type: 'coerce', Config: { TargetType: 'boolean' } })).toBe(true);
157
+ expect(step(7, { Type: 'coerce', Config: { TargetType: 'string' } })).toBe('7');
158
+ });
159
+ it('OnError=Skip drops the field; OnError=Fail propagates', () => {
160
+ expect(e.ExecutePipeline('x', {}, [{ Type: 'coerce', Config: { TargetType: 'number' }, OnError: 'Skip' }]).Skipped).toBe(true);
161
+ expect(() => e.ExecutePipeline('x', {}, [{ Type: 'coerce', Config: { TargetType: 'number' }, OnError: 'Fail' }])).toThrow();
162
+ });
163
+ it('a malformed custom expression compiles once and is reported', () => {
164
+ expect(() => e.ExecutePipeline('x', {}, [{ Type: 'custom', Config: { Expression: ')(' }, OnError: 'Fail' }])).toThrow();
165
+ });
166
+ });
167
+ //# sourceMappingURL=FieldRulesEvaluator.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldRulesEvaluator.test.js","sourceRoot":"","sources":["../../src/__tests__/FieldRulesEvaluator.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAG5F,QAAQ,CAAC,mEAAmE,EAAE,GAAG,EAAE;IAC/E,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QAClE,MAAM,IAAI,GAAc,EAAE,CAAC;QAC3B,sBAAsB,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;YAC1D,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;YAC7B,OAAO,eAAe,CAAC;QAC3B,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,mBAAmB;QACjF,MAAM,GAAG,GAAG,IAAI,oBAAoB,EAAE,CAAC,eAAe,CAAC,SAAS,EAAE,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;QACvH,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QACxC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;IAC3E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oEAAoE,EAAE,GAAG,EAAE;QAC1E,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,oBAAoB,EAAE,CAAC,eAAe,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;aAC1H,OAAO,CAAC,gCAAgC,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,qEAAqE,EAAE,GAAG,EAAE;IACjF,EAAE,CAAC,4DAA4D,EAAE,KAAK,IAAI,EAAE;QACxE,MAAM,IAAI,GAA6B,EAAE,CAAC;QAC1C,MAAM,SAAS,GAAG,IAAI,mBAAmB,CAAC,EAAE,sBAAsB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC7H,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,SAAS,CAAC,cAAc,CACtC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,EAAE,EACzB,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE,cAAc,EAAE,EAAE,gBAAgB,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,EAAE,CACxH,CAAC;QACF,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QACxC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IAC7F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;QACvE,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,mBAAmB,EAAE,CAAC,cAAc,CACtD,EAAE,CAAC,EAAE,EAAE,EAAE,EACT,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE,EAAE,CAAC,EAAE,CACxE,CAAC;QACF,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9B,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,sCAAsC,EAAE,GAAG,EAAE;IAClD,EAAE,CAAC,sEAAsE,EAAE,KAAK,IAAI,EAAE;QAClF,MAAM,IAAI,GAAqB,EAAE,CAAC;QAClC,MAAM,SAAS,GAAG,IAAI,mBAAmB,CAAC,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC3H,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,SAAS,CAAC,cAAc,CACtC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,EAAE,EAAE,EACnD,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,EAAE,CACnG,CAAC;QACF,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;QAC9C,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,eAAe;IACtG,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wFAAwF,EAAE,KAAK,IAAI,EAAE;QACpG,IAAI,QAAQ,GAA4B,EAAE,CAAC;QAC3C,MAAM,SAAS,GAAG,IAAI,mBAAmB,CAAC,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,GAAG,QAAQ,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAChH,MAAM,SAAS,CAAC,cAAc,CAC1B,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,EAAE,EAAE,EAC3C,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE;gCACvF,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,kCAAkC,EAAE;gCAC7E,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,eAAe,EAAE;6BACnD,EAAE,EAAE,EAAE,CAAC,EAAE,CACb,CAAC;QACF,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,cAAc,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,CAAC;IAClF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kEAAkE,EAAE,KAAK,IAAI,EAAE;QAC9E,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,mBAAmB,EAAE,CAAC,cAAc,CACtD,EAAE,CAAC,EAAE,EAAE,EAAE,EACT,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,CAC3F,CAAC;QACF,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9B,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,oCAAoC,EAAE,GAAG,EAAE;IAChD,MAAM,GAAG,GAAG,KAAK,EAAE,MAA+B,EAAE,OAAqB,EAAE,QAAkD,EAAE,EAAE,CAC7H,IAAI,mBAAmB,CAAC,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,CAAC,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAE1F,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;QAC3B,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;YAC/D,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;YACnI,MAAM,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QACzH,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;YAC5D,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;YACjI,MAAM,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,6BAA6B,EAAE,KAAK,IAAI,EAAE;YACzC,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;YACpH,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;YAClD,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,CACjB,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,EAAE,EACxD,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,0CAA0C,EAAE,EAAE,CAAC,EAAE,CAChI,CAAC;YACF,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,iFAAiF,EAAE,KAAK,IAAI,EAAE;YAC7F,MAAM,IAAI,GAAqB,EAAE,CAAC;YAClC,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,CACjB,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,EACxC,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,EAAE,EACjM,KAAK,EAAE,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC,CACpD,CAAC;YACF,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YACpC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/G,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,mEAAmE,EAAE,KAAK,IAAI,EAAE;YAC/E,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,CACjB,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,EAC7B,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,EAAE,EAChN,KAAK,IAAI,EAAE,CAAC,SAAS,CACxB,CAAC;YACF,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,qDAAqD,EAAE,KAAK,IAAI,EAAE;YACjE,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;YACzM,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC9B,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;QACxB,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;YAC9D,MAAM,OAAO,GAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,gBAAgB,EAAE,CAAC,EAAE,CAAC;YAC3I,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,OAAO,CAAC,CAAC;YACpE,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,GAAG,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,OAAO,CAAC,CAAC;YACnE,MAAM,CAAC,GAAG,CAAC,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;YAC/D,MAAM,CAAC,IAAI,CAAC,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,CAAC,cAAc,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACnG,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;YACnE,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,uBAAuB,EAAE,CAAC,EAAE,CAAC,CAAC;YAC7I,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC9B,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,0CAA0C,EAAE,GAAG,EAAE;QACtD,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;YAC3D,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,CACjB,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,EAC5B,EAAE,KAAK,EAAE,CAAC;wBACN,WAAW,EAAE,OAAO;wBACpB,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE;wBACzC,UAAU,EAAE;4BACR,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE;4BACpD,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,WAAW,EAAE,EAAE;yBAC1D;qBACJ,CAAC,EAAE,CACP,CAAC;YACF,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QAC1C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;YAClE,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,CACjB,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,GAAG,EAAE,EACpC,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CACvK,CAAC;YACF,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;QAClC,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;QACnE,MAAM,OAAO,GAAG,MAAM,GAAG,CACrB,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EACpB,EAAE,KAAK,EAAE;gBACL,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE;gBAC3D,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,YAAY;gBACxE,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE;aACnF,EAAE,CACN,CAAC;QACF,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;YAChE,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC;YACjB,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC;YAClB,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC;SACpB,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,4CAA4C,EAAE,GAAG,EAAE;IACxD,MAAM,CAAC,GAAG,IAAI,oBAAoB,EAAE,CAAC;IACrC,MAAM,IAAI,GAAG,CAAC,KAAc,EAAE,GAAG,KAA6D,EAAE,EAAE,CAAC,CAAC,CAAC,eAAe,CAAC,KAAK,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC,KAAK,CAAC;IAE7I,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC/G,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACzF,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,YAAY,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC1D,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC/F,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClG,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAClF,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtF,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACpF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC7D,MAAM,CAAC,CAAC,CAAC,eAAe,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/H,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,eAAe,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;IAChI,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;QACnE,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,eAAe,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;IAC5H,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=KeyedSerialTaskQueue.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"KeyedSerialTaskQueue.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/KeyedSerialTaskQueue.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { KeyedSerialTaskQueue } from '../KeyedSerialTaskQueue.js';
3
+ const delay = (ms) => new Promise((r) => setTimeout(r, ms));
4
+ describe('KeyedSerialTaskQueue', () => {
5
+ it('serializes tasks for the same key in enqueue order', async () => {
6
+ const q = new KeyedSerialTaskQueue();
7
+ const key = {};
8
+ const order = [];
9
+ q.enqueue(key, async () => { await delay(20); order.push(1); });
10
+ q.enqueue(key, async () => { await delay(5); order.push(2); });
11
+ q.enqueue(key, async () => { order.push(3); });
12
+ await q.flush();
13
+ expect(order).toEqual([1, 2, 3]);
14
+ });
15
+ it('runs tasks for different keys concurrently', async () => {
16
+ const q = new KeyedSerialTaskQueue();
17
+ const order = [];
18
+ q.enqueue({}, async () => { await delay(30); order.push('slow'); });
19
+ q.enqueue({}, async () => { order.push('fast'); });
20
+ await q.flush();
21
+ // Different keys → concurrent → the fast one finishes first despite enqueuing second.
22
+ expect(order).toEqual(['fast', 'slow']);
23
+ });
24
+ it('is fire-and-forget — a thrown task resolves undefined, never rejects', async () => {
25
+ const q = new KeyedSerialTaskQueue();
26
+ await expect(q.enqueue({}, async () => { throw new Error('boom'); })).resolves.toBeUndefined();
27
+ });
28
+ it('resolves the task value on success and undefined on isOk-falsy', async () => {
29
+ const q = new KeyedSerialTaskQueue();
30
+ await expect(q.enqueue({}, async () => 'val', { isOk: () => true })).resolves.toBe('val');
31
+ await expect(q.enqueue({}, async () => false, { isOk: (v) => v === true })).resolves.toBeUndefined();
32
+ });
33
+ it('flush reports failures (reject OR isOk-falsy) and rejections (threw) + routes to onError', async () => {
34
+ const errors = [];
35
+ const q = new KeyedSerialTaskQueue({ onError: (e) => errors.push(e) });
36
+ q.enqueue({}, async () => 'ok');
37
+ q.enqueue({}, async () => { throw new Error('boom'); });
38
+ q.enqueue({}, async () => false, { isOk: (v) => v === true });
39
+ const res = await q.flush();
40
+ expect(res).toEqual({ failures: 2, rejections: 1 });
41
+ expect(errors.length).toBe(2);
42
+ });
43
+ it('drains on re-flush — a second flush only counts tasks enqueued after the first', async () => {
44
+ const q = new KeyedSerialTaskQueue();
45
+ q.enqueue({}, async () => { throw new Error('a'); });
46
+ expect(await q.flush()).toEqual({ failures: 1, rejections: 1 });
47
+ q.enqueue({}, async () => 'ok');
48
+ expect(await q.flush()).toEqual({ failures: 0, rejections: 0 });
49
+ });
50
+ it('flush with nothing pending resolves to zero counts', async () => {
51
+ const q = new KeyedSerialTaskQueue();
52
+ expect(await q.flush()).toEqual({ failures: 0, rejections: 0 });
53
+ });
54
+ it('a failed task does not break the chain for its key', async () => {
55
+ const q = new KeyedSerialTaskQueue();
56
+ const key = {};
57
+ const order = [];
58
+ q.enqueue(key, async () => { throw new Error('boom'); });
59
+ q.enqueue(key, async () => { order.push(2); });
60
+ await q.flush();
61
+ expect(order).toEqual([2]);
62
+ });
63
+ it('passes the label to onError', async () => {
64
+ const seen = [];
65
+ const q = new KeyedSerialTaskQueue({ onError: (_e, label) => seen.push(label) });
66
+ q.enqueue({}, async () => { throw new Error('boom'); }, { label: 'my-task' });
67
+ await q.flush();
68
+ expect(seen).toEqual(['my-task']);
69
+ });
70
+ it('flush awaits tasks enqueued before it began, not ones enqueued after', async () => {
71
+ const q = new KeyedSerialTaskQueue();
72
+ const done = [];
73
+ q.enqueue({}, async () => { done.push('first'); }); // fast — in the flush snapshot
74
+ const flushed = q.flush(); // snapshots only 'first'
75
+ q.enqueue({}, async () => { await delay(30); done.push('second'); }); // slow, after flush began
76
+ await flushed;
77
+ expect(done).toEqual(['first']); // flush awaited only 'first'; 'second' is still running
78
+ await q.flush(); // next window awaits 'second'
79
+ expect(done).toEqual(['first', 'second']);
80
+ });
81
+ it('is self-bounding — many settled tasks do not accumulate (flush still awaits in-flight)', async () => {
82
+ const q = new KeyedSerialTaskQueue();
83
+ for (let i = 0; i < 50; i++) {
84
+ q.enqueue({}, async () => { });
85
+ }
86
+ // All settle; a flush after they've drained reports zero and resolves promptly.
87
+ await delay(5);
88
+ expect(await q.flush()).toEqual({ failures: 0, rejections: 0 });
89
+ });
90
+ });
91
+ //# sourceMappingURL=KeyedSerialTaskQueue.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"KeyedSerialTaskQueue.test.js","sourceRoot":"","sources":["../../src/__tests__/KeyedSerialTaskQueue.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import type { EntityDocumentResolver, FieldChange, FieldRuleSet, LookupResolver, PromptResolver } from './rules.js';
2
+ /** Options for an evaluator instance. */
3
+ export interface FieldRulesEvaluatorOptions {
4
+ /** Resolver for `lookup` sources. Omit if no rule uses a lookup (a lookup rule then errors). */
5
+ LookupResolver?: LookupResolver;
6
+ /** Resolver for `prompt` sources. Omit if no rule uses a prompt (a prompt rule then errors). */
7
+ PromptResolver?: PromptResolver;
8
+ /** Resolver for `entityDocument` sources. Omit if no rule renders one (such a rule then errors). */
9
+ EntityDocumentResolver?: EntityDocumentResolver;
10
+ }
11
+ /**
12
+ * Computes the field changes a {@link FieldRuleSet} would make to a record — WITHOUT mutating anything.
13
+ *
14
+ * Returning a list of {@link FieldChange} (old → new per rule) is what makes a dry-run preview possible:
15
+ * the caller decides whether to apply. Conditions are evaluated with the safe expression evaluator;
16
+ * value formulas + the transform pipeline run through the shared {@link FieldTransformEngine}; entity
17
+ * lookups go through the injected resolver. The engine never touches a database itself.
18
+ */
19
+ export declare class FieldRulesEvaluator {
20
+ private readonly transforms;
21
+ private readonly conditions;
22
+ private readonly lookupResolver?;
23
+ private readonly promptResolver?;
24
+ private readonly entityDocumentResolver?;
25
+ constructor(options?: FieldRulesEvaluatorOptions);
26
+ /** Computes one {@link FieldChange} per rule for a record (a row of field name → current value). */
27
+ ComputeChanges(record: Record<string, unknown>, ruleSet: FieldRuleSet): Promise<FieldChange[]>;
28
+ private evaluateRule;
29
+ private checkCondition;
30
+ private resolveSource;
31
+ private equal;
32
+ private message;
33
+ }
34
+ //# sourceMappingURL=FieldRulesEvaluator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldRulesEvaluator.d.ts","sourceRoot":"","sources":["../../src/fieldRules/FieldRulesEvaluator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACR,sBAAsB,EACtB,WAAW,EAEX,YAAY,EAEZ,cAAc,EACd,cAAc,EACjB,MAAM,SAAS,CAAC;AAEjB,yCAAyC;AACzC,MAAM,WAAW,0BAA0B;IACvC,gGAAgG;IAChG,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,gGAAgG;IAChG,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,oGAAoG;IACpG,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;CACnD;AAED;;;;;;;GAOG;AACH,qBAAa,mBAAmB;IAC5B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8B;IACzD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiC;IAC5D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAiB;IACjD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAiB;IACjD,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAyB;gBAErD,OAAO,CAAC,EAAE,0BAA0B;IAMhD,oGAAoG;IACvF,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;YAQ7F,YAAY;IAoC1B,OAAO,CAAC,cAAc;YAWR,aAAa;IA6C3B,OAAO,CAAC,KAAK;IAOb,OAAO,CAAC,OAAO;CAGlB"}