@mj-biz-apps/common-activity-sync 0.0.0 → 5.37.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/ActivitySyncEngine.d.ts +118 -0
- package/dist/ActivitySyncEngine.d.ts.map +1 -0
- package/dist/ActivitySyncEngine.js +620 -0
- package/dist/ActivitySyncEngine.js.map +1 -0
- package/dist/BaseActivitySyncExtension.d.ts +99 -0
- package/dist/BaseActivitySyncExtension.d.ts.map +1 -0
- package/dist/BaseActivitySyncExtension.js +3 -0
- package/dist/BaseActivitySyncExtension.js.map +1 -0
- package/dist/BaseActivitySyncProvider.d.ts +40 -0
- package/dist/BaseActivitySyncProvider.d.ts.map +1 -0
- package/dist/BaseActivitySyncProvider.js +76 -0
- package/dist/BaseActivitySyncProvider.js.map +1 -0
- package/dist/action-result.d.ts +38 -0
- package/dist/action-result.d.ts.map +1 -0
- package/dist/action-result.js +43 -0
- package/dist/action-result.js.map +1 -0
- package/dist/entity-names.d.ts +19 -0
- package/dist/entity-names.d.ts.map +1 -0
- package/dist/entity-names.js +19 -0
- package/dist/entity-names.js.map +1 -0
- package/dist/extensions.d.ts +37 -0
- package/dist/extensions.d.ts.map +1 -0
- package/dist/extensions.js +80 -0
- package/dist/extensions.js.map +1 -0
- package/dist/identity.d.ts +30 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +90 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/load.d.ts +17 -0
- package/dist/load.d.ts.map +1 -0
- package/dist/load.js +29 -0
- package/dist/load.js.map +1 -0
- package/dist/manual-log.d.ts +33 -0
- package/dist/manual-log.d.ts.map +1 -0
- package/dist/manual-log.js +230 -0
- package/dist/manual-log.js.map +1 -0
- package/dist/participants.d.ts +47 -0
- package/dist/participants.d.ts.map +1 -0
- package/dist/participants.js +59 -0
- package/dist/participants.js.map +1 -0
- package/dist/providers/FixtureActivitySyncProvider.d.ts +21 -0
- package/dist/providers/FixtureActivitySyncProvider.d.ts.map +1 -0
- package/dist/providers/FixtureActivitySyncProvider.js +58 -0
- package/dist/providers/FixtureActivitySyncProvider.js.map +1 -0
- package/dist/providers/GraphMessageMapper.d.ts +37 -0
- package/dist/providers/GraphMessageMapper.d.ts.map +1 -0
- package/dist/providers/GraphMessageMapper.js +91 -0
- package/dist/providers/GraphMessageMapper.js.map +1 -0
- package/dist/providers/MSGraphActivitySyncProvider.d.ts +22 -0
- package/dist/providers/MSGraphActivitySyncProvider.d.ts.map +1 -0
- package/dist/providers/MSGraphActivitySyncProvider.js +62 -0
- package/dist/providers/MSGraphActivitySyncProvider.js.map +1 -0
- package/dist/providers/MSGraphCalendarSyncProvider.d.ts +60 -0
- package/dist/providers/MSGraphCalendarSyncProvider.d.ts.map +1 -0
- package/dist/providers/MSGraphCalendarSyncProvider.js +145 -0
- package/dist/providers/MSGraphCalendarSyncProvider.js.map +1 -0
- package/dist/qualification.d.ts +91 -0
- package/dist/qualification.d.ts.map +1 -0
- package/dist/qualification.js +62 -0
- package/dist/qualification.js.map +1 -0
- package/dist/run.d.ts +67 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +84 -0
- package/dist/run.js.map +1 -0
- package/dist/sql.d.ts +17 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +50 -0
- package/dist/sql.js.map +1 -0
- package/dist/stages.d.ts +55 -0
- package/dist/stages.d.ts.map +1 -0
- package/dist/stages.js +109 -0
- package/dist/stages.js.map +1 -0
- package/dist/types.d.ts +130 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +38 -0
- package/dist/types.js.map +1 -0
- package/dist/watermark.d.ts +63 -0
- package/dist/watermark.d.ts.map +1 -0
- package/dist/watermark.js +103 -0
- package/dist/watermark.js.map +1 -0
- package/dist/writer.d.ts +132 -0
- package/dist/writer.d.ts.map +1 -0
- package/dist/writer.js +326 -0
- package/dist/writer.js.map +1 -0
- package/package.json +32 -5
- package/README.md +0 -12
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The two watermark rules, as enforced behaviour rather than advice.
|
|
3
|
+
*
|
|
4
|
+
* These live in the engine (and are consumed by {@link BaseActivitySyncProvider}) rather than being
|
|
5
|
+
* exported as helpers a provider may forget to call. Both failure modes are SILENT and PERMANENT,
|
|
6
|
+
* which is exactly the class of rule that must not be optional.
|
|
7
|
+
*
|
|
8
|
+
* @module @mj-biz-apps/common-activity-sync
|
|
9
|
+
*/
|
|
10
|
+
import { ActivitySourceKind, NormalizedItem } from './types.js';
|
|
11
|
+
/** The per-item tallies a run produces, and the only input to whether the watermark may move. */
|
|
12
|
+
export interface RunOutcome {
|
|
13
|
+
/** Passed qualification and was written (or was a duplicate — a duplicate has been seen). */
|
|
14
|
+
Settled: number;
|
|
15
|
+
/** Qualification said no. SEEN to a conclusion, so the watermark may pass it. */
|
|
16
|
+
Discarded: number;
|
|
17
|
+
/**
|
|
18
|
+
* Errored before reaching a conclusion. NOT seen.
|
|
19
|
+
*
|
|
20
|
+
* The distinction is the whole rule. Where a transport has no date filter, anything the
|
|
21
|
+
* watermark passes can never be re-fetched, so advancing over a failure loses the item
|
|
22
|
+
* permanently.
|
|
23
|
+
*/
|
|
24
|
+
Failed: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The newest point this batch can honestly claim to have seen.
|
|
28
|
+
*
|
|
29
|
+
* @param kind the surface — selects the basis (see {@link WatermarkBasisForKind})
|
|
30
|
+
* @param items the batch's normalized items
|
|
31
|
+
* @param observedAt when the provider looked; used by future-dated surfaces
|
|
32
|
+
* @returns the candidate watermark, or null when there is nothing to claim
|
|
33
|
+
*/
|
|
34
|
+
export declare function ResolveHighWatermark(kind: ActivitySourceKind, items: readonly NormalizedItem[], observedAt: Date): Date | null;
|
|
35
|
+
/**
|
|
36
|
+
* Whether the run earned the right to move the watermark forward.
|
|
37
|
+
*
|
|
38
|
+
* Any failure withholds it. The cost of not advancing is re-fetching items the next run will
|
|
39
|
+
* recognise as duplicates; the cost of advancing wrongly is losing them for good. Those are not
|
|
40
|
+
* comparable, so this is deliberately not a threshold.
|
|
41
|
+
*/
|
|
42
|
+
export declare function CanAdvanceWatermark(outcome: RunOutcome): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The watermark to persist after a run — the current value unless the run both earned an advance
|
|
45
|
+
* and actually moved forward.
|
|
46
|
+
*
|
|
47
|
+
* Never moves BACKWARDS. Two runs racing (a scheduled job whose `ConcurrencyMode` is not `Skip`)
|
|
48
|
+
* can otherwise leave the watermark behind where the earlier run reached, which silently re-reads
|
|
49
|
+
* a window on every subsequent pass.
|
|
50
|
+
*/
|
|
51
|
+
/** Calendar watermark lives in ActivitySyncConnection.Settings so it cannot hide behind LastSyncAt. */
|
|
52
|
+
export declare const CALENDAR_WATERMARK_SETTING = "CalendarLastSyncAt";
|
|
53
|
+
export declare function ParseConnectionSettings(settings: string | null | undefined): Record<string, unknown>;
|
|
54
|
+
/**
|
|
55
|
+
* One watermark per surface. Messages use LastSyncAt; calendar uses Settings.
|
|
56
|
+
* A shared column takes the max of both, so a meeting older than the newest
|
|
57
|
+
* email is judged already-seen and skipped forever.
|
|
58
|
+
*/
|
|
59
|
+
export declare function SurfaceWatermark(kind: ActivitySourceKind, lastSyncAt: Date | string | null | undefined, settings: string | null | undefined): Date | null;
|
|
60
|
+
/** Merge the calendar watermark into Settings without dropping other keys. */
|
|
61
|
+
export declare function MergeCalendarWatermark(settings: string | null | undefined, at: Date): string;
|
|
62
|
+
export declare function NextWatermark(current: Date | null, candidate: Date | null, outcome: RunOutcome): Date | null;
|
|
63
|
+
//# sourceMappingURL=watermark.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watermark.d.ts","sourceRoot":"","sources":["../src/watermark.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAyB,MAAM,YAAY,CAAC;AAEvF,iGAAiG;AACjG,MAAM,WAAW,UAAU;IACvB,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAChC,IAAI,EAAE,kBAAkB,EACxB,KAAK,EAAE,SAAS,cAAc,EAAE,EAChC,UAAU,EAAE,IAAI,GACjB,IAAI,GAAG,IAAI,CAMb;AAaD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAEhE;AAED;;;;;;;GAOG;AACH,uGAAuG;AACvG,eAAO,MAAM,0BAA0B,uBAAuB,CAAC;AAE/D,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAWpG;AAQD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC5B,IAAI,EAAE,kBAAkB,EACxB,UAAU,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,EAC5C,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACpC,IAAI,GAAG,IAAI,CAMb;AAED,8EAA8E;AAC9E,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,EAAE,EAAE,IAAI,GAAG,MAAM,CAI5F;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI,EAAE,SAAS,EAAE,IAAI,GAAG,IAAI,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI,GAAG,IAAI,CAQ5G"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The two watermark rules, as enforced behaviour rather than advice.
|
|
3
|
+
*
|
|
4
|
+
* These live in the engine (and are consumed by {@link BaseActivitySyncProvider}) rather than being
|
|
5
|
+
* exported as helpers a provider may forget to call. Both failure modes are SILENT and PERMANENT,
|
|
6
|
+
* which is exactly the class of rule that must not be optional.
|
|
7
|
+
*
|
|
8
|
+
* @module @mj-biz-apps/common-activity-sync
|
|
9
|
+
*/
|
|
10
|
+
import { WatermarkBasisForKind } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* The newest point this batch can honestly claim to have seen.
|
|
13
|
+
*
|
|
14
|
+
* @param kind the surface — selects the basis (see {@link WatermarkBasisForKind})
|
|
15
|
+
* @param items the batch's normalized items
|
|
16
|
+
* @param observedAt when the provider looked; used by future-dated surfaces
|
|
17
|
+
* @returns the candidate watermark, or null when there is nothing to claim
|
|
18
|
+
*/
|
|
19
|
+
export function ResolveHighWatermark(kind, items, observedAt) {
|
|
20
|
+
if (WatermarkBasisForKind(kind) === 'ObservationTime') {
|
|
21
|
+
// A future-dated surface reports when it LOOKED, not when its items happen.
|
|
22
|
+
return items.length > 0 ? observedAt : null;
|
|
23
|
+
}
|
|
24
|
+
return newestStartedAt(items);
|
|
25
|
+
}
|
|
26
|
+
/** The latest `StartedAt` across the batch, or null when the batch is empty. */
|
|
27
|
+
function newestStartedAt(items) {
|
|
28
|
+
let newest = null;
|
|
29
|
+
for (const item of items) {
|
|
30
|
+
if (newest === null || item.StartedAt.getTime() > newest.getTime()) {
|
|
31
|
+
newest = item.StartedAt;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return newest;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Whether the run earned the right to move the watermark forward.
|
|
38
|
+
*
|
|
39
|
+
* Any failure withholds it. The cost of not advancing is re-fetching items the next run will
|
|
40
|
+
* recognise as duplicates; the cost of advancing wrongly is losing them for good. Those are not
|
|
41
|
+
* comparable, so this is deliberately not a threshold.
|
|
42
|
+
*/
|
|
43
|
+
export function CanAdvanceWatermark(outcome) {
|
|
44
|
+
return outcome.Failed === 0;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The watermark to persist after a run — the current value unless the run both earned an advance
|
|
48
|
+
* and actually moved forward.
|
|
49
|
+
*
|
|
50
|
+
* Never moves BACKWARDS. Two runs racing (a scheduled job whose `ConcurrencyMode` is not `Skip`)
|
|
51
|
+
* can otherwise leave the watermark behind where the earlier run reached, which silently re-reads
|
|
52
|
+
* a window on every subsequent pass.
|
|
53
|
+
*/
|
|
54
|
+
/** Calendar watermark lives in ActivitySyncConnection.Settings so it cannot hide behind LastSyncAt. */
|
|
55
|
+
export const CALENDAR_WATERMARK_SETTING = 'CalendarLastSyncAt';
|
|
56
|
+
export function ParseConnectionSettings(settings) {
|
|
57
|
+
if (!settings)
|
|
58
|
+
return {};
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(settings);
|
|
61
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* hand-edited JSON is treated as empty — re-read the window, let dedupe absorb it */
|
|
67
|
+
}
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
function asDate(value) {
|
|
71
|
+
if (value == null || value === '')
|
|
72
|
+
return null;
|
|
73
|
+
const d = value instanceof Date ? value : new Date(value);
|
|
74
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* One watermark per surface. Messages use LastSyncAt; calendar uses Settings.
|
|
78
|
+
* A shared column takes the max of both, so a meeting older than the newest
|
|
79
|
+
* email is judged already-seen and skipped forever.
|
|
80
|
+
*/
|
|
81
|
+
export function SurfaceWatermark(kind, lastSyncAt, settings) {
|
|
82
|
+
if (kind === 'Calendar') {
|
|
83
|
+
const raw = ParseConnectionSettings(settings)[CALENDAR_WATERMARK_SETTING];
|
|
84
|
+
return typeof raw === 'string' ? asDate(raw) : null;
|
|
85
|
+
}
|
|
86
|
+
return asDate(lastSyncAt);
|
|
87
|
+
}
|
|
88
|
+
/** Merge the calendar watermark into Settings without dropping other keys. */
|
|
89
|
+
export function MergeCalendarWatermark(settings, at) {
|
|
90
|
+
const bag = ParseConnectionSettings(settings);
|
|
91
|
+
bag[CALENDAR_WATERMARK_SETTING] = at.toISOString();
|
|
92
|
+
return JSON.stringify(bag);
|
|
93
|
+
}
|
|
94
|
+
export function NextWatermark(current, candidate, outcome) {
|
|
95
|
+
if (!CanAdvanceWatermark(outcome) || candidate === null) {
|
|
96
|
+
return current;
|
|
97
|
+
}
|
|
98
|
+
if (current !== null && candidate.getTime() <= current.getTime()) {
|
|
99
|
+
return current;
|
|
100
|
+
}
|
|
101
|
+
return candidate;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=watermark.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watermark.js","sourceRoot":"","sources":["../src/watermark.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAsC,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAkBvF;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAChC,IAAwB,EACxB,KAAgC,EAChC,UAAgB;IAEhB,IAAI,qBAAqB,CAAC,IAAI,CAAC,KAAK,iBAAiB,EAAE,CAAC;QACpD,4EAA4E;QAC5E,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC;IAChD,CAAC;IACD,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;AAClC,CAAC;AAED,gFAAgF;AAChF,SAAS,eAAe,CAAC,KAAgC;IACrD,IAAI,MAAM,GAAgB,IAAI,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,MAAM,KAAK,IAAI,IAAI,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;YACjE,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC;QAC5B,CAAC;IACL,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAmB;IACnD,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC;AAChC,CAAC;AAED;;;;;;;GAOG;AACH,uGAAuG;AACvG,MAAM,CAAC,MAAM,0BAA0B,GAAG,oBAAoB,CAAC;AAE/D,MAAM,UAAU,uBAAuB,CAAC,QAAmC;IACvE,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,CAAC;IACzB,IAAI,CAAC;QACD,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC7C,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC1E,OAAO,MAAiC,CAAC;QAC7C,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACL,qFAAqF;IACzF,CAAC;IACD,OAAO,EAAE,CAAC;AACd,CAAC;AAED,SAAS,MAAM,CAAC,KAAuC;IACnD,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC/C,MAAM,CAAC,GAAG,KAAK,YAAY,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1D,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAChD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC5B,IAAwB,EACxB,UAA4C,EAC5C,QAAmC;IAEnC,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QACtB,MAAM,GAAG,GAAG,uBAAuB,CAAC,QAAQ,CAAC,CAAC,0BAA0B,CAAC,CAAC;QAC1E,OAAO,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACxD,CAAC;IACD,OAAO,MAAM,CAAC,UAAU,CAAC,CAAC;AAC9B,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,sBAAsB,CAAC,QAAmC,EAAE,EAAQ;IAChF,MAAM,GAAG,GAAG,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAC9C,GAAG,CAAC,0BAA0B,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;IACnD,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAAoB,EAAE,SAAsB,EAAE,OAAmB;IAC3F,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QACtD,OAAO,OAAO,CAAC;IACnB,CAAC;IACD,IAAI,OAAO,KAAK,IAAI,IAAI,SAAS,CAAC,OAAO,EAAE,IAAI,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;QAC/D,OAAO,OAAO,CAAC;IACnB,CAAC;IACD,OAAO,SAAS,CAAC;AACrB,CAAC"}
|
package/dist/writer.d.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One Activity plus N ActivityLink rows, atomically.
|
|
3
|
+
*
|
|
4
|
+
* ONE writer, TWO entry points (plans/mj-entity-action-workflow-adoption.md §3.2):
|
|
5
|
+
* - `Write` — the sync engine's path. Synced rows set Visibility = 'Private' explicitly and dispatch
|
|
6
|
+
* registered extensions inside the write transaction.
|
|
7
|
+
* - `WriteManual` — the manual / declarative path (`Common.LogActivity`, UI logging). Visibility
|
|
8
|
+
* defaults to the column's own 'Internal', no connection is stamped, and sync extensions do NOT
|
|
9
|
+
* run — those fire on ingest, not on a record being logged (see §8 of the same plan).
|
|
10
|
+
*
|
|
11
|
+
* Both share the same transactional core, dedupe, link writing and type resolution, so the
|
|
12
|
+
* declarative path and the ingest path cannot drift.
|
|
13
|
+
*
|
|
14
|
+
* Unmatched participants become unresolved links — never a new Person.
|
|
15
|
+
*/
|
|
16
|
+
import { type IMetadataProvider, type UserInfo } from '@memberjunction/core';
|
|
17
|
+
import type { mjBizAppsCommonActivityEntity, mjBizAppsCommonActivityLinkEntity } from '@mj-biz-apps/common-entities';
|
|
18
|
+
import type { ResolvedParty, UnresolvedParty, ActivityWriteContext } from './BaseActivitySyncExtension.js';
|
|
19
|
+
import type { ActivityDirection, ActivityIdentityKind, ActivityLinkRole, NormalizedItem } from './types.js';
|
|
20
|
+
export type ActivitySourceValue = 'Manual' | 'System' | 'Integration';
|
|
21
|
+
/** Mirrors `CK_Activity_Status`. */
|
|
22
|
+
export type ActivityStatusValue = 'Logged' | 'Scheduled' | 'Completed' | 'Cancelled' | 'Failed';
|
|
23
|
+
/** Mirrors `CK_Activity_Visibility`. */
|
|
24
|
+
export type ActivityVisibilityValue = 'Internal' | 'Private';
|
|
25
|
+
/** How much of an included body to persist. Default Snippet — Full is a per-connection opt-in. */
|
|
26
|
+
export type StoreBodyPolicy = 'None' | 'Snippet' | 'Full';
|
|
27
|
+
export declare const BODY_SNIPPET_CHARS = 500;
|
|
28
|
+
/** Read StoreBody from ActivitySyncConnection.Settings. Missing / invalid / unparseable → Snippet. */
|
|
29
|
+
export declare function StoreBodyFromSettings(settings: string | null | undefined): StoreBodyPolicy;
|
|
30
|
+
export declare function BodyForStorage(body: string | null | undefined, policy?: StoreBodyPolicy): string | null;
|
|
31
|
+
/**
|
|
32
|
+
* One link on an activity: either a resolved link to any entity's record (EntityName + RecordID)
|
|
33
|
+
* or an unresolved identity (IdentityKind + IdentityValue). Exactly one of the two shapes.
|
|
34
|
+
*/
|
|
35
|
+
export interface ActivityLinkSpec {
|
|
36
|
+
Role: ActivityLinkRole;
|
|
37
|
+
EntityName?: string;
|
|
38
|
+
RecordID?: string;
|
|
39
|
+
IdentityKind?: ActivityIdentityKind;
|
|
40
|
+
IdentityValue?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface WriteActivityInput {
|
|
43
|
+
Item: NormalizedItem;
|
|
44
|
+
ConnectionID: string;
|
|
45
|
+
SourceSystem: string;
|
|
46
|
+
/** Integration for a live provider; System for a fixture — never Integration from a non-live source. */
|
|
47
|
+
Source: ActivitySourceValue;
|
|
48
|
+
Resolved: ResolvedParty[];
|
|
49
|
+
Unresolved: UnresolvedParty[];
|
|
50
|
+
/** Default Snippet. Independent of SkippedContentPolicy. */
|
|
51
|
+
StoreBody?: StoreBodyPolicy;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A manually or declaratively logged activity — no sync connection, no ingest item.
|
|
55
|
+
*
|
|
56
|
+
* Defaults follow the column defaults for a manual log: Visibility 'Internal' (the sync path's
|
|
57
|
+
* 'Private' is for mail nobody chose to share), Status 'Logged', Direction 'Internal',
|
|
58
|
+
* Source 'Manual'. 'Integration' is deliberately not accepted here — that word belongs to the
|
|
59
|
+
* sync engine's entry point.
|
|
60
|
+
*/
|
|
61
|
+
export interface WriteManualActivityInput {
|
|
62
|
+
/** `ActivityType.Code` — resolved by CODE so renaming a type never breaks callers. */
|
|
63
|
+
TypeCode: string;
|
|
64
|
+
Title: string;
|
|
65
|
+
StartedAt: Date;
|
|
66
|
+
EndedAt?: Date | null;
|
|
67
|
+
Description?: string | null;
|
|
68
|
+
Direction?: ActivityDirection;
|
|
69
|
+
Status?: ActivityStatusValue;
|
|
70
|
+
Visibility?: ActivityVisibilityValue;
|
|
71
|
+
Source?: Exclude<ActivitySourceValue, 'Integration'>;
|
|
72
|
+
Location?: string | null;
|
|
73
|
+
/** JSON extras that are not query predicates. Stringified into `Activity.Details`. */
|
|
74
|
+
Details?: Record<string, unknown> | null;
|
|
75
|
+
/**
|
|
76
|
+
* Optional idempotency pair. When BOTH are set, a second write with the same pair is a no-op
|
|
77
|
+
* that reports `AlreadyPresent` — the same dedupe the sync path uses. SourceSystem is required
|
|
78
|
+
* whenever ExternalID is set (the column contract).
|
|
79
|
+
*/
|
|
80
|
+
SourceSystem?: string | null;
|
|
81
|
+
ExternalID?: string | null;
|
|
82
|
+
Links?: ActivityLinkSpec[];
|
|
83
|
+
}
|
|
84
|
+
export interface WriteActivityResult {
|
|
85
|
+
Success: boolean;
|
|
86
|
+
ActivityID: string | null;
|
|
87
|
+
AlreadyPresent: boolean;
|
|
88
|
+
Links: mjBizAppsCommonActivityLinkEntity[];
|
|
89
|
+
Activity: mjBizAppsCommonActivityEntity | null;
|
|
90
|
+
Issues: string[];
|
|
91
|
+
}
|
|
92
|
+
export interface WriteOptions {
|
|
93
|
+
ProviderTypeCode: string;
|
|
94
|
+
/**
|
|
95
|
+
* Runs inside the open write transaction, after the activity and its party
|
|
96
|
+
* links are saved and before commit. This is where registered extensions
|
|
97
|
+
* add app-specific links. Throwing rolls the write back.
|
|
98
|
+
*/
|
|
99
|
+
OnWritten?: (context: ActivityWriteContext) => Promise<void>;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Guard-clause validation for the manual entry point. Returns problems rather than throwing so the
|
|
103
|
+
* writer can report them on the result like every other failure.
|
|
104
|
+
*/
|
|
105
|
+
export declare function ValidateManualActivityInput(input: WriteManualActivityInput): string[];
|
|
106
|
+
export declare class ActivityWriter {
|
|
107
|
+
/** The sync engine's entry point: connection-stamped, Visibility 'Private', extensions dispatched. */
|
|
108
|
+
Write(input: WriteActivityInput, provider: IMetadataProvider, contextUser: UserInfo, options?: WriteOptions): Promise<WriteActivityResult>;
|
|
109
|
+
/**
|
|
110
|
+
* The manual / declarative entry point (`Common.LogActivity`, UI logging).
|
|
111
|
+
*
|
|
112
|
+
* Same transactional core, dedupe and link writing as `Write`; different defaults (Visibility
|
|
113
|
+
* 'Internal', no connection) and NO extension dispatch — sync extensions enrich an item being
|
|
114
|
+
* ingested, and a manually logged activity is not one.
|
|
115
|
+
*/
|
|
116
|
+
WriteManual(input: WriteManualActivityInput, provider: IMetadataProvider, contextUser: UserInfo): Promise<WriteActivityResult>;
|
|
117
|
+
BuildWriteContext(result: WriteActivityResult, input: WriteActivityInput, contextUser: UserInfo, provider: IMetadataProvider, providerTypeCode: string): ActivityWriteContext | null;
|
|
118
|
+
/**
|
|
119
|
+
* The shared transactional core: save the activity, write its links in sequence, run the
|
|
120
|
+
* optional in-transaction callback, commit. Any failure rolls the whole write back.
|
|
121
|
+
*/
|
|
122
|
+
private writeWithLinks;
|
|
123
|
+
private fillSyncFields;
|
|
124
|
+
private fillManualFields;
|
|
125
|
+
private planLinks;
|
|
126
|
+
private dedupeLinks;
|
|
127
|
+
private emptyResult;
|
|
128
|
+
private writeLink;
|
|
129
|
+
private resolveTypeByCode;
|
|
130
|
+
private findByExternalKey;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=writer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../src/writer.ts"],"names":[],"mappings":"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"}
|
package/dist/writer.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One Activity plus N ActivityLink rows, atomically.
|
|
3
|
+
*
|
|
4
|
+
* ONE writer, TWO entry points (plans/mj-entity-action-workflow-adoption.md §3.2):
|
|
5
|
+
* - `Write` — the sync engine's path. Synced rows set Visibility = 'Private' explicitly and dispatch
|
|
6
|
+
* registered extensions inside the write transaction.
|
|
7
|
+
* - `WriteManual` — the manual / declarative path (`Common.LogActivity`, UI logging). Visibility
|
|
8
|
+
* defaults to the column's own 'Internal', no connection is stamped, and sync extensions do NOT
|
|
9
|
+
* run — those fire on ingest, not on a record being logged (see §8 of the same plan).
|
|
10
|
+
*
|
|
11
|
+
* Both share the same transactional core, dedupe, link writing and type resolution, so the
|
|
12
|
+
* declarative path and the ingest path cannot drift.
|
|
13
|
+
*
|
|
14
|
+
* Unmatched participants become unresolved links — never a new Person.
|
|
15
|
+
*/
|
|
16
|
+
import { LogError, RunView, } from '@memberjunction/core';
|
|
17
|
+
import { ACTIVITY_SYNC_ENTITIES } from './entity-names.js';
|
|
18
|
+
import { EscapeText } from './sql.js';
|
|
19
|
+
function isDatabaseProvider(provider) {
|
|
20
|
+
return ('BeginEntityTransaction' in provider &&
|
|
21
|
+
typeof provider.BeginEntityTransaction === 'function');
|
|
22
|
+
}
|
|
23
|
+
export const BODY_SNIPPET_CHARS = 500;
|
|
24
|
+
/** Read StoreBody from ActivitySyncConnection.Settings. Missing / invalid / unparseable → Snippet. */
|
|
25
|
+
export function StoreBodyFromSettings(settings) {
|
|
26
|
+
if (!settings)
|
|
27
|
+
return 'Snippet';
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(settings);
|
|
30
|
+
if (parsed !== null && typeof parsed === 'object' && 'StoreBody' in parsed) {
|
|
31
|
+
const value = parsed.StoreBody;
|
|
32
|
+
if (value === 'None' || value === 'Full' || value === 'Snippet')
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// ignore
|
|
38
|
+
}
|
|
39
|
+
return 'Snippet';
|
|
40
|
+
}
|
|
41
|
+
export function BodyForStorage(body, policy = 'Snippet') {
|
|
42
|
+
const text = body ?? '';
|
|
43
|
+
if (policy === 'None')
|
|
44
|
+
return null;
|
|
45
|
+
if (policy === 'Full')
|
|
46
|
+
return text.length > 0 ? text : null;
|
|
47
|
+
if (!text)
|
|
48
|
+
return null;
|
|
49
|
+
return text.length <= BODY_SNIPPET_CHARS ? text : text.slice(0, BODY_SNIPPET_CHARS);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Guard-clause validation for the manual entry point. Returns problems rather than throwing so the
|
|
53
|
+
* writer can report them on the result like every other failure.
|
|
54
|
+
*/
|
|
55
|
+
export function ValidateManualActivityInput(input) {
|
|
56
|
+
const issues = [];
|
|
57
|
+
if (!input.TypeCode?.trim())
|
|
58
|
+
issues.push('TypeCode is required.');
|
|
59
|
+
if (!input.Title?.trim())
|
|
60
|
+
issues.push('Title is required.');
|
|
61
|
+
if (!(input.StartedAt instanceof Date) || Number.isNaN(input.StartedAt.getTime())) {
|
|
62
|
+
issues.push('StartedAt must be a valid date.');
|
|
63
|
+
}
|
|
64
|
+
else if (input.EndedAt instanceof Date && input.EndedAt.getTime() < input.StartedAt.getTime()) {
|
|
65
|
+
issues.push('EndedAt must be on or after StartedAt.');
|
|
66
|
+
}
|
|
67
|
+
if (input.ExternalID && !input.SourceSystem) {
|
|
68
|
+
issues.push('SourceSystem is required when ExternalID is set.');
|
|
69
|
+
}
|
|
70
|
+
for (const [index, link] of (input.Links ?? []).entries()) {
|
|
71
|
+
const resolved = Boolean(link.EntityName && link.RecordID);
|
|
72
|
+
const identity = Boolean(link.IdentityKind && link.IdentityValue);
|
|
73
|
+
if (resolved === identity) {
|
|
74
|
+
issues.push(`Link ${index + 1} must carry either EntityName + RecordID or IdentityKind + IdentityValue.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return issues;
|
|
78
|
+
}
|
|
79
|
+
export class ActivityWriter {
|
|
80
|
+
/** The sync engine's entry point: connection-stamped, Visibility 'Private', extensions dispatched. */
|
|
81
|
+
async Write(input, provider, contextUser, options) {
|
|
82
|
+
const result = this.emptyResult();
|
|
83
|
+
const typeID = await this.resolveTypeByCode(input.Item.TypeCode, contextUser);
|
|
84
|
+
if (!typeID) {
|
|
85
|
+
result.Issues.push(`No ActivityType with Code '${input.Item.TypeCode}' is seeded.`);
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
const existing = await this.findByExternalKey(input.SourceSystem, input.Item.ExternalID, contextUser);
|
|
89
|
+
if (existing) {
|
|
90
|
+
result.Success = true;
|
|
91
|
+
result.ActivityID = existing;
|
|
92
|
+
result.AlreadyPresent = true;
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
if (!isDatabaseProvider(provider)) {
|
|
96
|
+
result.Issues.push('This provider cannot open a transaction; ActivityWriter is server-only.');
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
const onWritten = options?.OnWritten;
|
|
100
|
+
const afterLinks = onWritten
|
|
101
|
+
? async (activity, links) => {
|
|
102
|
+
const writeContext = this.BuildWriteContext({ ...result, Activity: activity, ActivityID: activity.ID, Success: true, Links: links }, input, contextUser, provider, options.ProviderTypeCode);
|
|
103
|
+
if (!writeContext)
|
|
104
|
+
return links;
|
|
105
|
+
await onWritten(writeContext);
|
|
106
|
+
return writeContext.Links;
|
|
107
|
+
}
|
|
108
|
+
: undefined;
|
|
109
|
+
return this.writeWithLinks(provider, contextUser, result, (activity) => this.fillSyncFields(activity, input, typeID, contextUser), this.planLinks(input), afterLinks);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The manual / declarative entry point (`Common.LogActivity`, UI logging).
|
|
113
|
+
*
|
|
114
|
+
* Same transactional core, dedupe and link writing as `Write`; different defaults (Visibility
|
|
115
|
+
* 'Internal', no connection) and NO extension dispatch — sync extensions enrich an item being
|
|
116
|
+
* ingested, and a manually logged activity is not one.
|
|
117
|
+
*/
|
|
118
|
+
async WriteManual(input, provider, contextUser) {
|
|
119
|
+
const result = this.emptyResult();
|
|
120
|
+
const problems = ValidateManualActivityInput(input);
|
|
121
|
+
if (problems.length > 0) {
|
|
122
|
+
result.Issues.push(...problems);
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
const typeID = await this.resolveTypeByCode(input.TypeCode, contextUser);
|
|
126
|
+
if (!typeID) {
|
|
127
|
+
result.Issues.push(`No ActivityType with Code '${input.TypeCode}' is seeded.`);
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
if (input.SourceSystem && input.ExternalID) {
|
|
131
|
+
const existing = await this.findByExternalKey(input.SourceSystem, input.ExternalID, contextUser);
|
|
132
|
+
if (existing) {
|
|
133
|
+
result.Success = true;
|
|
134
|
+
result.ActivityID = existing;
|
|
135
|
+
result.AlreadyPresent = true;
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!isDatabaseProvider(provider)) {
|
|
140
|
+
result.Issues.push('This provider cannot open a transaction; ActivityWriter is server-only.');
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
return this.writeWithLinks(provider, contextUser, result, (activity) => this.fillManualFields(activity, input, typeID, contextUser), this.dedupeLinks(input.Links ?? []));
|
|
144
|
+
}
|
|
145
|
+
BuildWriteContext(result, input, contextUser, provider, providerTypeCode) {
|
|
146
|
+
if (!result.Activity || !result.ActivityID)
|
|
147
|
+
return null;
|
|
148
|
+
return {
|
|
149
|
+
Activity: result.Activity,
|
|
150
|
+
Links: result.Links,
|
|
151
|
+
Item: input.Item,
|
|
152
|
+
ResolvedParties: input.Resolved,
|
|
153
|
+
UnresolvedParties: input.Unresolved,
|
|
154
|
+
ConnectionID: input.ConnectionID,
|
|
155
|
+
ProviderTypeCode: providerTypeCode,
|
|
156
|
+
ContextUser: contextUser,
|
|
157
|
+
Provider: provider,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The shared transactional core: save the activity, write its links in sequence, run the
|
|
162
|
+
* optional in-transaction callback, commit. Any failure rolls the whole write back.
|
|
163
|
+
*/
|
|
164
|
+
async writeWithLinks(provider, contextUser, result, fill, links, afterLinks) {
|
|
165
|
+
const scope = await provider.BeginEntityTransaction();
|
|
166
|
+
try {
|
|
167
|
+
const activity = await provider.GetEntityObject(ACTIVITY_SYNC_ENTITIES.Activities, contextUser);
|
|
168
|
+
activity.NewRecord();
|
|
169
|
+
fill(activity);
|
|
170
|
+
if (!(await activity.Save())) {
|
|
171
|
+
await scope.Rollback();
|
|
172
|
+
result.Issues.push(activity.LatestResult?.CompleteMessage ?? 'Activity.Save failed.');
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
let sequence = 0;
|
|
176
|
+
for (const link of links) {
|
|
177
|
+
const row = await this.writeLink(link, activity.ID, ++sequence, provider, contextUser);
|
|
178
|
+
if (!row) {
|
|
179
|
+
await scope.Rollback();
|
|
180
|
+
result.Issues.push(`Link could not be written (role ${link.Role}).`);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
result.Links.push(row);
|
|
184
|
+
}
|
|
185
|
+
if (afterLinks) {
|
|
186
|
+
result.Links = await afterLinks(activity, result.Links);
|
|
187
|
+
}
|
|
188
|
+
await scope.Commit();
|
|
189
|
+
result.Success = true;
|
|
190
|
+
result.ActivityID = activity.ID;
|
|
191
|
+
result.Activity = activity;
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
LogError(`ActivityWriter.Write failed: ${err}`);
|
|
196
|
+
try {
|
|
197
|
+
await scope.Rollback();
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
/* already failed */
|
|
201
|
+
}
|
|
202
|
+
result.Issues.push(String(err));
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
fillSyncFields(activity, input, typeID, contextUser) {
|
|
207
|
+
activity.ActivityTypeID = typeID;
|
|
208
|
+
activity.Title = (input.Item.Subject || '(no subject)').slice(0, 200);
|
|
209
|
+
activity.StartedAt = input.Item.StartedAt;
|
|
210
|
+
activity.EndedAt = input.Item.EndedAt;
|
|
211
|
+
activity.Description = BodyForStorage(input.Item.Body, input.StoreBody ?? 'Snippet');
|
|
212
|
+
activity.Direction = input.Item.Direction;
|
|
213
|
+
activity.Status = input.Item.Cancelled ? 'Cancelled' : 'Logged';
|
|
214
|
+
activity.Visibility = 'Private';
|
|
215
|
+
activity.Source = input.Source;
|
|
216
|
+
activity.Location = input.Item.Location;
|
|
217
|
+
activity.Details = JSON.stringify(input.Item.Raw);
|
|
218
|
+
activity.ExternalThreadID = input.Item.ExternalThreadID;
|
|
219
|
+
activity.ActivitySyncConnectionID = input.ConnectionID;
|
|
220
|
+
activity.SourceSystem = input.SourceSystem;
|
|
221
|
+
activity.ExternalID = input.Item.ExternalID;
|
|
222
|
+
activity.LoggedByUserID = contextUser.ID;
|
|
223
|
+
}
|
|
224
|
+
fillManualFields(activity, input, typeID, contextUser) {
|
|
225
|
+
activity.ActivityTypeID = typeID;
|
|
226
|
+
activity.Title = input.Title.slice(0, 500);
|
|
227
|
+
activity.StartedAt = input.StartedAt;
|
|
228
|
+
activity.EndedAt = input.EndedAt ?? null;
|
|
229
|
+
activity.Description = input.Description ?? null;
|
|
230
|
+
activity.Direction = input.Direction ?? 'Internal';
|
|
231
|
+
activity.Status = input.Status ?? 'Logged';
|
|
232
|
+
activity.Visibility = input.Visibility ?? 'Internal';
|
|
233
|
+
activity.Source = input.Source ?? 'Manual';
|
|
234
|
+
activity.Location = input.Location ?? null;
|
|
235
|
+
activity.Details = input.Details ? JSON.stringify(input.Details) : null;
|
|
236
|
+
activity.SourceSystem = input.SourceSystem ?? null;
|
|
237
|
+
activity.ExternalID = input.ExternalID ?? null;
|
|
238
|
+
activity.LoggedByUserID = contextUser.ID;
|
|
239
|
+
}
|
|
240
|
+
planLinks(input) {
|
|
241
|
+
const links = [];
|
|
242
|
+
for (const party of input.Resolved) {
|
|
243
|
+
links.push({
|
|
244
|
+
Role: party.Role,
|
|
245
|
+
EntityName: party.Kind === 'Person' ? ACTIVITY_SYNC_ENTITIES.People : ACTIVITY_SYNC_ENTITIES.Organizations,
|
|
246
|
+
RecordID: party.RecordID,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
for (const party of input.Unresolved) {
|
|
250
|
+
links.push({
|
|
251
|
+
Role: party.Role,
|
|
252
|
+
IdentityKind: party.Kind,
|
|
253
|
+
IdentityValue: party.Value,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return this.dedupeLinks(links);
|
|
257
|
+
}
|
|
258
|
+
dedupeLinks(links) {
|
|
259
|
+
const seen = new Set();
|
|
260
|
+
const out = [];
|
|
261
|
+
for (const link of links) {
|
|
262
|
+
const key = link.RecordID
|
|
263
|
+
? `r:${link.EntityName}:${link.RecordID.toLowerCase()}`
|
|
264
|
+
: `i:${link.IdentityKind}:${(link.IdentityValue ?? '').toLowerCase()}`;
|
|
265
|
+
if (seen.has(key))
|
|
266
|
+
continue;
|
|
267
|
+
seen.add(key);
|
|
268
|
+
out.push(link);
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
emptyResult() {
|
|
273
|
+
return {
|
|
274
|
+
Success: false,
|
|
275
|
+
ActivityID: null,
|
|
276
|
+
AlreadyPresent: false,
|
|
277
|
+
Links: [],
|
|
278
|
+
Activity: null,
|
|
279
|
+
Issues: [],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
async writeLink(link, activityID, sequence, provider, contextUser) {
|
|
283
|
+
const entityID = link.EntityName
|
|
284
|
+
? provider.Entities.find((e) => e.Name === link.EntityName)?.ID
|
|
285
|
+
: undefined;
|
|
286
|
+
if (link.EntityName && !entityID)
|
|
287
|
+
return null;
|
|
288
|
+
const row = await provider.GetEntityObject(ACTIVITY_SYNC_ENTITIES.ActivityLinks, contextUser);
|
|
289
|
+
row.NewRecord();
|
|
290
|
+
row.ActivityID = activityID;
|
|
291
|
+
row.Role = link.Role;
|
|
292
|
+
row.Sequence = sequence;
|
|
293
|
+
if (link.RecordID && entityID) {
|
|
294
|
+
row.EntityID = entityID;
|
|
295
|
+
row.RecordID = link.RecordID;
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
row.IdentityKind = link.IdentityKind ?? 'Email';
|
|
299
|
+
row.IdentityValue = link.IdentityValue ?? '';
|
|
300
|
+
}
|
|
301
|
+
if (!(await row.Save()))
|
|
302
|
+
return null;
|
|
303
|
+
return row;
|
|
304
|
+
}
|
|
305
|
+
async resolveTypeByCode(code, contextUser) {
|
|
306
|
+
const rv = new RunView();
|
|
307
|
+
const res = await rv.RunView({
|
|
308
|
+
EntityName: ACTIVITY_SYNC_ENTITIES.ActivityTypes,
|
|
309
|
+
ExtraFilter: `Code = '${EscapeText(code)}'`,
|
|
310
|
+
MaxRows: 1,
|
|
311
|
+
ResultType: 'simple',
|
|
312
|
+
}, contextUser);
|
|
313
|
+
return res.Success ? (res.Results?.[0]?.ID ?? null) : null;
|
|
314
|
+
}
|
|
315
|
+
async findByExternalKey(sourceSystem, externalID, contextUser) {
|
|
316
|
+
const rv = new RunView();
|
|
317
|
+
const res = await rv.RunView({
|
|
318
|
+
EntityName: ACTIVITY_SYNC_ENTITIES.Activities,
|
|
319
|
+
ExtraFilter: `SourceSystem = '${EscapeText(sourceSystem)}' AND ExternalID = '${EscapeText(externalID)}'`,
|
|
320
|
+
MaxRows: 1,
|
|
321
|
+
ResultType: 'simple',
|
|
322
|
+
}, contextUser);
|
|
323
|
+
return res.Success ? (res.Results?.[0]?.ID ?? null) : null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
//# sourceMappingURL=writer.js.map
|