@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.
- package/dist/generic/BaseEntitySaveQueue.d.ts +60 -0
- package/dist/generic/BaseEntitySaveQueue.d.ts.map +1 -0
- package/dist/generic/BaseEntitySaveQueue.js +106 -0
- package/dist/generic/BaseEntitySaveQueue.js.map +1 -0
- package/dist/generic/EntityFieldRules.d.ts +97 -0
- package/dist/generic/EntityFieldRules.d.ts.map +1 -0
- package/dist/generic/EntityFieldRules.js +201 -0
- package/dist/generic/EntityFieldRules.js.map +1 -0
- package/dist/generic/authTypes.d.ts +1 -0
- package/dist/generic/authTypes.d.ts.map +1 -1
- package/dist/generic/authTypes.js +1 -0
- package/dist/generic/authTypes.js.map +1 -1
- package/dist/generic/baseEngine.d.ts +70 -0
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +140 -1
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +35 -15
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/compositeKey.js +1 -1
- package/dist/generic/compositeKey.js.map +1 -1
- package/dist/generic/databaseProviderBase.js +1 -1
- package/dist/generic/databaseProviderBase.js.map +1 -1
- package/dist/generic/entityInfo.d.ts +9 -0
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +10 -1
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/graphqlTypeNames.d.ts.map +1 -1
- package/dist/generic/graphqlTypeNames.js +6 -1
- package/dist/generic/graphqlTypeNames.js.map +1 -1
- package/dist/generic/interfaces.d.ts +24 -0
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +5 -0
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/queryResultEnricher.d.ts +77 -0
- package/dist/generic/queryResultEnricher.d.ts.map +1 -0
- package/dist/generic/queryResultEnricher.js +41 -0
- package/dist/generic/queryResultEnricher.js.map +1 -0
- package/dist/generic/runQuery.d.ts +17 -0
- package/dist/generic/runQuery.d.ts.map +1 -1
- package/dist/generic/runQuery.js.map +1 -1
- package/dist/generic/telemetryManager.d.ts +27 -4
- package/dist/generic/telemetryManager.d.ts.map +1 -1
- package/dist/generic/telemetryManager.js +50 -11
- package/dist/generic/telemetryManager.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- 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
|
-
|
|
855
|
+
private _myData: SomeEntity[] = [];
|
|
856
856
|
|
|
857
|
-
|
|
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: '
|
|
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:
|