@memberjunction/core 5.42.0 → 5.44.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 (53) hide show
  1. package/dist/generic/BaseEntitySaveQueue.d.ts +60 -0
  2. package/dist/generic/BaseEntitySaveQueue.d.ts.map +1 -0
  3. package/dist/generic/BaseEntitySaveQueue.js +106 -0
  4. package/dist/generic/BaseEntitySaveQueue.js.map +1 -0
  5. package/dist/generic/EntityFieldRules.d.ts +97 -0
  6. package/dist/generic/EntityFieldRules.d.ts.map +1 -0
  7. package/dist/generic/EntityFieldRules.js +201 -0
  8. package/dist/generic/EntityFieldRules.js.map +1 -0
  9. package/dist/generic/authTypes.d.ts +1 -0
  10. package/dist/generic/authTypes.d.ts.map +1 -1
  11. package/dist/generic/authTypes.js +1 -0
  12. package/dist/generic/authTypes.js.map +1 -1
  13. package/dist/generic/baseEngine.d.ts +70 -0
  14. package/dist/generic/baseEngine.d.ts.map +1 -1
  15. package/dist/generic/baseEngine.js +140 -1
  16. package/dist/generic/baseEngine.js.map +1 -1
  17. package/dist/generic/baseEntity.d.ts.map +1 -1
  18. package/dist/generic/baseEntity.js +35 -15
  19. package/dist/generic/baseEntity.js.map +1 -1
  20. package/dist/generic/compositeKey.js +1 -1
  21. package/dist/generic/compositeKey.js.map +1 -1
  22. package/dist/generic/databaseProviderBase.js +1 -1
  23. package/dist/generic/databaseProviderBase.js.map +1 -1
  24. package/dist/generic/entityInfo.d.ts +9 -0
  25. package/dist/generic/entityInfo.d.ts.map +1 -1
  26. package/dist/generic/entityInfo.js +10 -1
  27. package/dist/generic/entityInfo.js.map +1 -1
  28. package/dist/generic/graphqlTypeNames.d.ts.map +1 -1
  29. package/dist/generic/graphqlTypeNames.js +6 -1
  30. package/dist/generic/graphqlTypeNames.js.map +1 -1
  31. package/dist/generic/interfaces.d.ts +24 -0
  32. package/dist/generic/interfaces.d.ts.map +1 -1
  33. package/dist/generic/interfaces.js.map +1 -1
  34. package/dist/generic/providerBase.d.ts.map +1 -1
  35. package/dist/generic/providerBase.js +5 -0
  36. package/dist/generic/providerBase.js.map +1 -1
  37. package/dist/generic/queryResultEnricher.d.ts +77 -0
  38. package/dist/generic/queryResultEnricher.d.ts.map +1 -0
  39. package/dist/generic/queryResultEnricher.js +41 -0
  40. package/dist/generic/queryResultEnricher.js.map +1 -0
  41. package/dist/generic/runQuery.d.ts +17 -0
  42. package/dist/generic/runQuery.d.ts.map +1 -1
  43. package/dist/generic/runQuery.js.map +1 -1
  44. package/dist/generic/telemetryManager.d.ts +27 -4
  45. package/dist/generic/telemetryManager.d.ts.map +1 -1
  46. package/dist/generic/telemetryManager.js +50 -11
  47. package/dist/generic/telemetryManager.js.map +1 -1
  48. package/dist/index.d.ts +3 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +3 -0
  51. package/dist/index.js.map +1 -1
  52. package/package.json +3 -3
  53. package/readme.md +148 -4
package/readme.md CHANGED
@@ -852,18 +852,23 @@ export class MyEngine extends BaseEngine<MyEngine> {
852
852
  return super.getInstance<MyEngine>();
853
853
  }
854
854
 
855
- public MyData: SomeEntity[] = [];
855
+ private _myData: SomeEntity[] = [];
856
856
 
857
- protected get Config(): BaseEnginePropertyConfig[] {
858
- return [
857
+ public get MyData(): SomeEntity[] {
858
+ return this.GetConfigData<SomeEntity>('_myData');
859
+ }
860
+
861
+ public async Config(forceRefresh?: boolean, contextUser?: UserInfo): Promise<void> {
862
+ const params: Partial<BaseEnginePropertyConfig>[] = [
859
863
  {
860
- PropertyName: 'MyData',
864
+ PropertyName: '_myData',
861
865
  EntityName: 'Some Entity',
862
866
  Filter: 'IsActive = 1',
863
867
  OrderBy: 'Name ASC',
864
868
  AutoRefresh: true // Auto-refresh on entity save/delete events
865
869
  }
866
870
  ];
871
+ return await this.Load(params, undefined, forceRefresh, contextUser);
867
872
  }
868
873
  }
869
874
 
@@ -879,6 +884,31 @@ Key features:
879
884
  - Local caching support via `CacheLocal` and `CacheLocalTTL` options
880
885
  - Supports both entity and dataset loading
881
886
 
887
+ #### Permission-Constrained Loading
888
+
889
+ When a user lacks read permissions on entities an engine loads, the engine enters a **permission-constrained** state instead of failing with errors or retrying endlessly. This is an all-or-nothing check — if any entity config is denied, all configs for that engine are skipped.
890
+
891
+ The `GetConfigData<E>(propertyName)` method is the canonical way for engine getters to expose loaded data. It checks the data map for permission denial and throws a `PermissionConstrainedError` if the config was skipped, preventing consumers from silently operating on empty arrays.
892
+
893
+ ```typescript
894
+ // Consumer that wants graceful degradation (optional feature)
895
+ if (!AIEngineBase.Instance.IsPermissionConstrained) {
896
+ const models = AIEngineBase.Instance.Models;
897
+ // ... render AI features
898
+ } else {
899
+ // ... hide AI features, show notice
900
+ }
901
+
902
+ // Consumer that requires the data (hard error if missing)
903
+ const queries = QueryEngine.Instance.Queries; // throws PermissionConstrainedError if denied
904
+ ```
905
+
906
+ | State | `Loaded` | `IsPermissionConstrained` | Behavior |
907
+ |---|---|---|---|
908
+ | Not loaded | `false` | `false` | `EnsureLoaded()` retries normally |
909
+ | Loaded normally | `true` | `false` | Normal operation |
910
+ | Permission-constrained | `true` | `true` | `GetConfigData()` throws `PermissionConstrainedError`, no retry, no entity event handling |
911
+
882
912
  ---
883
913
 
884
914
  ### BaseEngineRegistry — cross-engine cache reverse lookup
@@ -1383,6 +1413,39 @@ CodeNameFromString('First Name'); // 'FirstName'
1383
1413
 
1384
1414
  ---
1385
1415
 
1416
+ ## Fire-and-Forget Entity Saves (`BaseEntitySaveQueue`)
1417
+
1418
+ `BaseEntitySaveQueue` is the entity-aware façade over `@memberjunction/global`'s `KeyedSerialTaskQueue` for **non-blocking persistence** — writing observability/log rows (agent-run steps, action-execution logs, AI prompt runs, record-process details) without blocking the work that produced them on a DB round-trip.
1419
+
1420
+ ```typescript
1421
+ import { BaseEntitySaveQueue } from '@memberjunction/core';
1422
+
1423
+ const queue = new BaseEntitySaveQueue();
1424
+
1425
+ // Fire-and-forget INSERT of a freshly NewRecord()'d entity.
1426
+ queue.Insert(logEntity);
1427
+
1428
+ // Fire-and-forget UPDATE chained after that entity's INSERT. The mutation runs INSIDE the
1429
+ // post-INSERT task, so the INSERT's finalizeSave reload can never revert it.
1430
+ queue.Update(logEntity, (e) => { e.Set('EndedAt', new Date()); e.Set('Status', 'Completed'); });
1431
+
1432
+ // At a run/goal boundary, flush to await all pending saves + surface failure counts.
1433
+ const { failures } = await queue.Flush();
1434
+ ```
1435
+
1436
+ **Why the `Update(applyMutation)` shape matters.** A fire-and-forget INSERT serializes the entity's current fields and, on completion, `BaseEntity.finalizeSave` reloads the inserted row (`init()` + `SetMany`). Any field mutated on that same instance *while the INSERT is in flight* is reverted, and a force-persisted UPDATE then writes the stale values — the classic "stuck at Running" bug. Because the queue runs `applyMutation` **inside** the post-INSERT task, the mutation always lands after the reload, making that race **impossible by construction**.
1437
+
1438
+ | Method | Purpose |
1439
+ |---|---|
1440
+ | `Insert(entity)` | Fire-and-forget create. The entity instance is the serialization key, so a later `Update` of the same instance waits for it. |
1441
+ | `Update(entity, applyMutation?)` | Fire-and-forget, force-persisted (`IgnoreDirtyState`) update chained after the INSERT; `applyMutation` runs post-INSERT (race-safe). |
1442
+ | `Flush()` | Await all pending saves; returns `{ failures, rejections }`. Call at a run/goal boundary. |
1443
+ | `new BaseEntitySaveQueue({ onError })` | Route failure messages to a structured logger (e.g. a category/metadata logger) instead of the default `LogError`. |
1444
+
1445
+ Single-primary-key entities only; the queue logs (never throws) on a failed save, since these rows are observability and must not break the work that produced them.
1446
+
1447
+ ---
1448
+
1386
1449
  ## Error Handling
1387
1450
 
1388
1451
  RunView and RunQuery do NOT throw exceptions on failure. Always check `Success`:
@@ -1514,6 +1577,37 @@ This library is written in TypeScript and provides full type definitions. All ge
1514
1577
 
1515
1578
  ISC License - see LICENSE file for details.
1516
1579
 
1580
+ ## Remote Operations (the 4th Data Primitive)
1581
+
1582
+ `BaseRemotableOperation<TInput, TOutput>` (defined in this package) is a typed, provider-routed server capability invoked from **one call site** on both the client (marshalled over GraphQL) and the server (in-process) — the missing peer of the three primitives MJCore already gives you:
1583
+
1584
+ ```mermaid
1585
+ graph LR
1586
+ subgraph "MJ data primitives — one call site, provider-routed"
1587
+ A["BaseEntity<br/><i>record CRUD</i>"]
1588
+ B["RunView<br/><i>dynamic set reads</i>"]
1589
+ C["RunQuery<br/><i>stored queries</i>"]
1590
+ D["BaseRemotableOperation<br/><b>typed RPC</b>"]
1591
+ end
1592
+ style D fill:#8b5cf6,color:#fff,stroke:#6d28d9
1593
+ ```
1594
+
1595
+ `entity.Save()` · `rv.RunView()` · `rq.RunQuery()` · **`op.Execute()`** — same shape, same tier-agnostic DX.
1596
+
1597
+ Before this primitive, exposing one non-CRUD capability ("render a template", "run a process") to the browser meant hand-writing a stack — a TypeGraphQL resolver, a typed GraphQL client (or an inline `gql` string + a provider cast), an Angular wrapper, **and** the input/output types twice (client + server), kept in sync by hand. A Remote Operation replaces all of it with one typed object:
1598
+
1599
+ ```typescript
1600
+ // typed in, typed out — identical on client and server; a wrong field is a compile error
1601
+ const result = await new TemplateRunOperation().Execute({ templateID, data });
1602
+ result.Output?.output;
1603
+ ```
1604
+
1605
+ New operations are declared as `MJ: Remote Operations` metadata rows; CodeGen emits the typed base, and the body is written by hand (**Manual**), authored by an LLM from the row's `Description` and approved (**AI**), or left as emitted boilerplate (**Default**). Transport, auth, the long-running progress channel, and approval gating are written **once** in the framework and shared by every operation.
1606
+
1607
+ > **Visual before/after**: See the [**Remote Operations Showcase**](./docs/REMOTE_OPERATIONS_SHOWCASE.md) — a diagram-driven tour of the layers this removes, built from two real migrations. *(Best starting point for sharing with the team.)*
1608
+ >
1609
+ > **Full Guide**: See the [**Remote Operations Guide**](../../guides/REMOTE_OPERATIONS_GUIDE.md) for when to use it (vs. an Action or a bespoke resolver), the three authoring modes, calling conventions, the auth chain, and long-running progress.
1610
+
1517
1611
  ## Virtual Entities
1518
1612
 
1519
1613
  Virtual entities are **read-only entities backed by SQL views** rather than physical database tables. They appear in the metadata catalog alongside regular entities but have no underlying base table — only a base view. This makes them ideal for exposing aggregated data, cross-database views, or complex computed datasets as first-class entities.
@@ -1718,6 +1812,56 @@ const params = EntityInfo.BuildOrganicKeyViewParams(record, relatedEntity, organ
1718
1812
 
1719
1813
  > **Full Guide**: See [Organic Keys Guide](./docs/organic-keys.md) for the complete schema, all 4 query patterns, normalization strategies, CodeGen configuration, Angular UI integration, and an end-to-end setup walkthrough.
1720
1814
 
1815
+ ## Entity Field Rules
1816
+
1817
+ `EntityFieldRules` is the **metadata-aware** layer on top of the pure field-rules engine in
1818
+ [`@memberjunction/global`](../MJGlobal/README.md#field-rules-engine). The pure engine is deliberately
1819
+ metadata-blind — it computes a per-field diff from a plain `Record<string, unknown>` and an injected
1820
+ lookup resolver, so it runs anywhere. `EntityFieldRules` adds the things that only make sense when the
1821
+ **target is a real MJ entity** and that need this package's metadata layer:
1822
+
1823
+ | Adds | Why it needs core |
1824
+ |---|---|
1825
+ | **`Validate(entityName, ruleSet)`** — target field exists? writable (not PK/read-only/virtual)? source `field` refs valid? | `EntityInfo` / `EntityFieldInfo` |
1826
+ | **Type coercion** — a formula yielding `"42"` becomes numeric `42` for a numeric column | `EntityFieldInfo.TSType` |
1827
+ | **Built-in lookup resolver** for `lookup` rule sources | `RunView` |
1828
+ | **`ApplyToEntity(entity, ruleSet, { DryRun })`** — write the computed values + `Save()` (Record Changes captures before/after) | `BaseEntity` |
1829
+
1830
+ **Scope:** the *target is always an MJ entity*; the *source* may be the entity's own fields plus an
1831
+ optional injected `Context` (a data context, a query result, an agent's output, related-entity lookups)
1832
+ — all data you already hold. When the *other side* is a **live external system**, that is the domain of
1833
+ [`@memberjunction/integration`](../Integration/engine/README.md#field-mapping--the-shared-transform-engine),
1834
+ which uses the same pure transform engine. `EntityFieldRules` is a writer *to* entities, not a
1835
+ bidirectional mapper.
1836
+
1837
+ ```ts
1838
+ import { EntityFieldRules } from '@memberjunction/core';
1839
+ import type { FieldRuleSet } from '@memberjunction/global';
1840
+
1841
+ const ruleSet: FieldRuleSet = {
1842
+ Rules: [
1843
+ { TargetField: 'Description', Source: { Kind: 'formula', Expression: "fields.Name + ' (normalized)'" } },
1844
+ { TargetField: 'Status', Source: { Kind: 'static', Value: 'Inactive' }, Condition: 'DaysSinceActivity > 365' },
1845
+ ],
1846
+ };
1847
+
1848
+ // 1) Pre-flight (synchronous, safe to run in a UX on every edit)
1849
+ const check = EntityFieldRules.Validate('Accounts', ruleSet);
1850
+ if (!check.Valid) console.warn(check.Errors);
1851
+
1852
+ // 2) Dry-run preview (computes the diff, writes nothing)
1853
+ const rules = new EntityFieldRules(contextUser);
1854
+ const preview = await rules.ApplyToEntity(account, ruleSet, { DryRun: true });
1855
+ // preview.Changes → per-field old → new; preview.Saved === false
1856
+
1857
+ // 3) Apply for real (writes + Save → Record Changes versioning)
1858
+ const result = await rules.ApplyToEntity(account, ruleSet);
1859
+ ```
1860
+
1861
+ > For **bulk** updates across a view / list / filtered set, the `FieldRulesProcessor` in
1862
+ > `@memberjunction/record-set-processor` runs `EntityFieldRules` per record with batching, concurrency,
1863
+ > and dry-run — that's the rules-based bulk-update tool.
1864
+
1721
1865
  ## Documentation
1722
1866
 
1723
1867
  For detailed guides on specific topics, see the [docs/](./docs/) folder: