@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,130 @@
1
+ export const tables = [
2
+ {
3
+ name: 'message',
4
+ columns: {
5
+ id: 'id_uuid',
6
+ message_set_id: 'foreign_uuid',
7
+ channel: 'string',
8
+ name: 'string',
9
+ status: 'string',
10
+ publish_date: 'datetime',
11
+ primary_source_code: { type: 'string', length: 180 },
12
+ primary_source_code_override: { type: 'string', length: 180 },
13
+ final_primary_source_code: { type: 'string', length: 180 },
14
+ source_remote_id: 'string',
15
+ source_submodule: 'string',
16
+ created_at: 'created_at',
17
+ modified_at: 'modified_at'
18
+ },
19
+ indexes: [{ columns: 'id', primary: true }, { columns: ['publish_date'] }, { columns: ['message_set_id'] }]
20
+ },
21
+ {
22
+ name: 'message_set',
23
+ columns: {
24
+ id: 'id_uuid',
25
+ campaign_id: 'foreign_uuid',
26
+ name: 'string',
27
+ remote_message_set_id: 'string',
28
+ remote_message_set_name: 'string',
29
+ message_template_id: 'foreign_uuid',
30
+ type: 'string',
31
+ status: 'string',
32
+ schedule: 'string',
33
+ source_code_format: 'string',
34
+ // default_content: 'json', //unused?
35
+ default_variables: 'json',
36
+ required_targeting: 'json', // This is set initially, and can't be changed by users, for things such as Emailable people, etc
37
+ targeting: 'json',
38
+ sequencing: 'json', // sequence of messages
39
+ send_strategy: 'string', // Sending strategy, such as Best open rate, best giving rate, etc
40
+ created_at: 'created_at',
41
+ modified_at: 'modified_at'
42
+ },
43
+ indexes: [
44
+ { columns: 'id', primary: true },
45
+ { columns: ['campaign_id', 'remote_message_set_id'], unique: true }
46
+ ]
47
+ },
48
+ {
49
+ name: 'campaign',
50
+ columns: {
51
+ id: 'id_uuid',
52
+ name: 'string',
53
+ channel: 'string',
54
+ plugin_id: 'id_uuid', // can't be null, must have a value
55
+ remote_campaign_id: 'string',
56
+ remote_campaign_name: 'string',
57
+ created_at: 'created_at',
58
+ modified_at: 'modified_at'
59
+ },
60
+ indexes: [
61
+ { columns: 'id', primary: true },
62
+ { columns: ['plugin_id', 'remote_campaign_id'], unique: true }
63
+ ]
64
+ },
65
+ {
66
+ name: 'message_content',
67
+ columns: {
68
+ id: 'id_uuid',
69
+ message_id: 'foreign_uuid',
70
+ content: 'json',
71
+ remote_data: 'json',
72
+ variables: 'json',
73
+ targeting: 'json',
74
+ estimated_targets: 'int',
75
+ created_at: 'created_at',
76
+ modified_at: 'modified_at'
77
+ },
78
+ indexes: [{ columns: 'id', primary: true }, { columns: ['message_id'], unique: true }]
79
+ },
80
+ {
81
+ name: 'message_override',
82
+ columns: {
83
+ id: 'id_uuid',
84
+ message_id: 'foreign_uuid',
85
+ overrides: 'json',
86
+ created_at: 'created_at',
87
+ modified_at: 'modified_at'
88
+ },
89
+ indexes: [{ columns: 'id', primary: true }, { columns: ['message_id'], unique: true }]
90
+ },
91
+ {
92
+ name: 'message_statistics',
93
+ columns: {
94
+ id: 'id_uuid',
95
+ message_id: 'foreign_uuid',
96
+ statistics: 'json',
97
+ created_at: 'created_at',
98
+ modified_at: 'modified_at'
99
+ },
100
+ indexes: [{ columns: 'id', primary: true }, { columns: ['message_id'], unique: true }]
101
+ },
102
+ {
103
+ name: 'message_queue',
104
+ columns: {
105
+ id: 'id_uuid',
106
+ message_id: 'foreign_uuid',
107
+ publish_metadata: 'json',
108
+ error: 'json',
109
+ created_at: 'created_at',
110
+ modified_at: 'modified_at'
111
+ },
112
+ indexes: [{ columns: 'id', primary: true }, { columns: ['message_id'], unique: true }]
113
+ },
114
+ {
115
+ name: 'message_template',
116
+ columns: {
117
+ id: 'id_uuid',
118
+ name: 'string',
119
+ content: 'json',
120
+ created_at: 'created_at',
121
+ modified_at: 'modified_at'
122
+ },
123
+ indexes: [{ columns: 'id', primary: true }]
124
+ }
125
+ // message_summary / global_message*_summary stay out of core until messaging is
126
+ // finalized; Frakture deployments may still ship those views separately.
127
+ ];
128
+ export default {
129
+ tables
130
+ };
@@ -0,0 +1,233 @@
1
+ {
2
+ menu: {
3
+ 'message-menu': {
4
+ title: 'Messages',
5
+ icon: 'messages',
6
+ type: 'collapse',
7
+ children: {
8
+ campaigns: {
9
+ title: 'Campaigns',
10
+ icon: 'campaign',
11
+ url: '/campaign'
12
+ }
13
+ /* messages:{
14
+ title: 'Messages',
15
+ icon:'message',
16
+ url: '/message',
17
+ },
18
+ */
19
+ }
20
+ }
21
+ },
22
+ routes: {
23
+ campaign: {
24
+ layout: 'full_width',
25
+ components: {
26
+ header: [
27
+ 'Campaigns',
28
+ {
29
+ component: 'Button',
30
+ properties: {
31
+ icon: 'plus',
32
+ content: 'Add Campaign',
33
+ onClick: {
34
+ action: 'navigate',
35
+ url: '/campaign/create'
36
+ }
37
+ }
38
+ }
39
+ ],
40
+ main: [
41
+ {
42
+ component: 'RecordTable',
43
+ properties: {
44
+ table: 'campaign',
45
+ columns: [
46
+ {
47
+ title: 'Id',
48
+ dataIndex: 'id',
49
+ sorter: true,
50
+ width: '20%'
51
+ },
52
+ {
53
+ title: 'Name',
54
+ dataIndex: 'name',
55
+ sorter: true,
56
+ template: '{{name}}',
57
+ width: '40%'
58
+ }
59
+ ],
60
+ onRecord: {
61
+ onClick: {
62
+ action: 'navigate',
63
+ url: '/campaign/{{record.id}}'
64
+ }
65
+ }
66
+ }
67
+ }
68
+ ]
69
+ }
70
+ },
71
+ 'campaign/create': {
72
+ layout: 'full_width',
73
+ components: {
74
+ main: [
75
+ {
76
+ path: 'edit',
77
+ component: 'RecordForm',
78
+ properties: {
79
+ table: 'campaign',
80
+ form: {
81
+ title: 'Create Campaign',
82
+ type: 'object',
83
+ required: [],
84
+ properties: {
85
+ name: { type: 'string', title: 'Name', default: '' }
86
+ }
87
+ },
88
+ uiSchema: {},
89
+ onSave: {
90
+ action: 'navigate',
91
+ url: '/campaign/{{record.id}}'
92
+ }
93
+ }
94
+ }
95
+ ]
96
+ }
97
+ },
98
+ 'campaign/:id/*': {
99
+ layout: 'sidebar',
100
+ components: {
101
+ sidebar: {
102
+ items: [
103
+ { component: 'Title', properties: { content: 'Emails' } },
104
+ {
105
+ component: 'Button',
106
+ properties: {
107
+ icon: 'plus',
108
+ content: 'Add Message Set',
109
+ onClick: {
110
+ action: 'object.create',
111
+ data: {
112
+ type: 'message_set',
113
+ attributes: {
114
+ label: 'Messages - {{date "MMMM, yyyy"}}',
115
+ campaign_id: '{{parameters.id}}'
116
+ }
117
+ },
118
+ redirect: '/campaign/{{parameters.id}}/message_set/{{record.id}}'
119
+ }
120
+ }
121
+ },
122
+ {
123
+ component: 'RecordList',
124
+ properties: {
125
+ table: 'message_set',
126
+ conditions: [{ eql: 'campaign_id={{id}}' }],
127
+ onRecord: {
128
+ onClick: {
129
+ action: 'navigate',
130
+ url: '/campaign/{{parameters.id}}/message_set/{{record.id}}/'
131
+ }
132
+ }
133
+ }
134
+ }
135
+ ]
136
+ },
137
+ main: [
138
+ { component: 'Title', properties: { content: 'Campaign Summary' } },
139
+ {
140
+ // 'path':'overview', //no path is the default
141
+ component: 'RecordDisplay',
142
+ properties: {
143
+ table: 'campaign',
144
+ id: '{{parameters.id}}',
145
+ template: '{{name}}'
146
+ }
147
+ }
148
+ ]
149
+ }
150
+ },
151
+ message: {
152
+ layout: 'full_width',
153
+ components: {
154
+ main: [
155
+ {
156
+ component: 'RecordTable',
157
+ properties: {
158
+ table: 'message',
159
+ columns: [
160
+ {
161
+ title: 'Id',
162
+ dataIndex: 'id',
163
+ sorter: true,
164
+ width: '20%'
165
+ },
166
+ {
167
+ title: 'Publish Date',
168
+ dataIndex: 'publish_date',
169
+ sorter: true,
170
+ width: '40%'
171
+ },
172
+ {
173
+ title: 'Name',
174
+ dataIndex: 'name',
175
+ sorter: true,
176
+ template: '{{name}}',
177
+ width: '40%'
178
+ }
179
+ ],
180
+ onRecord: {
181
+ onClick: {
182
+ action: 'navigate',
183
+ url: '/message/{{record.id}}'
184
+ }
185
+ }
186
+ }
187
+ }
188
+ ]
189
+ }
190
+ },
191
+ 'message/:id/*': {
192
+ layout: 'sidebar',
193
+ components: {
194
+ sidebar: [
195
+ {
196
+ component: 'RecordDisplay',
197
+ properties: {
198
+ table: 'message',
199
+ id: '{{parameters.id}}',
200
+ components: ['{{name}}']
201
+ }
202
+ }
203
+ ],
204
+ main: [
205
+ {
206
+ // 'path':'overview', //no path is the default
207
+ component: 'RecordDisplay',
208
+ properties: {
209
+ table: 'message',
210
+ id: '{{parameters.id}}',
211
+ components: ['{{name}}']
212
+ }
213
+ },
214
+ {
215
+ path: 'source_codes',
216
+ label: 'Source Codes',
217
+ component: 'RecordDisplay',
218
+ properties: {
219
+ table: 'message',
220
+ id: '{{parameters.id}}',
221
+ includes: {
222
+ source_codes: {
223
+ table: 'source_code'
224
+ }
225
+ },
226
+ components: ['{{source_code}}']
227
+ }
228
+ }
229
+ ]
230
+ }
231
+ }
232
+ }
233
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@engine9/schemas",
3
+ "version": "1.9.0",
4
+ "type": "module",
5
+ "description": "Engine9 standard schemas: shared tables, transforms, searches, and segments",
6
+ "main": "index.js",
7
+ "scripts": {
8
+ "test": "node --test person/upsert.test.js person_address/normalize.test.js person_email/upsert.test.js person_hash/hash.test.js person_phone/upsert.test.js stacks/stacks.test.js transaction/core/attribute.test.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "author": "",
14
+ "license": "MIT",
15
+ "dependencies": {
16
+ "@engine9/input-tools": "^2.4.1",
17
+ "@zerodep/address-normalize": "^2.4.10",
18
+ "handlebars": "^4.7.8",
19
+ "i18n-iso-countries": "^7.14.0"
20
+ },
21
+ "devDependencies": {
22
+ "eslint": "^9.34.0",
23
+ "eslint-plugin-jsonc": "^2.15.1",
24
+ "eslint-plugin-n": "^17.21.3"
25
+ }
26
+ }
@@ -0,0 +1,100 @@
1
+ import schema from './schema.js';
2
+ import upsertPerson from './transforms/inbound/upsert_tables.js';
3
+ import normalizeFieldNames from './transforms/inbound/normalize_field_names.js';
4
+ import simpleMap from './transforms/simpleMap.js';
5
+ import appendName from './transforms/outbound/appendName.js';
6
+ import metrics from './metrics.js';
7
+ const metadata = {
8
+ name: '@engine9/schemas/person',
9
+ schemas: ['schema.js'],
10
+ // Inbound people pipeline slots -> transform export keys (woven by core when installed)
11
+ inbound: {
12
+ normalize: ['normalizeFieldNames'],
13
+ upsert: ['upsertPerson']
14
+ }
15
+ };
16
+ const transforms = {
17
+ normalizeFieldNames,
18
+ upsertPerson,
19
+ simpleMap,
20
+ appendName
21
+ };
22
+ export const search = {
23
+ person: {
24
+ title: 'Primary Info',
25
+ form: {
26
+ title: 'Primary Info',
27
+ type: 'object',
28
+ properties: {
29
+ givenName: {
30
+ title: 'First Name',
31
+ type: 'string'
32
+ },
33
+ familyName: {
34
+ title: 'Last Name',
35
+ type: 'string'
36
+ },
37
+ ids: {
38
+ title: 'Person Ids (comma delimited)',
39
+ type: 'string'
40
+ },
41
+ idGreaterThanEqual: {
42
+ title: 'ID Greater than or equal to',
43
+ type: 'number'
44
+ },
45
+ idLessThan: {
46
+ title: 'ID less than',
47
+ type: 'number'
48
+ }
49
+ },
50
+ required: []
51
+ },
52
+ optionsToEQL: (options) => {
53
+ const text = [];
54
+ const { ids, idGreaterThanEqual, idLessThan, givenName = '', familyName = '' } = options;
55
+ const conditions = [];
56
+ if (ids) {
57
+ let arr = ids || '';
58
+ if (typeof ids === 'string') arr = ids.split(',');
59
+ if (arr.length === 0) arr = [0];
60
+ conditions.push({ eql: `id in (${arr.map((p) => parseInt(p, 10)).join(',')})` });
61
+ text.push(`Person ID in ${arr.map((p) => parseInt(p, 10)).join(',')}`);
62
+ }
63
+ if (idGreaterThanEqual) {
64
+ conditions.push({ eql: `id>=${idGreaterThanEqual}` });
65
+ text.push(`Person ID greater or equal to ${idGreaterThanEqual}`);
66
+ }
67
+ if (idLessThan) {
68
+ conditions.push({ eql: `id<${idLessThan}` });
69
+ text.push(`Person ID less than ${idLessThan}`);
70
+ }
71
+ if (givenName.length > 0) {
72
+ text.push(`First name like '${givenName}'`);
73
+ conditions.push({ eql: `given_name like '${givenName}'` });
74
+ }
75
+ if (familyName.length > 0) {
76
+ text.push(`Last name like '${givenName}'`);
77
+ conditions.push({ eql: `family_name like '${familyName}'` });
78
+ }
79
+ return {
80
+ text,
81
+ eql: {
82
+ table: 'person',
83
+ columns: ['id'],
84
+ conditions
85
+ }
86
+ };
87
+ }
88
+ }
89
+ };
90
+ export { metadata };
91
+ export { schema };
92
+ export { transforms };
93
+ export { metrics };
94
+ export default {
95
+ metadata,
96
+ schema,
97
+ transforms,
98
+ metrics,
99
+ search
100
+ };
@@ -0,0 +1,17 @@
1
+ export const people = () => {
2
+ return {
3
+ label: 'People',
4
+ description: '',
5
+ eql: {
6
+ table: 'person',
7
+ columns: [
8
+ { eql: 'min(created_at)', name: 'first_date' },
9
+ { eql: 'max(created_at)', name: 'last_date' },
10
+ { eql: 'count', name: 'records' }
11
+ ]
12
+ }
13
+ };
14
+ };
15
+ export default {
16
+ people
17
+ };
@@ -0,0 +1,47 @@
1
+ export const tables = [
2
+ {
3
+ name: 'person',
4
+ columns: {
5
+ id: 'id',
6
+ given_name: 'string',
7
+ family_name: 'string',
8
+ created_at: 'created_at',
9
+ modified_at: 'modified_at'
10
+ }
11
+ },
12
+ {
13
+ name: 'person_identifier',
14
+ columns: {
15
+ id: 'id',
16
+ person_id: 'person_id',
17
+ source_input_id: 'uuid',
18
+ id_type: {
19
+ type: 'enum',
20
+ nullable: false,
21
+ default_value: '',
22
+ values: ['', 'remote_person_id', 'email_hash_v1', 'phone_hash_v1', 'delegate']
23
+ },
24
+ id_value: 'id_string'
25
+ },
26
+ indexes: [
27
+ { columns: ['person_id'] },
28
+ { columns: ['id_value'] },
29
+ // useful for finding anyone related to an input
30
+ // same input can't have the same id value twice
31
+ { columns: ['source_input_id', 'id_value', 'person_id'], unique: true }
32
+ ]
33
+ }
34
+ /*
35
+ //sometimes useful, debating whether useful enough to be global
36
+ create view person_identifier_summary
37
+ as select p.*,
38
+ input.remote_input_id,
39
+ input.remote_input_name,input_type,
40
+ plugin_id,plugin.path,plugin.name,plugin.remote_plugin_id
41
+ from person_identifier p
42
+ join input on (p.source_input_id=input.id)
43
+ join plugin on (input.plugin_id=plugin.id); */
44
+ ];
45
+ export default {
46
+ tables
47
+ };
@@ -0,0 +1,16 @@
1
+ export async function transform({ batch }) {
2
+ batch.forEach((row) => {
3
+ const normalized = {};
4
+ Object.entries(row).forEach(([key, value]) => {
5
+ // Keep '.' so person_custom table.column keys survive normalize.
6
+ const normalizedKey = String(key).toLowerCase().replace(/[^a-z0-9_.]/g, '_');
7
+ normalized[normalizedKey] = value;
8
+ });
9
+ Object.keys(row).forEach((key) => delete row[key]);
10
+ Object.assign(row, normalized);
11
+ });
12
+ }
13
+
14
+ export default {
15
+ transform
16
+ };
@@ -0,0 +1,21 @@
1
+ export const bindings = {
2
+ tablesToUpsert: { path: 'sql.tables.upsert' }
3
+ };
4
+ const NAME_COLUMNS = ['given_name', 'family_name'];
5
+
6
+ export async function transform({ batch, tablesToUpsert }) {
7
+ if (batch.length === 0) return;
8
+ batch.forEach((o) => {
9
+ const present = NAME_COLUMNS.filter((column) => Object.hasOwn(o, column));
10
+ if (present.length === 0) return;
11
+ if (!o.person_id) throw new Error('Cannot update name, no person_id');
12
+ tablesToUpsert.person = tablesToUpsert.person || [];
13
+ const row = { id: o.person_id };
14
+ for (const column of present) row[column] = o[column];
15
+ tablesToUpsert.person.push(row);
16
+ });
17
+ }
18
+ export default {
19
+ bindings,
20
+ transform
21
+ };
@@ -0,0 +1,32 @@
1
+ export const description = 'Append a name';
2
+ export const bindings = {
3
+ names: {
4
+ path: 'sql.query',
5
+ options: {
6
+ table: 'person',
7
+ columns: ['id', 'given_name', 'family_name'],
8
+ lookup: [
9
+ {
10
+ personIdField: 'person_id',
11
+ tableLookupField: 'id'
12
+ }
13
+ ]
14
+ }
15
+ }
16
+ };
17
+ export const transform = ({ batch, names }) => {
18
+ const nameMap = names.reduce((a, b) => {
19
+ a[b.id] = a[b.id] || b;
20
+ return a;
21
+ }, {});
22
+ batch.forEach((data) => {
23
+ const o = nameMap[data.person_id] || {};
24
+ data.given_name = o.given_name || null;
25
+ data.family_name = o.family_name || null;
26
+ });
27
+ };
28
+ export default {
29
+ description,
30
+ bindings,
31
+ transform
32
+ };
@@ -0,0 +1,34 @@
1
+ import handlebars from 'handlebars';
2
+
3
+ const moduleExports = {
4
+ async transform({ batch, options }) {
5
+ if (!options._compiledMap) {
6
+ if (!options?.map) {
7
+ throw new Error('Error with transform, no options.map provided');
8
+ }
9
+ options._compiledMap = {};
10
+ Object.entries(options.map).forEach(([k, source]) => {
11
+ if (k === '*') {
12
+ options._compiledMap['*'] = '*'; // just applies all the other fields
13
+ } else {
14
+ options._compiledMap[k] = handlebars.compile(source);
15
+ }
16
+ });
17
+ }
18
+ const newBatch = batch.map((o) => {
19
+ const out = {};
20
+ Object.entries(options._compiledMap).forEach(([k, func]) => {
21
+ if (k === '*') {
22
+ Object.assign(out, o);
23
+ } else if (out[k] === undefined) {
24
+ // first one wins, specify it first if you want ahead of the '*'
25
+ out[k] = func(o);
26
+ }
27
+ });
28
+ return out;
29
+ });
30
+ return { batch: newBatch };
31
+ }
32
+ };
33
+
34
+ export default moduleExports;