@engine9/schemas 1.9.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 (122) hide show
  1. package/.eslintignore +1 -0
  2. package/.prettierrc +7 -0
  3. package/AGENTS.md +10 -0
  4. package/LICENSE +21 -0
  5. package/README.md +33 -0
  6. package/channels/email/README.md +83 -0
  7. package/channels/email/index.js +22 -0
  8. package/channels/email/search.js +86 -0
  9. package/channels/email/segments.js +81 -0
  10. package/eslint.config.js +21 -0
  11. package/event/README.md +79 -0
  12. package/event/index.js +17 -0
  13. package/event/schema.js +111 -0
  14. package/event/search.js +51 -0
  15. package/message/index.js +8 -0
  16. package/message/schema.js +130 -0
  17. package/message/ui.console.json5 +233 -0
  18. package/package.json +26 -0
  19. package/person/index.js +100 -0
  20. package/person/metrics.js +17 -0
  21. package/person/schema.js +47 -0
  22. package/person/transforms/inbound/normalize_field_names.js +16 -0
  23. package/person/transforms/inbound/upsert_tables.js +21 -0
  24. package/person/transforms/outbound/appendName.js +32 -0
  25. package/person/transforms/simpleMap.js +34 -0
  26. package/person/ui.console.json5 +165 -0
  27. package/person/upsert.test.js +72 -0
  28. package/person_address/index.js +30 -0
  29. package/person_address/normalize.js +232 -0
  30. package/person_address/normalize.test.js +69 -0
  31. package/person_address/schema.js +49 -0
  32. package/person_address/transforms/inbound/normalize.js +23 -0
  33. package/person_address/transforms/inbound/upsert_tables.js +51 -0
  34. package/person_address/transforms/outbound/appendAddress.js +87 -0
  35. package/person_address/ui.console.json5 +31 -0
  36. package/person_custom/index.js +24 -0
  37. package/person_custom/transforms/appendRemoteId.js +31 -0
  38. package/person_custom/transforms/inbound/upsert_tables.js +31 -0
  39. package/person_email/README.md +82 -0
  40. package/person_email/index.js +35 -0
  41. package/person_email/schema.js +49 -0
  42. package/person_email/search.js +61 -0
  43. package/person_email/segments.js +16 -0
  44. package/person_email/transforms/inbound/extract_identifiers.js +33 -0
  45. package/person_email/transforms/inbound/upsert_tables.js +142 -0
  46. package/person_email/transforms/outbound/appendEmail.js +32 -0
  47. package/person_email/transforms/outbound/appendEmailHash.js +20 -0
  48. package/person_email/transforms/outbound/tables.js +14 -0
  49. package/person_email/ui.console.json5 +58 -0
  50. package/person_email/upsert.test.js +126 -0
  51. package/person_hash/README.md +70 -0
  52. package/person_hash/hash.js +103 -0
  53. package/person_hash/hash.test.js +165 -0
  54. package/person_hash/index.js +37 -0
  55. package/person_hash/schema.js +54 -0
  56. package/person_hash/search.js +86 -0
  57. package/person_hash/transforms/inbound/extract_identifiers.js +70 -0
  58. package/person_hash/transforms/inbound/upsert_tables.js +105 -0
  59. package/person_hash/transforms/outbound/appendEmailHash.js +42 -0
  60. package/person_hash/transforms/outbound/appendPhoneHash.js +42 -0
  61. package/person_hash/ui.console.json5 +80 -0
  62. package/person_phone/README.md +79 -0
  63. package/person_phone/index.js +33 -0
  64. package/person_phone/schema.js +58 -0
  65. package/person_phone/search.js +79 -0
  66. package/person_phone/segments.js +26 -0
  67. package/person_phone/transforms/inbound/extract_identifiers.js +59 -0
  68. package/person_phone/transforms/inbound/upsert_tables.js +119 -0
  69. package/person_phone/transforms/outbound/appendPhoneHash.js +37 -0
  70. package/person_phone/ui.console.json5 +31 -0
  71. package/person_phone/upsert.test.js +50 -0
  72. package/person_remote/index.js +58 -0
  73. package/person_remote/metrics.js +17 -0
  74. package/person_remote/schema.js +21 -0
  75. package/person_remote/transforms/inbound/extract_identifiers.js +34 -0
  76. package/person_remote/transforms/inbound/upsert_tables.js +92 -0
  77. package/person_remote/transforms/outbound/appendRemotePersonId.js +59 -0
  78. package/plugin/index.js +13 -0
  79. package/plugin/schema.js +70 -0
  80. package/plugin/settings.js +19 -0
  81. package/plugin/ui.console.json5 +79 -0
  82. package/report/index.js +7 -0
  83. package/report/ui.console.json5 +21 -0
  84. package/segment/index.js +21 -0
  85. package/segment/schema.js +81 -0
  86. package/segment/search.js +64 -0
  87. package/segment/transforms/inbound/upsert.js +44 -0
  88. package/segment/ui.console.json5 +142 -0
  89. package/segment_stats/index.js +16 -0
  90. package/segment_stats/schema.js +30 -0
  91. package/source_code/index.js +16 -0
  92. package/source_code/metrics.js +28 -0
  93. package/source_code/schema.js +29 -0
  94. package/source_code/ui.console.json5 +31 -0
  95. package/stacks/limited-pii/index.js +8 -0
  96. package/stacks/limited-pii/stack.json +22 -0
  97. package/stacks/stacks.test.js +27 -0
  98. package/stacks/standard/index.js +7 -0
  99. package/stacks/standard/stack.json +20 -0
  100. package/tag/index.js +13 -0
  101. package/tag/schema.js +23 -0
  102. package/task/index.js +13 -0
  103. package/task/schema.js +54 -0
  104. package/task/ui.console.json5 +60 -0
  105. package/timeline/index.js +18 -0
  106. package/timeline/schema.js +18 -0
  107. package/timeline/transforms/inbound/upsert_tables.js +22 -0
  108. package/timeline/ui.console.json5 +31 -0
  109. package/transaction/contact_details/index.js +11 -0
  110. package/transaction/contact_details/schema.js +22 -0
  111. package/transaction/core/README.md +79 -0
  112. package/transaction/core/attribute.test.js +231 -0
  113. package/transaction/core/index.js +31 -0
  114. package/transaction/core/metrics.js +20 -0
  115. package/transaction/core/schema.js +45 -0
  116. package/transaction/core/search.js +87 -0
  117. package/transaction/core/segments.js +14 -0
  118. package/transaction/core/transforms/appendTransactionSummary.js +101 -0
  119. package/transaction/core/transforms/attribute.js +327 -0
  120. package/transaction/core/transforms/inbound/upsert_tables.js +57 -0
  121. package/utilities/limited-pii/index.js +14 -0
  122. package/utilities/limited-pii/settings.js +12 -0
@@ -0,0 +1,327 @@
1
+ import { mergeIntoQueue, uuidIsValid } from '@engine9/input-tools';
2
+
3
+ /*
4
+ Batch stand-in for TransactionBase.attribute.
5
+
6
+ Runs after the transaction upsert when the inbound weaver includes it.
7
+ A first pass usually finds no override rows. Run it again to re-attribute
8
+ once transaction_metadata_override has a row for the remote transaction.
9
+
10
+ A message matches when its source code equals the transaction source code
11
+ (or transaction_source_code_override) and publish_date is earlier than the
12
+ transaction timestamp plus 29 hours. When several messages match, the latest
13
+ publish_date wins.
14
+ */
15
+
16
+ const PUBLISH_GRACE_MS = 29 * 60 * 60 * 1000;
17
+
18
+ function sqlQuote(value) {
19
+ return String(value).replace(/'/g, "''");
20
+ }
21
+
22
+ export const bindings = ({ pluginId } = {}) => {
23
+ if (!pluginId) {
24
+ throw new Error('pluginId is required for attribute lookups');
25
+ }
26
+ if (!uuidIsValid(pluginId)) {
27
+ throw new Error(`pluginId must be a uuid for attribute lookups, got ${pluginId}`);
28
+ }
29
+ const safePluginId = sqlQuote(pluginId);
30
+ return {
31
+ // remote_entry_id = remote_transaction_id, transaction_bot_id = plugin.remote_plugin_id
32
+ overrides: {
33
+ path: 'sql.query',
34
+ options: {
35
+ table: 'transaction_metadata_override',
36
+ columns: [
37
+ 'remote_transaction_id',
38
+ 'transaction_bot_id',
39
+ 'transaction_source_code_override',
40
+ 'override_message_id',
41
+ 'ignore_reason',
42
+ { eql: 'source_code_dictionary.source_code_id', name: 'dictionary_source_code_id' },
43
+ { eql: 'message_source_code.source_code', name: 'matched_source_code' },
44
+ { eql: 'message_source_code.message_id', name: 'matched_message_id' },
45
+ { eql: 'message_source_code.publish_date', name: 'matched_publish_date' },
46
+ { eql: 'global_message.id', name: 'matched_message_uuid' },
47
+ { eql: 'global_message.publish_date', name: 'matched_message_publish_date' },
48
+ { eql: 'global_message.do_not_attribute', name: 'matched_do_not_attribute' }
49
+ ],
50
+ lookup: [{ personIdField: 'remote_entry_id', tableLookupField: 'remote_transaction_id' }],
51
+ joins: [
52
+ {
53
+ table: 'plugin',
54
+ join_eql: 'transaction_metadata_override.transaction_bot_id=plugin.remote_plugin_id'
55
+ },
56
+ {
57
+ table: 'source_code_dictionary',
58
+ type: 'left',
59
+ join_eql:
60
+ 'transaction_metadata_override.transaction_source_code_override=source_code_dictionary.source_code'
61
+ },
62
+ {
63
+ table: 'message_source_code',
64
+ type: 'left',
65
+ join_eql: 'transaction_metadata_override.transaction_source_code_override=message_source_code.source_code'
66
+ },
67
+ {
68
+ table: 'global_message',
69
+ type: 'left',
70
+ join_eql: 'message_source_code.message_id=global_message.message_id'
71
+ }
72
+ ],
73
+ conditions: [{ eql: `plugin.id='${safePluginId}'` }]
74
+ }
75
+ },
76
+ messageSourceCodes: {
77
+ path: 'sql.query',
78
+ options: {
79
+ table: 'message_source_code',
80
+ columns: [
81
+ 'source_code',
82
+ 'message_id',
83
+ 'publish_date',
84
+ { eql: 'global_message.id', name: 'message_uuid' },
85
+ { eql: 'global_message.publish_date', name: 'message_publish_date' },
86
+ { eql: 'global_message.do_not_attribute', name: 'do_not_attribute' }
87
+ ],
88
+ lookup: [{ personIdField: 'source_code', tableLookupField: 'source_code' }],
89
+ joins: [
90
+ {
91
+ table: 'global_message',
92
+ type: 'left',
93
+ join_eql: 'message_source_code.message_id=global_message.message_id'
94
+ }
95
+ ]
96
+ }
97
+ },
98
+ // remote_input_id is only on input, so the plugin join does not make `id` ambiguous
99
+ pluginInputs: {
100
+ path: 'sql.query',
101
+ options: {
102
+ table: 'input',
103
+ columns: [
104
+ { eql: 'input.id', name: 'input_id' },
105
+ 'remote_input_id',
106
+ { eql: 'plugin.remote_plugin_id', name: 'remote_plugin_id' }
107
+ ],
108
+ lookup: [{ personIdField: 'remote_input_id', tableLookupField: 'remote_input_id' }],
109
+ joins: [
110
+ {
111
+ table: 'plugin',
112
+ join_eql: 'input.plugin_id=plugin.id'
113
+ }
114
+ ],
115
+ conditions: [{ eql: `plugin.id='${safePluginId}'` }]
116
+ }
117
+ },
118
+ tablesToUpsert: { path: 'sql.tables.upsert' }
119
+ };
120
+ };
121
+
122
+ export const type = 'upsert';
123
+
124
+ function blankToNull(value) {
125
+ if (value == null) return null;
126
+ const text = String(value).trim();
127
+ return text === '' ? null : text;
128
+ }
129
+
130
+ function positiveInt(value) {
131
+ const n = parseInt(value, 10);
132
+ return Number.isFinite(n) && n > 0 ? n : 0;
133
+ }
134
+
135
+ function integerMessageId(value) {
136
+ if (value == null || value === '') return null;
137
+ if (typeof value === 'string' && uuidIsValid(value)) return null;
138
+ const n = Number(value);
139
+ return Number.isInteger(n) && n > 0 ? n : null;
140
+ }
141
+
142
+ function publishIsBefore(publishDate, ts) {
143
+ if (publishDate == null || publishDate === '' || ts == null || ts === '') return false;
144
+ const publishMs = new Date(publishDate).getTime();
145
+ const tsMs = new Date(ts).getTime();
146
+ if (Number.isNaN(publishMs) || Number.isNaN(tsMs)) return false;
147
+ return publishMs < tsMs + PUBLISH_GRACE_MS;
148
+ }
149
+
150
+ function candidateFromMessageSource(row) {
151
+ const sourceCode = blankToNull(row?.source_code);
152
+ if (!sourceCode) return null;
153
+ const messageId = integerMessageId(row.message_id);
154
+ const messageUuid = uuidIsValid(row.message_uuid) ? row.message_uuid : null;
155
+ if (messageId == null && !messageUuid) return null;
156
+ return {
157
+ source_code: sourceCode,
158
+ message_id: messageId,
159
+ message_uuid: messageUuid,
160
+ publish_date: row.message_publish_date || row.publish_date || null,
161
+ do_not_attribute: row.do_not_attribute
162
+ };
163
+ }
164
+
165
+ function candidateFromOverride(row) {
166
+ const sourceCode = blankToNull(row?.matched_source_code);
167
+ if (!sourceCode) return null;
168
+ return candidateFromMessageSource({
169
+ source_code: sourceCode,
170
+ message_id: row.matched_message_id,
171
+ message_uuid: row.matched_message_uuid,
172
+ publish_date: row.matched_publish_date,
173
+ message_publish_date: row.matched_message_publish_date,
174
+ do_not_attribute: row.matched_do_not_attribute
175
+ });
176
+ }
177
+
178
+ function chooseMessage(candidates, ts, { respectDoNotAttribute }) {
179
+ let best = null;
180
+ let bestPublish = null;
181
+ for (const candidate of candidates) {
182
+ if (!candidate) continue;
183
+ if (respectDoNotAttribute && Number(candidate.do_not_attribute) === 1) continue;
184
+ if (!publishIsBefore(candidate.publish_date, ts)) continue;
185
+ const publishMs = new Date(candidate.publish_date).getTime();
186
+ const messageKey = String(candidate.message_id ?? candidate.message_uuid ?? '');
187
+ const bestKey = best ? String(best.message_id ?? best.message_uuid ?? '') : '';
188
+ if (!best || publishMs > bestPublish || (publishMs === bestPublish && messageKey < bestKey)) {
189
+ best = candidate;
190
+ bestPublish = publishMs;
191
+ }
192
+ }
193
+ return best;
194
+ }
195
+
196
+ function remotePluginIdFor(row, pluginInputs, overrideBotId) {
197
+ const remoteInputId = blankToNull(row.remote_input_id);
198
+ if (remoteInputId) {
199
+ const match = pluginInputs.find((p) => String(p.remote_input_id ?? '') === remoteInputId);
200
+ if (match?.remote_plugin_id) return String(match.remote_plugin_id);
201
+ }
202
+ const inputId = row.input_id;
203
+ if (inputId) {
204
+ const match = pluginInputs.find((p) => (p.input_id ?? p.id) === inputId && p.remote_plugin_id);
205
+ if (match?.remote_plugin_id) return String(match.remote_plugin_id);
206
+ }
207
+ return overrideBotId ? String(overrideBotId) : null;
208
+ }
209
+
210
+ function mergeAttribution(existing, incoming) {
211
+ return { ...existing, ...incoming };
212
+ }
213
+
214
+ function mergeTransactionUpdate(existing, incoming) {
215
+ return { ...existing, ...incoming, id: existing.id ?? incoming.id };
216
+ }
217
+
218
+ /*
219
+ Upsert already queued this batch object: write the attribution columns onto
220
+ that object. A later attribute pass queues a narrow transaction update by id.
221
+ */
222
+ function applyTransactionAttribution(tablesToUpsert, row, fields) {
223
+ Object.assign(row, fields);
224
+ tablesToUpsert.transaction = tablesToUpsert.transaction || [];
225
+ const queued = tablesToUpsert.transaction.find((item) => item === row || (row.id != null && item.id === row.id));
226
+ if (queued) {
227
+ if (queued !== row) Object.assign(queued, fields);
228
+ return;
229
+ }
230
+ if (row.id == null || row.id === '') {
231
+ throw new Error('attribute requires a transaction id, or a transaction row already queued by upsert');
232
+ }
233
+ mergeIntoQueue(tablesToUpsert.transaction, { id: row.id, ...fields }, {
234
+ keyFields: ['id'],
235
+ merge: mergeTransactionUpdate,
236
+ label: 'transaction'
237
+ });
238
+ }
239
+
240
+ export async function transform({
241
+ batch,
242
+ tablesToUpsert,
243
+ overrides = [],
244
+ messageSourceCodes = [],
245
+ pluginInputs = []
246
+ }) {
247
+ if (!batch?.length) return;
248
+ tablesToUpsert.attribution = tablesToUpsert.attribution || [];
249
+
250
+ const overridesByRemote = new Map();
251
+ for (const row of overrides) {
252
+ const remoteId = blankToNull(row.remote_transaction_id);
253
+ if (!remoteId) continue;
254
+ if (!overridesByRemote.has(remoteId)) overridesByRemote.set(remoteId, []);
255
+ overridesByRemote.get(remoteId).push(row);
256
+ }
257
+
258
+ const messagesByCode = new Map();
259
+ for (const row of messageSourceCodes) {
260
+ const candidate = candidateFromMessageSource(row);
261
+ if (!candidate) continue;
262
+ if (!messagesByCode.has(candidate.source_code)) messagesByCode.set(candidate.source_code, []);
263
+ messagesByCode.get(candidate.source_code).push(candidate);
264
+ }
265
+
266
+ batch.forEach((row) => {
267
+ const remoteId = blankToNull(row.remote_entry_id);
268
+ const overrideRows = remoteId ? overridesByRemote.get(remoteId) || [] : [];
269
+ const override = overrideRows[0] || null;
270
+ const overrideCode = blankToNull(override?.transaction_source_code_override);
271
+ // An override row with a blank code does not fall back to the transaction
272
+ // source code. The legacy column is NOT NULL default '', and attribute()
273
+ // treats any non-null override as the only code to match.
274
+ const usingOverride = override != null;
275
+ const effectiveCode = usingOverride ? overrideCode : blankToNull(row.source_code);
276
+
277
+ let overrideSourceCodeId = positiveInt(override?.dictionary_source_code_id);
278
+ if (!overrideSourceCodeId && usingOverride && overrideCode === blankToNull(row.source_code)) {
279
+ overrideSourceCodeId = positiveInt(row.source_code_id);
280
+ }
281
+
282
+ const overrideMessages = overrideRows.map(candidateFromOverride).filter(Boolean);
283
+ const lookedUp = effectiveCode ? messagesByCode.get(effectiveCode) || [] : [];
284
+ const match = chooseMessage(usingOverride ? [...overrideMessages, ...lookedUp] : lookedUp, row.ts, {
285
+ respectDoNotAttribute: !usingOverride
286
+ });
287
+
288
+ const recommendedUuid = match?.message_uuid || null;
289
+ const fields = {
290
+ override_source_code_id: overrideSourceCodeId,
291
+ final_source_code_id: overrideSourceCodeId > 0 ? overrideSourceCodeId : positiveInt(row.source_code_id),
292
+ recommended_message_id: recommendedUuid,
293
+ override_message_id:
294
+ row.override_message_id && uuidIsValid(row.override_message_id) ? row.override_message_id : null,
295
+ final_message_id: null
296
+ };
297
+ fields.final_message_id = fields.override_message_id || recommendedUuid || null;
298
+ applyTransactionAttribution(tablesToUpsert, row, fields);
299
+
300
+ if (!match || !remoteId || match.message_id == null) return;
301
+ const botId = remotePluginIdFor(row, pluginInputs, override?.transaction_bot_id);
302
+ if (!botId) {
303
+ throw new Error(
304
+ `attribute matched a message for remote_entry_id ${remoteId} but plugin.remote_plugin_id was not loaded`
305
+ );
306
+ }
307
+ mergeIntoQueue(
308
+ tablesToUpsert.attribution,
309
+ {
310
+ transaction_bot_id: botId,
311
+ remote_transaction_id: remoteId,
312
+ recommended_message_id: match.message_id
313
+ },
314
+ {
315
+ keyFields: ['transaction_bot_id', 'remote_transaction_id'],
316
+ merge: mergeAttribution,
317
+ label: 'attribution'
318
+ }
319
+ );
320
+ });
321
+ }
322
+
323
+ export default {
324
+ bindings,
325
+ type,
326
+ transform
327
+ };
@@ -0,0 +1,57 @@
1
+ import { getEntryTypeId, getTimelineEntryUUID } from '@engine9/input-tools';
2
+ /* appends a recurs_id no matter what */
3
+ function appendRecursId(d) {
4
+ //If there's a recurs_id that's positive, use it.
5
+ if (parseInt(d.recurs_id)) {
6
+ d.recurs_id = parseInt(d.recurs_id);
7
+ return d;
8
+ }
9
+ //Otherwise set it based on the 'recurs' flag
10
+ switch (d.recurs) {
11
+ case 'semi-annually':
12
+ d.recurs_id = 6;
13
+ break;
14
+ case 'annually':
15
+ d.recurs_id = 5;
16
+ break;
17
+ case 'quarterly':
18
+ d.recurs_id = 4;
19
+ break;
20
+ case 'monthly':
21
+ d.recurs_id = 3;
22
+ break;
23
+ case 'weekly':
24
+ d.recurs_id = 2;
25
+ break;
26
+ case 'daily':
27
+ d.recurs_id = 1;
28
+ break;
29
+ default: {
30
+ if (d.recurring_number > 1) d.recurs_id = 3;
31
+ else d.recurs_id = 0;
32
+ }
33
+ }
34
+ return d;
35
+ }
36
+ export const bindings = {
37
+ tablesToUpsert: { path: 'sql.tables.upsert' }
38
+ };
39
+ export async function transform(opts) {
40
+ const { batch, tablesToUpsert, pluginId } = opts;
41
+ if (batch.length === 0) return;
42
+ tablesToUpsert.transaction = tablesToUpsert.transaction || [];
43
+ const hasRecurs = batch.some((b) => b.recurs !== undefined || b.recurs_id !== undefined);
44
+
45
+ batch.forEach((o) => {
46
+ if (hasRecurs) {
47
+ appendRecursId(o);
48
+ }
49
+ o.entry_type_id = getEntryTypeId(o);
50
+ o.id = getTimelineEntryUUID(o, { defaults: { plugin_id: pluginId } });
51
+ tablesToUpsert.transaction.push(o);
52
+ });
53
+ }
54
+ export default {
55
+ bindings,
56
+ transform
57
+ };
@@ -0,0 +1,14 @@
1
+ import { settings } from './settings.js';
2
+
3
+ const metadata = {
4
+ name: '@engine9/schemas/utilities/limited-pii',
5
+ description:
6
+ 'On-demand PII controls: exclude_pii steers installDefaultPlugins and default export table lists without installing the limited-pii stack by itself.',
7
+ unique: true
8
+ };
9
+
10
+ export { metadata, settings };
11
+ export default {
12
+ metadata,
13
+ settings
14
+ };
@@ -0,0 +1,12 @@
1
+ export const settings = [
2
+ {
3
+ name: 'exclude_pii',
4
+ type: 'boolean',
5
+ default: false,
6
+ description:
7
+ 'When true, installDefaultPlugins prefers @engine9/schemas/stacks/limited-pii over default_stack, and installs of stacks/standard, person_email, person_phone, and person_address are refused. Does not drop existing plaintext tables. Also omits those tables from the default export list when tables is not set.'
8
+ }
9
+ ];
10
+ export default {
11
+ settings
12
+ };