ga4-export-fixer 0.9.0-dev.15 → 0.9.0-dev.17

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/documentation.js CHANGED
@@ -173,8 +173,8 @@ const getColumnDescriptions = (config, columnMetadata) => {
173
173
  // Item-level enrichments are not yet supported and throw at SQL gen time — skip here.
174
174
  if (config && Array.isArray(config.enrichments) && config.enrichments.length > 0) {
175
175
  config.enrichments.forEach(e => {
176
- const level = e.level ?? 'event';
177
- if (level !== 'event') return;
176
+ const level = e.level ?? 'row';
177
+ if (level !== 'row') return;
178
178
  const joinKeys = Array.isArray(e.joinKey) ? e.joinKey : [e.joinKey];
179
179
  const joinKeyText = joinKeys.join(', ');
180
180
  const sourceText = renderEnrichmentSource(e.source);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ga4-export-fixer",
3
- "version": "0.9.0-dev.15",
3
+ "version": "0.9.0-dev.17",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -69,8 +69,9 @@ const ga4EventsEnhancedConfig = {
69
69
  // each entry is a queryBuilder step (raw {name, query} or structured {name, select, from, ...})
70
70
  customSteps: [],
71
71
  // declarative external-data enrichments joined into the pipeline
72
- // each entry: { name, level: 'event' | 'item', source, joinKey, columns, dedupe? }
73
- // 'item' level is accepted at config time but throws at SQL gen not yet implemented
72
+ // each entry: { name, source, joinKey, columns, level?, dedupe? }
73
+ // `level` is optional defaults to 'row' (one row of the enclosing table per join match).
74
+ // 'item' targets the items[] array (GA4-specific, ecommerce events only).
74
75
  enrichments: [],
75
76
  };
76
77
 
@@ -164,10 +164,10 @@ const _generateEnhancedEventsSQL = (mergedConfig) => {
164
164
  const itemListAttribution = mergedConfig.itemListAttribution;
165
165
 
166
166
  // Build enrichment-source CTEs and gather per-level join/column data. The utility routes
167
- // event-level and item-level entries through separate output channels. Done up here so the
167
+ // row-level and item-level entries through separate output channels. Done up here so the
168
168
  // items-scaffold activation state is known before building event_data (which needs
169
169
  // _item_row_id when the scaffold is active for any reason).
170
- const { steps: enrichmentSteps, event: eventEnrichments, item: itemEnrichments }
170
+ const { steps: enrichmentSteps, row: rowEnrichments, item: itemEnrichments }
171
171
  = utils.buildEnrichments(mergedConfig.enrichments);
172
172
  const itemEnrichmentsActive = itemEnrichments.joins.length > 0;
173
173
  const itemsScaffoldActive = !!itemListAttribution || itemEnrichmentsActive;
@@ -279,7 +279,7 @@ ${excludedEventsSQL}`,
279
279
  // items_unnested and need no extension.
280
280
  const itemJoinKeysFromEventData = new Set();
281
281
  for (const [i, e] of (mergedConfig.enrichments ?? []).entries()) {
282
- const level = e.level ?? 'event';
282
+ const level = e.level ?? 'row';
283
283
  if (level !== 'item') continue;
284
284
  const joinKeys = Array.isArray(e.joinKey) ? e.joinKey : [e.joinKey];
285
285
  for (const c of joinKeys) {
@@ -424,13 +424,13 @@ ${excludedEventsSQL}`,
424
424
  } : {};
425
425
  const itemListExcludedColumns = itemListSteps ? ['_item_row_id'] : [];
426
426
 
427
- // Wrap overlapping event-level enrichment columns in coalesce(enrich_<name>.<col>, <original>)
427
+ // Wrap overlapping row-level enrichment columns in coalesce(enrich_<name>.<col>, <original>)
428
428
  // so a missed JOIN falls back to the existing value. Purely additive columns (no overlap)
429
429
  // pass through unchanged. Source-of-original precedence matches the final SELECT's spread
430
430
  // order: itemListOverrides first (overrides finalColumnOrder for `items`), then
431
431
  // session_data (wins over event_data in getFinalColumnOrder when both have the column).
432
- const wrappedEventEnrichmentColumns = {};
433
- for (const [col, enrichExpr] of Object.entries(eventEnrichments.columns)) {
432
+ const wrappedRowEnrichmentColumns = {};
433
+ for (const [col, enrichExpr] of Object.entries(rowEnrichments.columns)) {
434
434
  let originalExpr;
435
435
  if (col in itemListOverrides) {
436
436
  originalExpr = itemListOverrides[col];
@@ -439,7 +439,7 @@ ${excludedEventsSQL}`,
439
439
  } else if (col in eventDataStep.select.columns && eventDataStep.select.columns[col] !== undefined) {
440
440
  originalExpr = `event_data.${col}`;
441
441
  }
442
- wrappedEventEnrichmentColumns[col] = originalExpr
442
+ wrappedRowEnrichmentColumns[col] = originalExpr
443
443
  ? `coalesce(${enrichExpr}, ${originalExpr})`
444
444
  : enrichExpr;
445
445
  }
@@ -449,7 +449,7 @@ ${excludedEventsSQL}`,
449
449
  const alreadyMapped = [
450
450
  ...Object.keys(finalColumnOrder),
451
451
  ...Object.keys(itemListOverrides),
452
- ...eventEnrichments.columnNames,
452
+ ...rowEnrichments.columnNames,
453
453
  'entrances',
454
454
  mergedConfig.sessionParams.length > 0 ? 'session_params_prep' : undefined,
455
455
  'data_is_final',
@@ -466,8 +466,8 @@ ${excludedEventsSQL}`,
466
466
  // get the most important columns in the correct order
467
467
  ...finalColumnOrder,
468
468
  ...itemListOverrides,
469
- // event-level enrichment columns: coalesce with the original when overlapping; otherwise add.
470
- ...wrappedEventEnrichmentColumns,
469
+ // row-level enrichment columns: coalesce with the original when overlapping; otherwise add.
470
+ ...wrappedRowEnrichmentColumns,
471
471
  // explicit pass-throughs for the rest of event_data and session_data
472
472
  ...utils.buildQualifiedPassThroughs(eventDataStep, alreadyMapped),
473
473
  ...utils.buildQualifiedPassThroughs(sessionDataStep, alreadyMapped),
@@ -489,8 +489,8 @@ ${excludedEventsSQL}`,
489
489
  table: 'session_data',
490
490
  on: 'using(session_id)'
491
491
  },
492
- // The left joins for the event-level enrichment ctes
493
- ...eventEnrichments.joins,
492
+ // The left joins for the row-level enrichment ctes
493
+ ...rowEnrichments.joins,
494
494
  ],
495
495
  where: helpers.incrementalDateFilter(mergedConfig)
496
496
  };
@@ -234,7 +234,7 @@ const validateEnhancedEventsConfig = (config, options = {}) => {
234
234
  if (!Array.isArray(config.enrichments)) {
235
235
  throw new Error(`config.enrichments must be an array. Received: ${JSON.stringify(config.enrichments)}`);
236
236
  }
237
- const validLevels = ['event', 'item'];
237
+ const validLevels = ['row', 'item'];
238
238
  const seenNames = new Set();
239
239
  for (let i = 0; i < config.enrichments.length; i++) {
240
240
  const entry = config.enrichments[i];
package/utils.js CHANGED
@@ -515,25 +515,27 @@ const buildPassThroughs = (explicitColumns, sourceColumns) => {
515
515
 
516
516
  /**
517
517
  * Builds the per-enrichment CTE definitions, JOIN clauses, and column-name mappings for the
518
- * declarative `enrichments` feature. Routes event-level and item-level entries through
518
+ * declarative `enrichments` feature. Routes row-level and item-level entries through
519
519
  * separate output channels so the caller can attach them to different downstream CTEs.
520
520
  *
521
521
  * Pure config-to-data mapping. No knowledge of downstream CTEs or specific table modules —
522
522
  * intended to be called by any table module that exposes an `enrichments` config field.
523
523
  *
524
524
  * Encapsulates one generation-time throw:
525
- * - Same-level enrichment-vs-enrichment column collisions (two event-level enrichments or
525
+ * - Same-level enrichment-vs-enrichment column collisions (two row-level enrichments or
526
526
  * two item-level enrichments targeting the same column). Cross-level same-name is allowed —
527
- * the two columns target structurally distinct slots (`enhanced_events.<col>` vs
527
+ * the two columns target structurally distinct slots (e.g. `enhanced_events.<col>` vs
528
528
  * `items[].<col>`).
529
529
  *
530
530
  * @param {Array<Object>} enrichments - Validated enrichment entries. Each entry has fields:
531
- * { name, level, source, joinKey, columns, dedupe? }. `level` is 'event' (default) or 'item'.
531
+ * { name, source, joinKey, columns, level?, dedupe? }. `level` is 'row' (default) or 'item'.
532
+ * 'row' means one row of the enclosing table per join match; 'item' targets a nested array
533
+ * (currently only the GA4 items[] array).
532
534
  * @returns {Object} A struct with four fields:
533
535
  * - `steps` — array of queryBuilder source-CTE step definitions (one `enrich_<name>` per
534
536
  * entry, regardless of level — all source CTEs go to the top of the pipeline).
535
- * - `event` — { joins, columns, columnNames } for event-level enrichments. Caller attaches
536
- * `joins` to the event-grained downstream CTE (e.g. `enhanced_events`) and spreads `columns`
537
+ * - `row` — { joins, columns, columnNames } for row-level enrichments. Caller attaches
538
+ * `joins` to the row-grained downstream CTE (e.g. `enhanced_events`) and spreads `columns`
537
539
  * into that CTE's `select.columns`.
538
540
  * - `item` — { joins, columns, columnNames } for item-level enrichments. Caller attaches
539
541
  * `joins` to the item-grained downstream CTE (e.g. `items_rebuilt`) and folds `columns`
@@ -545,20 +547,20 @@ const buildPassThroughs = (explicitColumns, sourceColumns) => {
545
547
  * enrichment names and the conflicting column in the error message).
546
548
  *
547
549
  * @example
548
- * const { steps, event, item } = buildEnrichments(config.enrichments);
549
- * // event.joins → attach to enhanced_events; event.columns → spread into enhanced_events
550
+ * const { steps, row, item } = buildEnrichments(config.enrichments);
551
+ * // row.joins → attach to enhanced_events; row.columns → spread into enhanced_events
550
552
  * // item.joins → attach to items_rebuilt; item.columns → fold into items struct
551
553
  */
552
554
  const buildEnrichments = (enrichments) => {
553
555
  const steps = [];
554
556
  const channels = {
555
- event: { joins: [], columns: {}, columnNames: new Set() },
557
+ row: { joins: [], columns: {}, columnNames: new Set() },
556
558
  item: { joins: [], columns: {}, columnNames: new Set() },
557
559
  };
558
560
  const columnOwner = {};
559
561
 
560
562
  for (const [i, e] of (enrichments ?? []).entries()) {
561
- const level = e.level ?? 'event';
563
+ const level = e.level ?? 'row';
562
564
  const channel = channels[level];
563
565
  const joinKeys = Array.isArray(e.joinKey) ? e.joinKey : [e.joinKey];
564
566
  const cteName = `enrich_${e.name}`;
@@ -598,7 +600,7 @@ const buildEnrichments = (enrichments) => {
598
600
  }
599
601
  }
600
602
 
601
- return { steps, event: channels.event, item: channels.item, columnOwner };
603
+ return { steps, row: channels.row, item: channels.item, columnOwner };
602
604
  };
603
605
 
604
606