ga4-export-fixer 0.1.6-dev.0 → 0.1.6-dev.1

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/index.js CHANGED
@@ -1,26 +1,44 @@
1
1
  const helpers = require('./helpers.js');
2
2
  const ga4EventsEnhanced = require('./tables/ga4EventsEnhanced.js');
3
3
  const preOperations = require('./preOperations.js');
4
+ const { validateConfig } = require('./inputValidation.js');
4
5
 
5
6
  // export setPreOperations with default configuration for usage with downstream tables
6
- const setPreOperations = (self, incremental, preOperationsConfig) => {
7
- const defaultPreOperationsConfig = {
8
- dateRangeStartFullRefresh: 'date(2000, 1, 1)',
9
- dateRangeEnd: 'current_date()',
10
- numberOfPreviousDaysToScan: 10,
11
- };
12
-
13
- const config = {
14
- self,
15
- incremental,
16
- preOperations: {...defaultPreOperationsConfig, ...preOperationsConfig}
7
+ const setPreOperations = (config) => {
8
+ if (!config || !config.self) {
9
+ throw new Error('setPreOperations: config.self is required. Pass the table\'s "self()" reference in the config object.');
10
+ }
11
+ if (config.incremental === undefined || config.incremental === null) {
12
+ throw new Error('setPreOperations: config.incremental is required. Pass a boolean indicating whether the table uses incremental mode.');
13
+ }
14
+
15
+ /*
16
+ Todo: consider improving the validateConfig function to cover this use case as well
17
+ */
18
+
19
+ const defaultConfig = {
20
+ self: undefined,
21
+ incremental: undefined,
22
+ test: false,
23
+ testConfig: {
24
+ dateRangeStart: 'current_date()-1',
25
+ dateRangeEnd: 'current_date()',
26
+ },
27
+ preOperations: {
28
+ dateRangeStartFullRefresh: 'date(2000, 1, 1)',
29
+ dateRangeEnd: 'current_date()',
30
+ numberOfPreviousDaysToScan: 10,
31
+ },
17
32
  };
18
33
 
19
- return preOperations.setPreOperations(config);
34
+ const mergedConfig = utils.mergeSQLConfigurations(defaultConfig, config);
35
+
36
+ return preOperations.setPreOperations(mergedConfig);
20
37
  };
21
38
 
22
39
  module.exports = {
23
40
  helpers,
24
41
  ga4EventsEnhanced,
25
- setPreOperations
42
+ setPreOperations,
43
+ validateConfig
26
44
  };
@@ -0,0 +1,246 @@
1
+ const { isDataformTableReferenceObject } = require('./utils.js');
2
+
3
+ /**
4
+ * Validates a GA4 export fixer configuration object.
5
+ * Validation is performed on mergedConfig (default values merged with user input).
6
+ * All fields are required in the merged config; optional fields are only optional for user input
7
+ * and receive their values from the default configuration during merge.
8
+ *
9
+ * @param {Object} config - The merged configuration object to validate.
10
+ * @throws {Error} If any configuration value is invalid or missing.
11
+ */
12
+ const validateConfig = (config) => {
13
+ try {
14
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
15
+ throw new Error(`config must be a non-null object. Received: ${JSON.stringify(config)}`);
16
+ }
17
+
18
+ // sourceTable - required; string or Dataform table reference
19
+ if (config.sourceTable === undefined || config.sourceTable === null) {
20
+ throw new Error("config.sourceTable is required. Provide a Dataform table reference (using the ref() function) or a string in format '`project.dataset.table`'.");
21
+ }
22
+ if (isDataformTableReferenceObject(config.sourceTable)) {
23
+ // Valid Dataform reference
24
+ } else if (typeof config.sourceTable === 'string') {
25
+ if (!config.sourceTable.trim()) {
26
+ throw new Error("config.sourceTable must be a non-empty string. Received empty string.");
27
+ }
28
+ if (!/^`[^\.]+\.[^\.]+\.[^\.]+`$/.test(config.sourceTable.trim())) {
29
+ throw new Error(`config.sourceTable must be in the format '\`project.dataset.table\`' (with backticks). Received: ${JSON.stringify(config.sourceTable)}`);
30
+ }
31
+ } else {
32
+ throw new Error(`config.sourceTable must be a Dataform table reference object or a string in format '\`project.dataset.table\`'. Received: ${JSON.stringify(config.sourceTable)}`);
33
+ }
34
+
35
+ // self - required when using Dataform; must be valid format
36
+ // config.self is required when config.test === true and must be a non-empty string in format '\`project.dataset.table\`' (using the ref() function)
37
+ if (config.test !== true) {
38
+ if (typeof config.self !== 'string' || !config.self.trim() || !/^`[^`]+`$/.test(config.self.trim())) {
39
+ throw new Error(`config.self is required when config.test === true and must be a non-empty string in format '\`project.dataset.table\`' (using the ref() function). Received: ${JSON.stringify(config.self)}`);
40
+ }
41
+ }
42
+
43
+ // incremental - required when using Dataform; must be boolean
44
+ if (typeof config.incremental !== 'boolean') {
45
+ throw new Error(`config.incremental must be a boolean. Received: ${JSON.stringify(config.incremental)}`);
46
+ }
47
+
48
+ // schemaLock - optional; must be undefined or a string in "YYYYMMDD" format (e.g., "20260101")
49
+ if (typeof config.schemaLock !== 'undefined') {
50
+ if (typeof config.schemaLock !== 'string' || !/^\d{8}$/.test(config.schemaLock)) {
51
+ throw new Error(`config.schemaLock must be a string in "YYYYMMDD" format (e.g., "20260101"). Received: ${JSON.stringify(config.schemaLock)}`);
52
+ }
53
+ // Must be a valid date
54
+ const year = parseInt(config.schemaLock.slice(0, 4), 10);
55
+ const month = parseInt(config.schemaLock.slice(4, 6), 10);
56
+ const day = parseInt(config.schemaLock.slice(6, 8), 10);
57
+ const date = new Date(year, month - 1, day);
58
+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
59
+ throw new Error(`config.schemaLock must be a valid date. Received: ${JSON.stringify(config.schemaLock)}`);
60
+ }
61
+ // Must be at least 20241009
62
+ if (config.schemaLock < "20241009") {
63
+ throw new Error(`config.schemaLock must be a date string equal to or greater than "20241009". Received: ${JSON.stringify(config.schemaLock)}`);
64
+ }
65
+ }
66
+
67
+ // includedExportTypes - required
68
+ if (typeof config.includedExportTypes === 'undefined') {
69
+ throw new Error("config.includedExportTypes is required.");
70
+ }
71
+ if (!config.includedExportTypes || typeof config.includedExportTypes !== 'object' || Array.isArray(config.includedExportTypes)) {
72
+ throw new Error(`config.includedExportTypes must be an object. Received: ${JSON.stringify(config.includedExportTypes)}`);
73
+ }
74
+ for (const key of ['daily', 'intraday']) {
75
+ if (!(key in config.includedExportTypes)) {
76
+ throw new Error(`config.includedExportTypes.${key} is required.`);
77
+ }
78
+ if (typeof config.includedExportTypes[key] !== 'boolean') {
79
+ throw new Error(`config.includedExportTypes.${key} must be a boolean. Received: ${JSON.stringify(config.includedExportTypes[key])}`);
80
+ }
81
+ }
82
+ if (!config.includedExportTypes.daily && !config.includedExportTypes.intraday) {
83
+ throw new Error("At least one of config.includedExportTypes.daily or config.includedExportTypes.intraday must be true.");
84
+ }
85
+
86
+ // timezone - required
87
+ if (typeof config.timezone === 'undefined') {
88
+ throw new Error("config.timezone is required.");
89
+ }
90
+ if (typeof config.timezone !== 'string' || !config.timezone.trim()) {
91
+ throw new Error(`config.timezone must be a non-empty string (e.g. 'Etc/UTC', 'Europe/Helsinki'). Received: ${JSON.stringify(config.timezone)}`);
92
+ }
93
+
94
+ // customTimestampParam - optional; must be undefined or a non-empty string
95
+ if (typeof config.customTimestampParam !== 'undefined') {
96
+ if (typeof config.customTimestampParam !== 'string' || !config.customTimestampParam.trim()) {
97
+ throw new Error(`config.customTimestampParam must be a non-empty string when provided. Received: ${JSON.stringify(config.customTimestampParam)}`);
98
+ }
99
+ }
100
+
101
+ // dataIsFinal - required
102
+ if (typeof config.dataIsFinal === 'undefined') {
103
+ throw new Error("config.dataIsFinal is required.");
104
+ }
105
+ if (typeof config.dataIsFinal !== 'object' || Array.isArray(config.dataIsFinal)) {
106
+ throw new Error(`config.dataIsFinal must be an object. Received: ${JSON.stringify(config.dataIsFinal)}`);
107
+ }
108
+ if (typeof config.dataIsFinal.detectionMethod === 'undefined') {
109
+ throw new Error("config.dataIsFinal.detectionMethod is required.");
110
+ }
111
+ if (typeof config.dataIsFinal.detectionMethod !== 'string' || (config.dataIsFinal.detectionMethod !== 'EXPORT_TYPE' && config.dataIsFinal.detectionMethod !== 'DAY_THRESHOLD')) {
112
+ throw new Error(`config.dataIsFinal.detectionMethod must be 'EXPORT_TYPE' or 'DAY_THRESHOLD'. Received: ${JSON.stringify(config.dataIsFinal.detectionMethod)}`);
113
+ }
114
+ if (
115
+ config.dataIsFinal.detectionMethod === 'DAY_THRESHOLD' &&
116
+ typeof config.dataIsFinal.dayThreshold === 'undefined'
117
+ ) {
118
+ throw new Error("config.dataIsFinal.dayThreshold is required when detectionMethod is 'DAY_THRESHOLD'.");
119
+ }
120
+ if (
121
+ config.dataIsFinal.detectionMethod === 'DAY_THRESHOLD' &&
122
+ (typeof config.dataIsFinal.dayThreshold !== 'number' || !Number.isInteger(config.dataIsFinal.dayThreshold) || config.dataIsFinal.dayThreshold < 0)
123
+ ) {
124
+ throw new Error(`config.dataIsFinal.dayThreshold must be a non-negative integer. Received: ${JSON.stringify(config.dataIsFinal.dayThreshold)}`);
125
+ }
126
+ // EXPORT_TYPE detection relies on daily export metadata; intraday-only requires DAY_THRESHOLD instead.
127
+ if (
128
+ config.includedExportTypes.intraday &&
129
+ !config.includedExportTypes.daily &&
130
+ config.dataIsFinal.detectionMethod !== 'DAY_THRESHOLD'
131
+ ) {
132
+ throw new Error(`config.dataIsFinal.detectionMethod must be 'DAY_THRESHOLD' when only intraday export is enabled (config.includedExportTypes.daily is false). A dayThreshold of 1 is recommended for intraday-only configurations. Received: ${JSON.stringify(config.dataIsFinal.detectionMethod)}`);
133
+ }
134
+
135
+ // test - optional; when defined, must be a boolean
136
+ if (typeof config.test !== 'undefined' && typeof config.test !== 'boolean') {
137
+ throw new Error(`config.test must be a boolean when defined. Received: ${JSON.stringify(config.test)}`);
138
+ }
139
+
140
+ // testConfig - optional; when included, must be an object with optional dateRangeStart and dateRangeEnd
141
+ if (typeof config.testConfig !== 'undefined') {
142
+ if (!config.testConfig || typeof config.testConfig !== 'object' || Array.isArray(config.testConfig)) {
143
+ throw new Error(`config.testConfig must be an object when included. Received: ${JSON.stringify(config.testConfig)}`);
144
+ }
145
+ if (config.testConfig.dateRangeStart !== undefined && (typeof config.testConfig.dateRangeStart !== 'string' || !config.testConfig.dateRangeStart.trim())) {
146
+ throw new Error(`config.testConfig.dateRangeStart must be a non-empty string (SQL date expression) when provided. Received: ${JSON.stringify(config.testConfig.dateRangeStart)}`);
147
+ }
148
+ if (config.testConfig.dateRangeEnd !== undefined && (typeof config.testConfig.dateRangeEnd !== 'string' || !config.testConfig.dateRangeEnd.trim())) {
149
+ throw new Error(`config.testConfig.dateRangeEnd must be a non-empty string (SQL date expression) when provided. Received: ${JSON.stringify(config.testConfig.dateRangeEnd)}`);
150
+ }
151
+ }
152
+
153
+ // bufferDays - required
154
+ if (typeof config.bufferDays !== 'number' || !Number.isInteger(config.bufferDays) || config.bufferDays < 0) {
155
+ throw new Error(`config.bufferDays must be a non-negative integer. Received: ${JSON.stringify(config.bufferDays)}`);
156
+ }
157
+
158
+ // preOperations - required
159
+ if (config.preOperations === undefined) {
160
+ throw new Error("config.preOperations is required.");
161
+ }
162
+ if (!config.preOperations || typeof config.preOperations !== 'object' || Array.isArray(config.preOperations)) {
163
+ throw new Error(`config.preOperations must be an object. Received: ${JSON.stringify(config.preOperations)}`);
164
+ }
165
+ if (config.preOperations.numberOfPreviousDaysToScan === undefined) {
166
+ throw new Error("config.preOperations.numberOfPreviousDaysToScan is required.");
167
+ }
168
+ const v = config.preOperations.numberOfPreviousDaysToScan;
169
+ if (typeof v !== 'number' || isNaN(v) || !Number.isInteger(v) || v < 0) {
170
+ throw new Error(`config.preOperations.numberOfPreviousDaysToScan must be a non-negative integer. Received: ${JSON.stringify(v)}`);
171
+ }
172
+ if (config.preOperations.dateRangeStartFullRefresh === undefined || config.preOperations.dateRangeStartFullRefresh === null) {
173
+ throw new Error("config.preOperations.dateRangeStartFullRefresh is required.");
174
+ }
175
+ if (typeof config.preOperations.dateRangeStartFullRefresh !== 'string' || !config.preOperations.dateRangeStartFullRefresh.trim()) {
176
+ throw new Error(`config.preOperations.dateRangeStartFullRefresh must be a non-empty string (SQL date expression). Received: ${JSON.stringify(config.preOperations.dateRangeStartFullRefresh)}`);
177
+ }
178
+ if (config.preOperations.dateRangeEnd === undefined || config.preOperations.dateRangeEnd === null) {
179
+ throw new Error("config.preOperations.dateRangeEnd is required.");
180
+ }
181
+ if (typeof config.preOperations.dateRangeEnd !== 'string' || !config.preOperations.dateRangeEnd.trim()) {
182
+ throw new Error(`config.preOperations.dateRangeEnd must be a non-empty string (SQL date expression). Received: ${JSON.stringify(config.preOperations.dateRangeEnd)}`);
183
+ }
184
+ if (config.preOperations.incrementalStartOverride !== undefined && config.preOperations.incrementalStartOverride !== null && config.preOperations.incrementalStartOverride !== '') {
185
+ if (typeof config.preOperations.incrementalStartOverride !== 'string' || !config.preOperations.incrementalStartOverride.trim()) {
186
+ throw new Error(`config.preOperations.incrementalStartOverride must be a non-empty string when provided. Received: ${JSON.stringify(config.preOperations.incrementalStartOverride)}`);
187
+ }
188
+ }
189
+ if (config.preOperations.incrementalEndOverride !== undefined && config.preOperations.incrementalEndOverride !== null && config.preOperations.incrementalEndOverride !== '') {
190
+ if (typeof config.preOperations.incrementalEndOverride !== 'string' || !config.preOperations.incrementalEndOverride.trim()) {
191
+ throw new Error(`config.preOperations.incrementalEndOverride must be a non-empty string when provided. Received: ${JSON.stringify(config.preOperations.incrementalEndOverride)}`);
192
+ }
193
+ }
194
+
195
+ // Array fields - all required
196
+ const stringArrayKeys = ['defaultExcludedEventParams', 'excludedEventParams', 'sessionParams', 'defaultExcludedEvents', 'excludedEvents', 'excludedColumns'];
197
+ for (const key of stringArrayKeys) {
198
+ if (config[key] === undefined) {
199
+ throw new Error(`config.${key} is required.`);
200
+ }
201
+ if (!Array.isArray(config[key])) {
202
+ throw new Error(`config.${key} must be an array. Received: ${JSON.stringify(config[key])}`);
203
+ }
204
+ for (let i = 0; i < config[key].length; i++) {
205
+ if (typeof config[key][i] !== 'string' || !config[key][i].trim()) {
206
+ throw new Error(`config.${key}[${i}] must be a non-empty string. Received: ${JSON.stringify(config[key][i])}`);
207
+ }
208
+ }
209
+ }
210
+
211
+ // eventParamsToColumns - required
212
+ if (config.eventParamsToColumns === undefined) {
213
+ throw new Error("config.eventParamsToColumns is required.");
214
+ }
215
+ if (!Array.isArray(config.eventParamsToColumns)) {
216
+ throw new Error(`config.eventParamsToColumns must be an array. Received: ${JSON.stringify(config.eventParamsToColumns)}`);
217
+ }
218
+ const validEventParamTypes = ['string', 'int', 'int64', 'double', 'float', 'float64'];
219
+ for (let i = 0; i < config.eventParamsToColumns.length; i++) {
220
+ const item = config.eventParamsToColumns[i];
221
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
222
+ throw new Error(`config.eventParamsToColumns[${i}] must be an object with 'name' and 'type' properties. Received: ${JSON.stringify(item)}`);
223
+ }
224
+ if (!item.name || typeof item.name !== 'string' || !item.name.trim()) {
225
+ throw new Error(`config.eventParamsToColumns[${i}].name must be a non-empty string. Received: ${JSON.stringify(item.name)}`);
226
+ }
227
+ if (item.type !== undefined && item.type !== null) {
228
+ if (!validEventParamTypes.includes(item.type)) {
229
+ throw new Error(`config.eventParamsToColumns[${i}].type must be one of: ${validEventParamTypes.join(', ')}. Received: ${JSON.stringify(item.type)}`);
230
+ }
231
+ }
232
+ if (item.columnName !== undefined && item.columnName !== null && item.columnName !== '') {
233
+ if (typeof item.columnName !== 'string' || !item.columnName.trim()) {
234
+ throw new Error(`config.eventParamsToColumns[${i}].columnName must be a non-empty string when provided. Received: ${JSON.stringify(item.columnName)}`);
235
+ }
236
+ }
237
+ }
238
+ } catch (e) {
239
+ e.message = `Config validation: ${e.message}`;
240
+ throw e;
241
+ }
242
+ };
243
+
244
+ module.exports = {
245
+ validateConfig
246
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ga4-export-fixer",
3
- "version": "0.1.6-dev.0",
3
+ "version": "0.1.6-dev.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -9,7 +9,8 @@
9
9
  "utils.js",
10
10
  "preOperations.js",
11
11
  "constants.js",
12
- "tables"
12
+ "tables",
13
+ "inputValidation.js"
13
14
  ],
14
15
  "scripts": {
15
16
  "test": "node tests/ga4EventsEnhanced.test.js",
@@ -1,5 +1,6 @@
1
1
  const helpers = require('../helpers.js');
2
2
  const utils = require('../utils.js');
3
+ const inputValidation = require('../inputValidation.js');
3
4
  const constants = require('../constants.js');
4
5
  const preOperations = require('../preOperations.js');
5
6
 
@@ -207,7 +208,7 @@ const generateEnhancedEventsSQL = (config) => {
207
208
  const mergedConfig = utils.mergeSQLConfigurations(defaultConfig, config);
208
209
 
209
210
  // validate the config and throw an error if it's invalid
210
- utils.validateConfig(mergedConfig);
211
+ inputValidation.validateConfig(mergedConfig);
211
212
 
212
213
  if (!mergedConfig.sourceTable || typeof mergedConfig.sourceTable !== 'string' || mergedConfig.sourceTable.trim() === '') {
213
214
  throw new Error("generateEnhancedEventsSQL: 'sourceTable' is a required parameter in config and must be a non-empty string.");
package/utils.js CHANGED
@@ -404,247 +404,6 @@ const processDate = (dateInput) => {
404
404
  throw new Error(`processDate: Unsupported date input format: ${JSON.stringify(dateInput)}. Expected formats are: YYYYMMDD, YYYY-MM-DD, or BigQuery SQL statement.`);
405
405
  };
406
406
 
407
- /**
408
- * Validates a GA4 export fixer configuration object.
409
- * Validation is performed on mergedConfig (default values merged with user input).
410
- * All fields are required in the merged config; optional fields are only optional for user input
411
- * and receive their values from the default configuration during merge.
412
- *
413
- * @param {Object} config - The merged configuration object to validate.
414
- * @throws {Error} If any configuration value is invalid or missing.
415
- */
416
- const validateConfig = (config) => {
417
- try {
418
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
419
- throw new Error(`config must be a non-null object. Received: ${JSON.stringify(config)}`);
420
- }
421
-
422
- // sourceTable - required; string or Dataform table reference
423
- if (config.sourceTable === undefined || config.sourceTable === null) {
424
- throw new Error("config.sourceTable is required. Provide a Dataform table reference (using the ref() function) or a string in format '`project.dataset.table`'.");
425
- }
426
- if (isDataformTableReferenceObject(config.sourceTable)) {
427
- // Valid Dataform reference
428
- } else if (typeof config.sourceTable === 'string') {
429
- if (!config.sourceTable.trim()) {
430
- throw new Error("config.sourceTable must be a non-empty string. Received empty string.");
431
- }
432
- if (!/^`[^\.]+\.[^\.]+\.[^\.]+`$/.test(config.sourceTable.trim())) {
433
- throw new Error(`config.sourceTable must be in the format '\`project.dataset.table\`' (with backticks). Received: ${JSON.stringify(config.sourceTable)}`);
434
- }
435
- } else {
436
- throw new Error(`config.sourceTable must be a Dataform table reference object or a string in format '\`project.dataset.table\`'. Received: ${JSON.stringify(config.sourceTable)}`);
437
- }
438
-
439
- // self - required when using Dataform; must be valid format
440
- // config.self is required when config.test === true and must be a non-empty string in format '\`project.dataset.table\`' (using the ref() function)
441
- if (config.test !== true) {
442
- if (typeof config.self !== 'string' || !config.self.trim() || !/^`[^`]+`$/.test(config.self.trim())) {
443
- throw new Error(`config.self is required when config.test === true and must be a non-empty string in format '\`project.dataset.table\`' (using the ref() function). Received: ${JSON.stringify(config.self)}`);
444
- }
445
- }
446
-
447
- // incremental - required when using Dataform; must be boolean
448
- if (typeof config.incremental !== 'boolean') {
449
- throw new Error(`config.incremental must be a boolean. Received: ${JSON.stringify(config.incremental)}`);
450
- }
451
-
452
- // schemaLock - optional; must be undefined or a string in "YYYYMMDD" format (e.g., "20260101")
453
- if (typeof config.schemaLock !== 'undefined') {
454
- if (typeof config.schemaLock !== 'string' || !/^\d{8}$/.test(config.schemaLock)) {
455
- throw new Error(`config.schemaLock must be a string in "YYYYMMDD" format (e.g., "20260101"). Received: ${JSON.stringify(config.schemaLock)}`);
456
- }
457
- // Must be a valid date
458
- const year = parseInt(config.schemaLock.slice(0, 4), 10);
459
- const month = parseInt(config.schemaLock.slice(4, 6), 10);
460
- const day = parseInt(config.schemaLock.slice(6, 8), 10);
461
- const date = new Date(year, month - 1, day);
462
- if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
463
- throw new Error(`config.schemaLock must be a valid date. Received: ${JSON.stringify(config.schemaLock)}`);
464
- }
465
- // Must be at least 20241009
466
- if (config.schemaLock < "20241009") {
467
- throw new Error(`config.schemaLock must be a date string equal to or greater than "20241009". Received: ${JSON.stringify(config.schemaLock)}`);
468
- }
469
- }
470
-
471
- // includedExportTypes - required
472
- if (typeof config.includedExportTypes === 'undefined') {
473
- throw new Error("config.includedExportTypes is required.");
474
- }
475
- if (!config.includedExportTypes || typeof config.includedExportTypes !== 'object' || Array.isArray(config.includedExportTypes)) {
476
- throw new Error(`config.includedExportTypes must be an object. Received: ${JSON.stringify(config.includedExportTypes)}`);
477
- }
478
- for (const key of ['daily', 'intraday']) {
479
- if (!(key in config.includedExportTypes)) {
480
- throw new Error(`config.includedExportTypes.${key} is required.`);
481
- }
482
- if (typeof config.includedExportTypes[key] !== 'boolean') {
483
- throw new Error(`config.includedExportTypes.${key} must be a boolean. Received: ${JSON.stringify(config.includedExportTypes[key])}`);
484
- }
485
- }
486
- if (!config.includedExportTypes.daily && !config.includedExportTypes.intraday) {
487
- throw new Error("At least one of config.includedExportTypes.daily or config.includedExportTypes.intraday must be true.");
488
- }
489
-
490
- // timezone - required
491
- if (typeof config.timezone === 'undefined') {
492
- throw new Error("config.timezone is required.");
493
- }
494
- if (typeof config.timezone !== 'string' || !config.timezone.trim()) {
495
- throw new Error(`config.timezone must be a non-empty string (e.g. 'Etc/UTC', 'Europe/Helsinki'). Received: ${JSON.stringify(config.timezone)}`);
496
- }
497
-
498
- // customTimestampParam - optional; must be undefined or a non-empty string
499
- if (typeof config.customTimestampParam !== 'undefined') {
500
- if (typeof config.customTimestampParam !== 'string' || !config.customTimestampParam.trim()) {
501
- throw new Error(`config.customTimestampParam must be a non-empty string when provided. Received: ${JSON.stringify(config.customTimestampParam)}`);
502
- }
503
- }
504
-
505
- // dataIsFinal - required
506
- if (typeof config.dataIsFinal === 'undefined') {
507
- throw new Error("config.dataIsFinal is required.");
508
- }
509
- if (typeof config.dataIsFinal !== 'object' || Array.isArray(config.dataIsFinal)) {
510
- throw new Error(`config.dataIsFinal must be an object. Received: ${JSON.stringify(config.dataIsFinal)}`);
511
- }
512
- if (typeof config.dataIsFinal.detectionMethod === 'undefined') {
513
- throw new Error("config.dataIsFinal.detectionMethod is required.");
514
- }
515
- if (typeof config.dataIsFinal.detectionMethod !== 'string' || (config.dataIsFinal.detectionMethod !== 'EXPORT_TYPE' && config.dataIsFinal.detectionMethod !== 'DAY_THRESHOLD')) {
516
- throw new Error(`config.dataIsFinal.detectionMethod must be 'EXPORT_TYPE' or 'DAY_THRESHOLD'. Received: ${JSON.stringify(config.dataIsFinal.detectionMethod)}`);
517
- }
518
- if (
519
- config.dataIsFinal.detectionMethod === 'DAY_THRESHOLD' &&
520
- typeof config.dataIsFinal.dayThreshold === 'undefined'
521
- ) {
522
- throw new Error("config.dataIsFinal.dayThreshold is required when detectionMethod is 'DAY_THRESHOLD'.");
523
- }
524
- if (
525
- config.dataIsFinal.detectionMethod === 'DAY_THRESHOLD' &&
526
- (typeof config.dataIsFinal.dayThreshold !== 'number' || !Number.isInteger(config.dataIsFinal.dayThreshold) || config.dataIsFinal.dayThreshold < 0)
527
- ) {
528
- throw new Error(`config.dataIsFinal.dayThreshold must be a non-negative integer. Received: ${JSON.stringify(config.dataIsFinal.dayThreshold)}`);
529
- }
530
- // EXPORT_TYPE detection relies on daily export metadata; intraday-only requires DAY_THRESHOLD instead.
531
- if (
532
- config.includedExportTypes.intraday &&
533
- !config.includedExportTypes.daily &&
534
- config.dataIsFinal.detectionMethod !== 'DAY_THRESHOLD'
535
- ) {
536
- throw new Error(`config.dataIsFinal.detectionMethod must be 'DAY_THRESHOLD' when only intraday export is enabled (config.includedExportTypes.daily is false). A dayThreshold of 1 is recommended for intraday-only configurations. Received: ${JSON.stringify(config.dataIsFinal.detectionMethod)}`);
537
- }
538
-
539
- // test - optional; when defined, must be a boolean
540
- if (typeof config.test !== 'undefined' && typeof config.test !== 'boolean') {
541
- throw new Error(`config.test must be a boolean when defined. Received: ${JSON.stringify(config.test)}`);
542
- }
543
-
544
- // testConfig - optional; when included, must be an object with optional dateRangeStart and dateRangeEnd
545
- if (typeof config.testConfig !== 'undefined') {
546
- if (!config.testConfig || typeof config.testConfig !== 'object' || Array.isArray(config.testConfig)) {
547
- throw new Error(`config.testConfig must be an object when included. Received: ${JSON.stringify(config.testConfig)}`);
548
- }
549
- if (config.testConfig.dateRangeStart !== undefined && (typeof config.testConfig.dateRangeStart !== 'string' || !config.testConfig.dateRangeStart.trim())) {
550
- throw new Error(`config.testConfig.dateRangeStart must be a non-empty string (SQL date expression) when provided. Received: ${JSON.stringify(config.testConfig.dateRangeStart)}`);
551
- }
552
- if (config.testConfig.dateRangeEnd !== undefined && (typeof config.testConfig.dateRangeEnd !== 'string' || !config.testConfig.dateRangeEnd.trim())) {
553
- throw new Error(`config.testConfig.dateRangeEnd must be a non-empty string (SQL date expression) when provided. Received: ${JSON.stringify(config.testConfig.dateRangeEnd)}`);
554
- }
555
- }
556
-
557
- // bufferDays - required
558
- if (typeof config.bufferDays !== 'number' || !Number.isInteger(config.bufferDays) || config.bufferDays < 0) {
559
- throw new Error(`config.bufferDays must be a non-negative integer. Received: ${JSON.stringify(config.bufferDays)}`);
560
- }
561
-
562
- // preOperations - required
563
- if (config.preOperations === undefined) {
564
- throw new Error("config.preOperations is required.");
565
- }
566
- if (!config.preOperations || typeof config.preOperations !== 'object' || Array.isArray(config.preOperations)) {
567
- throw new Error(`config.preOperations must be an object. Received: ${JSON.stringify(config.preOperations)}`);
568
- }
569
- if (config.preOperations.numberOfPreviousDaysToScan === undefined) {
570
- throw new Error("config.preOperations.numberOfPreviousDaysToScan is required.");
571
- }
572
- const v = config.preOperations.numberOfPreviousDaysToScan;
573
- if (typeof v !== 'number' || isNaN(v) || !Number.isInteger(v) || v < 0) {
574
- throw new Error(`config.preOperations.numberOfPreviousDaysToScan must be a non-negative integer. Received: ${JSON.stringify(v)}`);
575
- }
576
- if (config.preOperations.dateRangeStartFullRefresh === undefined || config.preOperations.dateRangeStartFullRefresh === null) {
577
- throw new Error("config.preOperations.dateRangeStartFullRefresh is required.");
578
- }
579
- if (typeof config.preOperations.dateRangeStartFullRefresh !== 'string' || !config.preOperations.dateRangeStartFullRefresh.trim()) {
580
- throw new Error(`config.preOperations.dateRangeStartFullRefresh must be a non-empty string (SQL date expression). Received: ${JSON.stringify(config.preOperations.dateRangeStartFullRefresh)}`);
581
- }
582
- if (config.preOperations.dateRangeEnd === undefined || config.preOperations.dateRangeEnd === null) {
583
- throw new Error("config.preOperations.dateRangeEnd is required.");
584
- }
585
- if (typeof config.preOperations.dateRangeEnd !== 'string' || !config.preOperations.dateRangeEnd.trim()) {
586
- throw new Error(`config.preOperations.dateRangeEnd must be a non-empty string (SQL date expression). Received: ${JSON.stringify(config.preOperations.dateRangeEnd)}`);
587
- }
588
- if (config.preOperations.incrementalStartOverride !== undefined && config.preOperations.incrementalStartOverride !== null && config.preOperations.incrementalStartOverride !== '') {
589
- if (typeof config.preOperations.incrementalStartOverride !== 'string' || !config.preOperations.incrementalStartOverride.trim()) {
590
- throw new Error(`config.preOperations.incrementalStartOverride must be a non-empty string when provided. Received: ${JSON.stringify(config.preOperations.incrementalStartOverride)}`);
591
- }
592
- }
593
- if (config.preOperations.incrementalEndOverride !== undefined && config.preOperations.incrementalEndOverride !== null && config.preOperations.incrementalEndOverride !== '') {
594
- if (typeof config.preOperations.incrementalEndOverride !== 'string' || !config.preOperations.incrementalEndOverride.trim()) {
595
- throw new Error(`config.preOperations.incrementalEndOverride must be a non-empty string when provided. Received: ${JSON.stringify(config.preOperations.incrementalEndOverride)}`);
596
- }
597
- }
598
-
599
- // Array fields - all required
600
- const stringArrayKeys = ['defaultExcludedEventParams', 'excludedEventParams', 'sessionParams', 'defaultExcludedEvents', 'excludedEvents', 'excludedColumns'];
601
- for (const key of stringArrayKeys) {
602
- if (config[key] === undefined) {
603
- throw new Error(`config.${key} is required.`);
604
- }
605
- if (!Array.isArray(config[key])) {
606
- throw new Error(`config.${key} must be an array. Received: ${JSON.stringify(config[key])}`);
607
- }
608
- for (let i = 0; i < config[key].length; i++) {
609
- if (typeof config[key][i] !== 'string' || !config[key][i].trim()) {
610
- throw new Error(`config.${key}[${i}] must be a non-empty string. Received: ${JSON.stringify(config[key][i])}`);
611
- }
612
- }
613
- }
614
-
615
- // eventParamsToColumns - required
616
- if (config.eventParamsToColumns === undefined) {
617
- throw new Error("config.eventParamsToColumns is required.");
618
- }
619
- if (!Array.isArray(config.eventParamsToColumns)) {
620
- throw new Error(`config.eventParamsToColumns must be an array. Received: ${JSON.stringify(config.eventParamsToColumns)}`);
621
- }
622
- const validEventParamTypes = ['string', 'int', 'int64', 'double', 'float', 'float64'];
623
- for (let i = 0; i < config.eventParamsToColumns.length; i++) {
624
- const item = config.eventParamsToColumns[i];
625
- if (!item || typeof item !== 'object' || Array.isArray(item)) {
626
- throw new Error(`config.eventParamsToColumns[${i}] must be an object with 'name' and 'type' properties. Received: ${JSON.stringify(item)}`);
627
- }
628
- if (!item.name || typeof item.name !== 'string' || !item.name.trim()) {
629
- throw new Error(`config.eventParamsToColumns[${i}].name must be a non-empty string. Received: ${JSON.stringify(item.name)}`);
630
- }
631
- if (item.type !== undefined && item.type !== null) {
632
- if (!validEventParamTypes.includes(item.type)) {
633
- throw new Error(`config.eventParamsToColumns[${i}].type must be one of: ${validEventParamTypes.join(', ')}. Received: ${JSON.stringify(item.type)}`);
634
- }
635
- }
636
- if (item.columnName !== undefined && item.columnName !== null && item.columnName !== '') {
637
- if (typeof item.columnName !== 'string' || !item.columnName.trim()) {
638
- throw new Error(`config.eventParamsToColumns[${i}].columnName must be a non-empty string when provided. Received: ${JSON.stringify(item.columnName)}`);
639
- }
640
- }
641
- }
642
- } catch (e) {
643
- e.message = `Config validation: ${e.message}`;
644
- throw e;
645
- }
646
- };
647
-
648
407
  module.exports = {
649
408
  mergeUniqueArrays,
650
409
  mergeSQLConfigurations,
@@ -653,6 +412,5 @@ module.exports = {
653
412
  isDataformTableReferenceObject,
654
413
  setDataformContext,
655
414
  selectOtherColumns,
656
- processDate,
657
- validateConfig
415
+ processDate
658
416
  };