@memberjunction/lists 0.0.1 → 5.36.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/README.md +292 -28
- package/dist/AudienceResolver.d.ts +22 -0
- package/dist/AudienceResolver.d.ts.map +1 -0
- package/dist/AudienceResolver.js +25 -0
- package/dist/AudienceResolver.js.map +1 -0
- package/dist/ListOperations.d.ts +215 -0
- package/dist/ListOperations.d.ts.map +1 -0
- package/dist/ListOperations.js +829 -0
- package/dist/ListOperations.js.map +1 -0
- package/dist/ListSharing.d.ts +157 -0
- package/dist/ListSharing.d.ts.map +1 -0
- package/dist/ListSharing.js +568 -0
- package/dist/ListSharing.js.map +1 -0
- package/dist/deltaToken.d.ts +47 -0
- package/dist/deltaToken.d.ts.map +1 -0
- package/dist/deltaToken.js +123 -0
- package/dist/deltaToken.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -7
package/README.md
CHANGED
|
@@ -1,45 +1,309 @@
|
|
|
1
1
|
# @memberjunction/lists
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pure TypeScript core for MemberJunction Lists. Owns list materialization,
|
|
4
|
+
set algebra, delta computation, lineage tracking, refresh modes, drop
|
|
5
|
+
guards, sharing, and audience resolution. Framework-agnostic — no
|
|
6
|
+
GraphQL, HTTP, or Angular dependencies.
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
This is the **source of truth** for the type contracts that the rest of
|
|
9
|
+
the stack (the `ListOperationsResolver` in `@memberjunction/server`, the
|
|
10
|
+
`GraphQLListsClient` in `@memberjunction/graphql-dataprovider`, the
|
|
11
|
+
Phase-N list Actions in `@memberjunction/core-actions`, and the Angular
|
|
12
|
+
components in `@memberjunction/ng-list-management`) all import from.
|
|
6
13
|
|
|
7
|
-
|
|
14
|
+
## Installation
|
|
8
15
|
|
|
9
|
-
|
|
16
|
+
```bash
|
|
17
|
+
npm install @memberjunction/lists
|
|
18
|
+
```
|
|
10
19
|
|
|
11
|
-
|
|
12
|
-
1. Configure OIDC trusted publishing for the package name `@memberjunction/lists`
|
|
13
|
-
2. Enable secure, token-less publishing from CI/CD workflows
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
20
|
+
## What's in here
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
```
|
|
23
|
+
ListOperations → materialize / refresh / compose / preview-and-apply
|
|
24
|
+
ListSharing → direct shares, email invitations, audit log, capabilities
|
|
25
|
+
AudienceResolver → friendly-named re-export of ResolveSource (Communications)
|
|
26
|
+
deltaToken → HMAC-signed preview-token contract (server enforced)
|
|
27
|
+
types → ListSource, ListDelta, ListCapabilities, etc.
|
|
28
|
+
```
|
|
17
29
|
|
|
18
|
-
|
|
30
|
+
## Public API surface
|
|
19
31
|
|
|
20
|
-
|
|
32
|
+
### Core types (`types.ts`)
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
```typescript
|
|
35
|
+
type ListRefreshMode = 'Additive' | 'Sync';
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
37
|
+
type ListSource =
|
|
38
|
+
| { kind: 'list'; listId: string }
|
|
39
|
+
| { kind: 'view'; viewId: string; runtimeParams?: Record<string, unknown> }
|
|
40
|
+
| { kind: 'adhoc'; entityName: string; extraFilter: string };
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
interface ResolvedRecordSet {
|
|
43
|
+
EntityName: string;
|
|
44
|
+
RecordIds: string[]; // MJ: List Detail.RecordID format
|
|
45
|
+
TotalCount?: number;
|
|
46
|
+
}
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
48
|
+
interface ListDelta {
|
|
49
|
+
TargetListId: string | null;
|
|
50
|
+
EntityName: string;
|
|
51
|
+
ToAdd: string[];
|
|
52
|
+
ToRemove: string[];
|
|
53
|
+
Unchanged: string[];
|
|
54
|
+
Counts: { Add; Remove; Unchanged; SourceTotal; TargetTotal };
|
|
55
|
+
Warnings: ListDeltaWarning[];
|
|
56
|
+
DeltaToken: string; // HMAC, 5-min TTL
|
|
57
|
+
}
|
|
36
58
|
|
|
37
|
-
|
|
59
|
+
interface ApplyResult {
|
|
60
|
+
Success: boolean;
|
|
61
|
+
ResultCode: 'SUCCESS' | 'DROP_NOT_CONFIRMED' | 'STALE_DELTA' |
|
|
62
|
+
'INVALID_TOKEN' | 'PERMISSION_DENIED' | 'TARGET_NOT_FOUND' |
|
|
63
|
+
'PARTIAL_SUCCESS' | 'UNEXPECTED_ERROR';
|
|
64
|
+
Message: string;
|
|
65
|
+
CreatedListId?: string;
|
|
66
|
+
TargetListId?: string;
|
|
67
|
+
Counts?: { Added; Removed; Failed };
|
|
68
|
+
Errors?: string[];
|
|
69
|
+
}
|
|
70
|
+
```
|
|
38
71
|
|
|
39
|
-
|
|
40
|
-
- [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
|
|
41
|
-
- [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
|
72
|
+
### `ListOperations`
|
|
42
73
|
|
|
43
|
-
|
|
74
|
+
Constructor: `new ListOperations(contextUser, provider?)`.
|
|
44
75
|
|
|
45
|
-
|
|
76
|
+
```typescript
|
|
77
|
+
// Read-only
|
|
78
|
+
ResolveSource(source: ListSource): Promise<ResolvedRecordSet>;
|
|
79
|
+
GetListMembers(listId: string): Promise<ResolvedRecordSet>;
|
|
80
|
+
|
|
81
|
+
// Preview (never mutates) — returns a signed DeltaToken
|
|
82
|
+
ComputeDelta(target: ListSource | 'new', source: ListSource, mode: ListRefreshMode): Promise<ListDelta>;
|
|
83
|
+
ComputeSetOp(op: 'union' | 'intersection' | 'difference', inputs: ListSource[], target?: ListSource | 'new'): Promise<ListDelta>;
|
|
84
|
+
|
|
85
|
+
// Commit (server enforces token validity + drop guard)
|
|
86
|
+
ApplyDelta(delta: ListDelta, opts: { ConfirmDrops: boolean; DeltaToken: string }): Promise<ApplyResult>;
|
|
87
|
+
|
|
88
|
+
// High-level convenience
|
|
89
|
+
MaterializeFromView(viewId: string, opts: MaterializeOptions): Promise<ApplyResult>;
|
|
90
|
+
AddViewResultsToList(viewId: string, listId: string): Promise<ApplyResult>;
|
|
91
|
+
RefreshFromSource(listId: string, mode: ListRefreshMode, opts: { ConfirmDrops: boolean }): Promise<ApplyResult>;
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### The drop-row warning contract
|
|
95
|
+
|
|
96
|
+
Every mutating list operation goes through `ComputeDelta` first. The
|
|
97
|
+
returned `DeltaToken` is an HMAC over `{targetListId, source signature,
|
|
98
|
+
mode, timestamp}` with a 5-minute TTL.
|
|
99
|
+
|
|
100
|
+
`ApplyDelta` enforces, **server-side**, in this order:
|
|
101
|
+
|
|
102
|
+
1. Token signature + 5-min TTL — else `INVALID_TOKEN`.
|
|
103
|
+
2. Re-compute against current target state — else `STALE_DELTA`.
|
|
104
|
+
3. `Counts.Remove > 0 && !ConfirmDrops` — else `DROP_NOT_CONFIRMED`.
|
|
105
|
+
4. Editor permission on target (placeholder; Phase 2 wires the real
|
|
106
|
+
`ResourcePermission` check).
|
|
107
|
+
|
|
108
|
+
The Angular `<mj-list-delta-confirm>` dialog is the only UI path that
|
|
109
|
+
produces a valid token. The server rejects drops regardless of UI.
|
|
110
|
+
|
|
111
|
+
### `ListSharing`
|
|
112
|
+
|
|
113
|
+
Constructor: `new ListSharing(contextUser, provider?)`.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
type SharePermissionLevel = 'View' | 'Edit' | 'Owner';
|
|
117
|
+
|
|
118
|
+
type ShareTarget =
|
|
119
|
+
| { kind: 'user'; userId: string }
|
|
120
|
+
| { kind: 'role'; roleId: string };
|
|
121
|
+
|
|
122
|
+
// Direct shares (recipient already has an MJ account)
|
|
123
|
+
Share(args: { ListID; Target; PermissionLevel }): Promise<ShareResult>;
|
|
124
|
+
Unshare(permissionId: string): Promise<ShareResult>;
|
|
125
|
+
GetSharesForList(listId: string): Promise<ListShareSummary[]>;
|
|
126
|
+
GetListsSharedWithUser(): Promise<SharedListSummary[]>;
|
|
127
|
+
|
|
128
|
+
// Email invitations (recipient may not yet have an account)
|
|
129
|
+
Invite(args: { ListID; Email; Role: 'Editor' | 'Viewer'; TtlMs? }): Promise<InviteResult>;
|
|
130
|
+
AcceptInvitation(token: string): Promise<AcceptInvitationResult>;
|
|
131
|
+
RevokeInvitation(invitationId: string): Promise<ShareResult>;
|
|
132
|
+
|
|
133
|
+
// Permission introspection for UI gating
|
|
134
|
+
ResolveEffectivePermission(listId: string): Promise<SharePermissionLevel | null>;
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Every mutation also writes an `MJ: Audit Logs` entry. Audit log types
|
|
138
|
+
are seeded via metadata sync (see
|
|
139
|
+
[`metadata/audit-log-types/.list-sharing-audit-types.json`](../../metadata/audit-log-types/.list-sharing-audit-types.json)).
|
|
140
|
+
|
|
141
|
+
Direct user-shares fan out via the platform `CreateShareNotification`
|
|
142
|
+
dispatcher (registered by `@memberjunction/notifications`) — recipients
|
|
143
|
+
get in-app + email notifications per their preferences. Role-target
|
|
144
|
+
shares are not fanned out per-user (that's a separate worker).
|
|
145
|
+
|
|
146
|
+
### Capability gating
|
|
147
|
+
|
|
148
|
+
UI components should derive button visibility from the caller's
|
|
149
|
+
effective permission level, not from raw entity-permission flags. The
|
|
150
|
+
mapping is centralized so UI and server agree:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
const level: SharePermissionLevel | null =
|
|
154
|
+
await sharing.ResolveEffectivePermission(listId);
|
|
155
|
+
|
|
156
|
+
const caps: ListCapabilities = capabilitiesForLevel(level);
|
|
157
|
+
// caps.CanEdit / CanRefresh / CanShare / CanDelete / CanRunOperations
|
|
158
|
+
// Server-side enforcement remains the source of truth — these are a
|
|
159
|
+
// UX convenience so users don't see buttons they'll be rejected on.
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Mapping per mockup 19:
|
|
163
|
+
|
|
164
|
+
| Level | Read | Edit | Refresh | Share | Delete | Operations |
|
|
165
|
+
| ------- | :--: | :--: | :-----: | :---: | :----: | :--------: |
|
|
166
|
+
| Owner | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
|
167
|
+
| Editor | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ |
|
|
168
|
+
| Viewer | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
|
|
169
|
+
| null | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
|
|
170
|
+
|
|
171
|
+
### `AudienceResolver`
|
|
172
|
+
|
|
173
|
+
Friendly-named entry point for resolving an `AudienceSource` (= `ListSource`)
|
|
174
|
+
to a `ResolvedRecordSet`. Identical semantics to
|
|
175
|
+
`ListOperations.ResolveSource`, exposed under a name that reads
|
|
176
|
+
naturally for Communications and other audience-aware code.
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const resolver = new AudienceResolver(contextUser, provider);
|
|
180
|
+
const result = await resolver.Resolve({ kind: 'list', listId });
|
|
181
|
+
// result.RecordIds — array of record IDs in the target audience
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### `deltaToken`
|
|
185
|
+
|
|
186
|
+
Internal helpers exposed for resolver-layer use. Server bootstrap must
|
|
187
|
+
set the HMAC secret:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import { SetDeltaTokenSecret } from '@memberjunction/lists';
|
|
191
|
+
|
|
192
|
+
SetDeltaTokenSecret(process.env.MJ_LIST_DELTA_SECRET);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Falls back to the env var `MJ_LIST_DELTA_SECRET` if no explicit call is
|
|
196
|
+
made. Refuses to use a default secret — silent fallback would let a
|
|
197
|
+
misconfigured server hand out forgeable tokens.
|
|
198
|
+
|
|
199
|
+
## Examples
|
|
200
|
+
|
|
201
|
+
### Save a view's current results as a new list with lineage
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
const ops = new ListOperations(currentUser, provider);
|
|
205
|
+
const result = await ops.MaterializeFromView('view-abc', {
|
|
206
|
+
ListName: 'Q4 VIP Donors',
|
|
207
|
+
Description: 'Materialized from the Q4 Donor View',
|
|
208
|
+
CategoryId: 'cat-marketing',
|
|
209
|
+
RememberLineage: true, // can refresh from source later
|
|
210
|
+
UseSnapshot: false, // refresh re-reads the live view
|
|
211
|
+
RefreshMode: 'Additive', // default refresh mode for this list
|
|
212
|
+
});
|
|
213
|
+
// result.CreatedListId, result.Counts.Added
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Refresh a lineage-bearing list (Sync mode, will drop missing rows)
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
const result = await ops.RefreshFromSource(listId, 'Sync', {
|
|
220
|
+
ConfirmDrops: true, // required when Sync mode would remove records
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Preview a 3-way intersection without committing
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
const delta = await ops.ComputeSetOp('intersection', [
|
|
228
|
+
{ kind: 'list', listId: 'list-a' },
|
|
229
|
+
{ kind: 'view', viewId: 'view-b' },
|
|
230
|
+
{ kind: 'view', viewId: 'view-c' },
|
|
231
|
+
]);
|
|
232
|
+
// delta.ToAdd has the resulting record IDs; nothing has been saved.
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Share a list with an Editor
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
const sharing = new ListSharing(currentUser, provider);
|
|
239
|
+
const result = await sharing.Share({
|
|
240
|
+
ListID: 'list-abc',
|
|
241
|
+
Target: { kind: 'user', userId: 'user-xyz' },
|
|
242
|
+
PermissionLevel: 'Edit',
|
|
243
|
+
});
|
|
244
|
+
// In-app + email notification fires automatically via the platform handler.
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Invite someone by email
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
const inv = await sharing.Invite({
|
|
251
|
+
ListID: 'list-abc',
|
|
252
|
+
Email: 'recipient@example.com',
|
|
253
|
+
Role: 'Editor',
|
|
254
|
+
TtlMs: 7 * 24 * 60 * 60 * 1000,
|
|
255
|
+
});
|
|
256
|
+
// inv.Token + inv.ExpiresAt — the caller builds the accept URL.
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Architecture
|
|
260
|
+
|
|
261
|
+
Sits at the bottom of a four-layer stack:
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
┌────────────────────────────────────────────────────────────┐
|
|
265
|
+
│ Angular UI (@memberjunction/ng-list-management, │
|
|
266
|
+
│ @memberjunction/ng-explorer-core, etc.) │
|
|
267
|
+
└────────────────────────────┬───────────────────────────────┘
|
|
268
|
+
↓ uses
|
|
269
|
+
┌────────────────────────────────────────────────────────────┐
|
|
270
|
+
│ GraphQLListsClient (@memberjunction/graphql-dataprovider) │
|
|
271
|
+
└────────────────────────────┬───────────────────────────────┘
|
|
272
|
+
↓ wire format
|
|
273
|
+
┌────────────────────────────────────────────────────────────┐
|
|
274
|
+
│ ListOperationsResolver (@memberjunction/server) │
|
|
275
|
+
└────────────────────────────┬───────────────────────────────┘
|
|
276
|
+
↓ delegates to
|
|
277
|
+
┌────────────────────────────────────────────────────────────┐
|
|
278
|
+
│ ListOperations / ListSharing / AudienceResolver ← this │
|
|
279
|
+
│ (@memberjunction/lists) │
|
|
280
|
+
└────────────────────────────────────────────────────────────┘
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
Every layer is a thin pass-through. The Actions in
|
|
284
|
+
`@memberjunction/core-actions` (`MaterializeListFromViewAction`,
|
|
285
|
+
`ShareListAction`, etc.) are parallel consumers — they import directly
|
|
286
|
+
from this package so AI agents, workflows, and scheduled jobs get the
|
|
287
|
+
same capability as the UI.
|
|
288
|
+
|
|
289
|
+
## Dependencies
|
|
290
|
+
|
|
291
|
+
- `@memberjunction/core` — `Metadata`, `RunView`, `UserInfo`, `IMetadataProvider`
|
|
292
|
+
- `@memberjunction/core-entities` — `MJListEntity`, `MJResourcePermissionEntity`, etc.
|
|
293
|
+
- `@memberjunction/global` — class registration utilities
|
|
294
|
+
|
|
295
|
+
No HTTP, no GraphQL, no Angular.
|
|
296
|
+
|
|
297
|
+
## Testing
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
cd packages/Lists
|
|
301
|
+
npm run test
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
76 vitest cases across `deltaToken` (HMAC roundtrip + secret resolution),
|
|
305
|
+
`ListOperations` (math, set-ops, drop guard, materialize, refresh modes,
|
|
306
|
+
snapshot vs live), `AudienceResolver`, and `ListSharing` (share / invite
|
|
307
|
+
/ accept / revoke lifecycle, capability mapping).
|
|
308
|
+
|
|
309
|
+
Coverage tracked at 88%+ lines; reports via `npm run test:coverage`.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
2
|
+
import type { AudienceSource, ResolvedRecordSet } from '@memberjunction/lists-base';
|
|
3
|
+
/**
|
|
4
|
+
* Friendly-named entry point for resolving an `AudienceSource` to a
|
|
5
|
+
* `ResolvedRecordSet`. Thin re-export over `ListOperations.ResolveSource`
|
|
6
|
+
* — semantically identical, but callers reading audience-related code
|
|
7
|
+
* don't need to know about the list-operations machinery underneath.
|
|
8
|
+
*
|
|
9
|
+
* Stateless on purpose; rebuild per call. Multi-provider safe: pass the
|
|
10
|
+
* provider through and the underlying `ListOperations` will scope to it.
|
|
11
|
+
*/
|
|
12
|
+
export declare class AudienceResolver {
|
|
13
|
+
private readonly contextUser;
|
|
14
|
+
private readonly provider;
|
|
15
|
+
constructor(contextUser: UserInfo, provider?: IMetadataProvider);
|
|
16
|
+
/**
|
|
17
|
+
* Resolve an `AudienceSource` to the record IDs it represents +
|
|
18
|
+
* the entity those IDs belong to. Never mutates.
|
|
19
|
+
*/
|
|
20
|
+
Resolve(source: AudienceSource): Promise<ResolvedRecordSet>;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=AudienceResolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AudienceResolver.d.ts","sourceRoot":"","sources":["../src/AudienceResolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAIpF;;;;;;;;GAQG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAW;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgC;gBAE7C,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,iBAAiB;IAK/D;;;OAGG;IACU,OAAO,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAIzE"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ListOperations } from './ListOperations.js';
|
|
2
|
+
/**
|
|
3
|
+
* Friendly-named entry point for resolving an `AudienceSource` to a
|
|
4
|
+
* `ResolvedRecordSet`. Thin re-export over `ListOperations.ResolveSource`
|
|
5
|
+
* — semantically identical, but callers reading audience-related code
|
|
6
|
+
* don't need to know about the list-operations machinery underneath.
|
|
7
|
+
*
|
|
8
|
+
* Stateless on purpose; rebuild per call. Multi-provider safe: pass the
|
|
9
|
+
* provider through and the underlying `ListOperations` will scope to it.
|
|
10
|
+
*/
|
|
11
|
+
export class AudienceResolver {
|
|
12
|
+
constructor(contextUser, provider) {
|
|
13
|
+
this.contextUser = contextUser;
|
|
14
|
+
this.provider = provider;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolve an `AudienceSource` to the record IDs it represents +
|
|
18
|
+
* the entity those IDs belong to. Never mutates.
|
|
19
|
+
*/
|
|
20
|
+
async Resolve(source) {
|
|
21
|
+
const ops = new ListOperations(this.contextUser, this.provider);
|
|
22
|
+
return ops.ResolveSource(source);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=AudienceResolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AudienceResolver.js","sourceRoot":"","sources":["../src/AudienceResolver.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElD;;;;;;;;GAQG;AACH,MAAM,OAAO,gBAAgB;IAI3B,YAAY,WAAqB,EAAE,QAA4B;QAC7D,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,OAAO,CAAC,MAAsB;QACzC,MAAM,GAAG,GAAG,IAAI,cAAc,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QAChE,OAAO,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC;CACF"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
2
|
+
import type { ApplyResult, ListDelta, ListRefreshMode, ListSource, MaterializeOptions, ResolvedRecordSet, SetOpKind } from '@memberjunction/lists-base';
|
|
3
|
+
/** Target argument shared by `ComputeDelta` / `ComputeSetOp`.
|
|
4
|
+
* Server-internal — not exposed on the public type surface because no
|
|
5
|
+
* consumer outside this package needs to compose it. */
|
|
6
|
+
export type DeltaTarget = ListSource | 'new';
|
|
7
|
+
/**
|
|
8
|
+
* Core list-operations engine. Pure-ish TypeScript: takes a `UserInfo` +
|
|
9
|
+
* optional `IMetadataProvider` and talks to data exclusively through
|
|
10
|
+
* `Metadata` / `RunView` / `BaseEntity`. No GraphQL, no HTTP, no Angular.
|
|
11
|
+
*
|
|
12
|
+
* Public methods are PascalCase per MJ convention. Internal helpers are
|
|
13
|
+
* camelCase. Every mutating method delegates through `ComputeDelta` →
|
|
14
|
+
* `ApplyDelta` to enforce the drop-row warning contract.
|
|
15
|
+
*/
|
|
16
|
+
export declare class ListOperations {
|
|
17
|
+
private readonly contextUser;
|
|
18
|
+
private readonly provider;
|
|
19
|
+
constructor(contextUser: UserInfo, provider?: IMetadataProvider);
|
|
20
|
+
/**
|
|
21
|
+
* Resolve any `ListSource` to a concrete set of record IDs + the entity
|
|
22
|
+
* name those IDs belong to. Never mutates. Used by every higher-level
|
|
23
|
+
* operation (`ComputeDelta`, `ComputeSetOp`, audience resolution, etc.).
|
|
24
|
+
*
|
|
25
|
+
* Record IDs are returned in MJ List Detail format — single-PK entities
|
|
26
|
+
* use the raw value; composite-PK entities use the canonical
|
|
27
|
+
* `Field1|Value1||Field2|Value2` form (`CompositeKey.ToConcatenatedString`).
|
|
28
|
+
*/
|
|
29
|
+
ResolveSource(source: ListSource): Promise<ResolvedRecordSet>;
|
|
30
|
+
/**
|
|
31
|
+
* Return the current members of a list as record IDs. Convenience wrapper
|
|
32
|
+
* over `ResolveSource({ kind: 'list', listId })`.
|
|
33
|
+
*/
|
|
34
|
+
GetListMembers(listId: string): Promise<ResolvedRecordSet>;
|
|
35
|
+
/**
|
|
36
|
+
* Preview a refresh / materialization. Never mutates. Returns a fully
|
|
37
|
+
* populated `ListDelta` including a signed `DeltaToken` that `ApplyDelta`
|
|
38
|
+
* will accept (subject to TTL + re-computation + permission checks).
|
|
39
|
+
*
|
|
40
|
+
* - `target = 'new'`: equivalent to materializing a new list — every
|
|
41
|
+
* record in `source` becomes a `ToAdd`, no removals are possible.
|
|
42
|
+
* - `target = ListSource(kind:'list')`: a refresh of that list against
|
|
43
|
+
* `source`. `mode` controls whether removals are allowed (`Sync`) or
|
|
44
|
+
* forbidden (`Additive`).
|
|
45
|
+
*/
|
|
46
|
+
ComputeDelta(target: DeltaTarget, source: ListSource, mode: ListRefreshMode): Promise<ListDelta>;
|
|
47
|
+
/**
|
|
48
|
+
* Preview a set-op (union / intersection / difference) across two or more
|
|
49
|
+
* sources, optionally projected into a target list. Same drop-warning
|
|
50
|
+
* semantics as `ComputeDelta`: any operation that would remove records
|
|
51
|
+
* from an existing target sets `Counts.Remove > 0` and produces a
|
|
52
|
+
* `WILL_REMOVE_RECORDS` warning.
|
|
53
|
+
*
|
|
54
|
+
* `difference` is left-to-right: `inputs[0] − inputs[1] − inputs[2] …`.
|
|
55
|
+
*/
|
|
56
|
+
ComputeSetOp(op: SetOpKind, inputs: ListSource[], target?: DeltaTarget): Promise<ListDelta>;
|
|
57
|
+
/**
|
|
58
|
+
* Apply a previously previewed `ListDelta` to its target list. Enforces
|
|
59
|
+
* the full drop-row warning contract (server-side, non-bypassable):
|
|
60
|
+
*
|
|
61
|
+
* 1. The token must verify (signature + 5-min TTL).
|
|
62
|
+
* 2. If the delta would remove records, `opts.ConfirmDrops` must be true.
|
|
63
|
+
* 3. The target list's current membership must still equal what the
|
|
64
|
+
* preview observed — otherwise `STALE_DELTA` (UI is expected to
|
|
65
|
+
* re-preview and ask the user again).
|
|
66
|
+
* 4. Caller must hold Editor permission (placeholder — Phase 2 wires
|
|
67
|
+
* the real ResourcePermission check).
|
|
68
|
+
*
|
|
69
|
+
* Only mutates when all four pass. Concurrent mutations between preview
|
|
70
|
+
* and apply surface as `STALE_DELTA` rather than silently overwriting.
|
|
71
|
+
*/
|
|
72
|
+
ApplyDelta(delta: ListDelta, opts: {
|
|
73
|
+
ConfirmDrops: boolean;
|
|
74
|
+
DeltaToken: string;
|
|
75
|
+
}): Promise<ApplyResult>;
|
|
76
|
+
/**
|
|
77
|
+
* Materialize a new list from a User View. Creates an `MJ: List` record,
|
|
78
|
+
* captures lineage when requested, and bulk-inserts the resolved members
|
|
79
|
+
* as `MJ: List Details` rows.
|
|
80
|
+
*
|
|
81
|
+
* Lineage semantics:
|
|
82
|
+
* - `RememberLineage = false` → one-shot copy; the list cannot be
|
|
83
|
+
* refreshed against the view later. Useful when the user explicitly
|
|
84
|
+
* wants a frozen point-in-time snapshot decoupled from the view.
|
|
85
|
+
* - `RememberLineage = true, UseSnapshot = false` → list remembers
|
|
86
|
+
* `SourceViewID`; future refreshes re-run the **live** view.
|
|
87
|
+
* - `RememberLineage = true, UseSnapshot = true` → list remembers
|
|
88
|
+
* `SourceViewID` and a JSON snapshot of the view's filter state;
|
|
89
|
+
* future refreshes re-evaluate that **snapshot** even if the view
|
|
90
|
+
* itself has since been edited.
|
|
91
|
+
*
|
|
92
|
+
* Never produces drops (target is brand new) — no delta-token needed.
|
|
93
|
+
*/
|
|
94
|
+
MaterializeFromView(viewId: string, opts: MaterializeOptions): Promise<ApplyResult>;
|
|
95
|
+
/**
|
|
96
|
+
* Refresh an existing list against its captured source view. The list
|
|
97
|
+
* must have been created with `RememberLineage = true` (i.e. it has a
|
|
98
|
+
* non-null `SourceViewID`) — otherwise this returns `TARGET_NOT_FOUND`.
|
|
99
|
+
*
|
|
100
|
+
* When `list.UseSnapshot = true`, the source is re-evaluated against the
|
|
101
|
+
* filter snapshot captured at materialization time. When `false`, the
|
|
102
|
+
* live view is re-run — which means edits to the view since
|
|
103
|
+
* materialization will be reflected in the result.
|
|
104
|
+
*
|
|
105
|
+
* In `Sync` mode, callers MUST pass `ConfirmDrops: true` or the
|
|
106
|
+
* server-side drop guard will reject the apply with `DROP_NOT_CONFIRMED`.
|
|
107
|
+
* On success, `LastRefreshedAt` and `LastRefreshedByUserID` are updated.
|
|
108
|
+
*/
|
|
109
|
+
RefreshFromSource(listId: string, mode: ListRefreshMode, opts: {
|
|
110
|
+
ConfirmDrops: boolean;
|
|
111
|
+
}): Promise<ApplyResult>;
|
|
112
|
+
/**
|
|
113
|
+
* Add a view's results to an existing list. Always additive — never
|
|
114
|
+
* removes existing members. Dedupes silently, so safe to re-run.
|
|
115
|
+
*
|
|
116
|
+
* No drop-confirmation needed (this op cannot drop). The implementation
|
|
117
|
+
* pipes through `ComputeDelta` + `ApplyDelta` so it still gets the
|
|
118
|
+
* token/staleness/permission machinery for free.
|
|
119
|
+
*/
|
|
120
|
+
AddViewResultsToList(viewId: string, listId: string): Promise<ApplyResult>;
|
|
121
|
+
private resolveListSource;
|
|
122
|
+
private resolveViewSource;
|
|
123
|
+
private resolveAdhocSource;
|
|
124
|
+
/**
|
|
125
|
+
* Resolve a list's entity name from its `EntityID` foreign key.
|
|
126
|
+
* Single-entity is intentional — multi-entity lists are not supported.
|
|
127
|
+
*/
|
|
128
|
+
private resolveEntityNameFromList;
|
|
129
|
+
/**
|
|
130
|
+
* Serialize a primary-key tuple into the MJ List Detail `RecordID` format.
|
|
131
|
+
* Single-PK entities return the raw value; composite-PK entities use the
|
|
132
|
+
* canonical `Field1|Value1||Field2|Value2` concatenation matching
|
|
133
|
+
* `CompositeKey.ToConcatenatedString` defaults.
|
|
134
|
+
*/
|
|
135
|
+
private serializeRecordId;
|
|
136
|
+
private buildDelta;
|
|
137
|
+
private buildWarnings;
|
|
138
|
+
/**
|
|
139
|
+
* Apply a set-op across N already-resolved sources. We accept the resolved
|
|
140
|
+
* sets (not raw `ListSource[]`) so callers can decide how to handle mixed
|
|
141
|
+
* entities before we collapse identities to strings.
|
|
142
|
+
*/
|
|
143
|
+
private applySetOp;
|
|
144
|
+
private detectMixedEntities;
|
|
145
|
+
private assertEntitiesMatch;
|
|
146
|
+
private verifyTokenForDelta;
|
|
147
|
+
private verifyTargetNotDrifted;
|
|
148
|
+
/**
|
|
149
|
+
* Placeholder for the Phase 2 ResourcePermission check. Phase 0 ships a
|
|
150
|
+
* stub so the call site exists and the failure mode is reachable in
|
|
151
|
+
* tests, but it currently always passes — server bootstrap MUST wire in
|
|
152
|
+
* the real check before Phase 2 ships.
|
|
153
|
+
*/
|
|
154
|
+
private checkEditorPermission;
|
|
155
|
+
/**
|
|
156
|
+
* Idempotent batch apply. We persist add/remove independently so a
|
|
157
|
+
* single bad record (e.g. constraint violation) doesn't abort the whole
|
|
158
|
+
* delta — we collect failures and surface a `PARTIAL_SUCCESS` result.
|
|
159
|
+
*/
|
|
160
|
+
private applyDeltaMutations;
|
|
161
|
+
/**
|
|
162
|
+
* Locate and delete the `MJ: List Details` rows that map to the given
|
|
163
|
+
* record IDs. We scope the lookup to a single `ListID` so a bad RecordID
|
|
164
|
+
* value can't accidentally drop rows from other lists.
|
|
165
|
+
*/
|
|
166
|
+
private removeDeltaRecords;
|
|
167
|
+
/**
|
|
168
|
+
* Pick the source for a refresh based on the list's lineage. Live mode
|
|
169
|
+
* just points back at the original view ID. Snapshot mode reads the
|
|
170
|
+
* captured filter blob and constructs an ad-hoc source against the
|
|
171
|
+
* list's entity using whichever WHERE clause the view had at
|
|
172
|
+
* materialization time.
|
|
173
|
+
*/
|
|
174
|
+
private resolveRefreshSource;
|
|
175
|
+
/**
|
|
176
|
+
* Parse the captured `SourceFilterSnapshot` and turn it into an ad-hoc
|
|
177
|
+
* `ListSource`. We prefer `customWhereClause` over `whereClause` because
|
|
178
|
+
* the custom form is the user-authored override; smart-filter clauses
|
|
179
|
+
* fall in last because they're AI-derived. Returns null if no usable
|
|
180
|
+
* clause is present (in which case the caller surfaces a clear error).
|
|
181
|
+
*/
|
|
182
|
+
private buildAdhocSourceFromSnapshot;
|
|
183
|
+
/**
|
|
184
|
+
* Bump `LastRefreshedAt` + `LastRefreshedByUserID` after a successful
|
|
185
|
+
* refresh. Best-effort — a failure here doesn't roll back the apply
|
|
186
|
+
* (the records are already in the right state), but we do log it so
|
|
187
|
+
* the discrepancy is observable.
|
|
188
|
+
*/
|
|
189
|
+
private stampRefreshMetadata;
|
|
190
|
+
private failure;
|
|
191
|
+
/**
|
|
192
|
+
* Load the User View and serialize its filter state into a JSON blob.
|
|
193
|
+
* We only capture the fields that actually drive the result set — grid
|
|
194
|
+
* layout, sort, and smart-filter explanation are layout/explanation
|
|
195
|
+
* metadata, not part of the filter contract, so they're omitted. The
|
|
196
|
+
* snapshot is versioned (`v: 1`) so future schema evolutions can
|
|
197
|
+
* branch on shape without breaking older snapshots.
|
|
198
|
+
*/
|
|
199
|
+
private captureViewFilterSnapshot;
|
|
200
|
+
/**
|
|
201
|
+
* Create + save the parent `MJ: List` record. Lineage fields are only
|
|
202
|
+
* populated when `RememberLineage = true` on `opts` — otherwise the new
|
|
203
|
+
* list is a plain one-shot copy with no upstream reference.
|
|
204
|
+
*/
|
|
205
|
+
private createListWithLineage;
|
|
206
|
+
/**
|
|
207
|
+
* Bulk-insert `MJ: List Details` rows for the given record IDs. We
|
|
208
|
+
* surface a per-record error rather than aborting the whole batch so
|
|
209
|
+
* the caller can show partial-success counts.
|
|
210
|
+
*/
|
|
211
|
+
private insertListMembers;
|
|
212
|
+
private metadata;
|
|
213
|
+
private runView;
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=ListOperations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ListOperations.d.ts","sourceRoot":"","sources":["../src/ListOperations.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,iBAAiB,EAKjB,QAAQ,EACT,MAAM,sBAAsB,CAAC;AAS9B,OAAO,KAAK,EACV,WAAW,EAIX,SAAS,EAGT,eAAe,EACf,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EACjB,SAAS,EACV,MAAM,4BAA4B,CAAC;AAEpC;;yDAEyD;AACzD,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,KAAK,CAAC;AAE7C;;;;;;;;GAQG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAW;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgC;gBAE7C,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,iBAAiB;IAK/D;;;;;;;;OAQG;IACU,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAiB1E;;;OAGG;IACU,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAIvE;;;;;;;;;;OAUG;IACU,YAAY,CACvB,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,SAAS,CAAC;IA4CrB;;;;;;;;OAQG;IACU,YAAY,CACvB,EAAE,EAAE,SAAS,EACb,MAAM,EAAE,UAAU,EAAE,EACpB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,SAAS,CAAC;IA0DrB;;;;;;;;;;;;;;OAcG;IACU,UAAU,CACrB,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE;QAAE,YAAY,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAClD,OAAO,CAAC,WAAW,CAAC;IA6BvB;;;;;;;;;;;;;;;;;OAiBG;IACU,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,WAAW,CAAC;IA0ChG;;;;;;;;;;;;;OAaG;IACU,iBAAiB,CAC5B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,eAAe,EACrB,IAAI,EAAE;QAAE,YAAY,EAAE,OAAO,CAAA;KAAE,GAC9B,OAAO,CAAC,WAAW,CAAC;IAoBvB;;;;;;;OAOG;IACU,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;YAWzE,iBAAiB;YA6BjB,iBAAiB;YAsCjB,kBAAkB;IA0BhC;;;OAGG;IACH,OAAO,CAAC,yBAAyB;IAWjC;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;YAaX,UAAU;IAwCxB,OAAO,CAAC,aAAa;IAmCrB;;;;OAIG;IACH,OAAO,CAAC,UAAU;IA0BlB,OAAO,CAAC,mBAAmB;IAY3B,OAAO,CAAC,mBAAmB;YAQb,mBAAmB;YAoCnB,sBAAsB;IAwBpC;;;;;OAKG;YACW,qBAAqB;IAOnC;;;;OAIG;YACW,mBAAmB;IAkDjC;;;;OAIG;YACW,kBAAkB;IA0BhC;;;;;;OAMG;YACW,oBAAoB;IAqClC;;;;;;OAMG;IACH,OAAO,CAAC,4BAA4B;IA0BpC;;;;;OAKG;YACW,oBAAoB;IAkBlC,OAAO,CAAC,OAAO;IAIf;;;;;;;OAOG;YACW,yBAAyB;IAwBvC;;;;OAIG;YACW,qBAAqB;IA+BnC;;;;OAIG;YACW,iBAAiB;IA4B/B,OAAO,CAAC,QAAQ;IAQhB,OAAO,CAAC,OAAO;CAGhB"}
|