@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.
Files changed (90) hide show
  1. package/dist/ActivitySyncEngine.d.ts +118 -0
  2. package/dist/ActivitySyncEngine.d.ts.map +1 -0
  3. package/dist/ActivitySyncEngine.js +620 -0
  4. package/dist/ActivitySyncEngine.js.map +1 -0
  5. package/dist/BaseActivitySyncExtension.d.ts +99 -0
  6. package/dist/BaseActivitySyncExtension.d.ts.map +1 -0
  7. package/dist/BaseActivitySyncExtension.js +3 -0
  8. package/dist/BaseActivitySyncExtension.js.map +1 -0
  9. package/dist/BaseActivitySyncProvider.d.ts +40 -0
  10. package/dist/BaseActivitySyncProvider.d.ts.map +1 -0
  11. package/dist/BaseActivitySyncProvider.js +76 -0
  12. package/dist/BaseActivitySyncProvider.js.map +1 -0
  13. package/dist/action-result.d.ts +38 -0
  14. package/dist/action-result.d.ts.map +1 -0
  15. package/dist/action-result.js +43 -0
  16. package/dist/action-result.js.map +1 -0
  17. package/dist/entity-names.d.ts +19 -0
  18. package/dist/entity-names.d.ts.map +1 -0
  19. package/dist/entity-names.js +19 -0
  20. package/dist/entity-names.js.map +1 -0
  21. package/dist/extensions.d.ts +37 -0
  22. package/dist/extensions.d.ts.map +1 -0
  23. package/dist/extensions.js +80 -0
  24. package/dist/extensions.js.map +1 -0
  25. package/dist/identity.d.ts +30 -0
  26. package/dist/identity.d.ts.map +1 -0
  27. package/dist/identity.js +90 -0
  28. package/dist/identity.js.map +1 -0
  29. package/dist/index.d.ts +29 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +29 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/load.d.ts +17 -0
  34. package/dist/load.d.ts.map +1 -0
  35. package/dist/load.js +29 -0
  36. package/dist/load.js.map +1 -0
  37. package/dist/manual-log.d.ts +33 -0
  38. package/dist/manual-log.d.ts.map +1 -0
  39. package/dist/manual-log.js +230 -0
  40. package/dist/manual-log.js.map +1 -0
  41. package/dist/participants.d.ts +47 -0
  42. package/dist/participants.d.ts.map +1 -0
  43. package/dist/participants.js +59 -0
  44. package/dist/participants.js.map +1 -0
  45. package/dist/providers/FixtureActivitySyncProvider.d.ts +21 -0
  46. package/dist/providers/FixtureActivitySyncProvider.d.ts.map +1 -0
  47. package/dist/providers/FixtureActivitySyncProvider.js +58 -0
  48. package/dist/providers/FixtureActivitySyncProvider.js.map +1 -0
  49. package/dist/providers/GraphMessageMapper.d.ts +37 -0
  50. package/dist/providers/GraphMessageMapper.d.ts.map +1 -0
  51. package/dist/providers/GraphMessageMapper.js +91 -0
  52. package/dist/providers/GraphMessageMapper.js.map +1 -0
  53. package/dist/providers/MSGraphActivitySyncProvider.d.ts +22 -0
  54. package/dist/providers/MSGraphActivitySyncProvider.d.ts.map +1 -0
  55. package/dist/providers/MSGraphActivitySyncProvider.js +62 -0
  56. package/dist/providers/MSGraphActivitySyncProvider.js.map +1 -0
  57. package/dist/providers/MSGraphCalendarSyncProvider.d.ts +60 -0
  58. package/dist/providers/MSGraphCalendarSyncProvider.d.ts.map +1 -0
  59. package/dist/providers/MSGraphCalendarSyncProvider.js +145 -0
  60. package/dist/providers/MSGraphCalendarSyncProvider.js.map +1 -0
  61. package/dist/qualification.d.ts +91 -0
  62. package/dist/qualification.d.ts.map +1 -0
  63. package/dist/qualification.js +62 -0
  64. package/dist/qualification.js.map +1 -0
  65. package/dist/run.d.ts +67 -0
  66. package/dist/run.d.ts.map +1 -0
  67. package/dist/run.js +84 -0
  68. package/dist/run.js.map +1 -0
  69. package/dist/sql.d.ts +17 -0
  70. package/dist/sql.d.ts.map +1 -0
  71. package/dist/sql.js +50 -0
  72. package/dist/sql.js.map +1 -0
  73. package/dist/stages.d.ts +55 -0
  74. package/dist/stages.d.ts.map +1 -0
  75. package/dist/stages.js +109 -0
  76. package/dist/stages.js.map +1 -0
  77. package/dist/types.d.ts +130 -0
  78. package/dist/types.d.ts.map +1 -0
  79. package/dist/types.js +38 -0
  80. package/dist/types.js.map +1 -0
  81. package/dist/watermark.d.ts +63 -0
  82. package/dist/watermark.d.ts.map +1 -0
  83. package/dist/watermark.js +103 -0
  84. package/dist/watermark.js.map +1 -0
  85. package/dist/writer.d.ts +132 -0
  86. package/dist/writer.d.ts.map +1 -0
  87. package/dist/writer.js +326 -0
  88. package/dist/writer.js.map +1 -0
  89. package/package.json +32 -5
  90. 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":"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"}
@@ -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