@@ -14,8 +14,81 @@ const parseMigrationMode = value => {
1414 return ALLOWED_MIGRATION_MODES . has ( normalized ) ? normalized : 'full' ;
1515} ;
1616
17- // Default configuration with fallbacks
18- module . exports = {
17+ const RECOMMENDED_INCREMENTAL_FIELDS = [ 'updatedAt' , 'updatedBy' ] ;
18+
19+ const collectKnownSchemaFields = migratorConfig => {
20+ const knownFields = new Set ( ) ;
21+ if ( ! migratorConfig ) {
22+ return knownFields ;
23+ }
24+
25+ Object . values ( migratorConfig ) . forEach ( definition => {
26+ if ( ! definition || typeof definition !== 'object' ) {
27+ return ;
28+ }
29+ ( definition . requiredFields || [ ] ) . forEach ( field => knownFields . add ( field ) ) ;
30+ ( definition . optionalFields || [ ] ) . forEach ( field => knownFields . add ( field ) ) ;
31+ const defaults = definition . hasDefaults || [ ] ;
32+ defaults . forEach ( field => knownFields . add ( field ) ) ;
33+ } ) ;
34+
35+ return knownFields ;
36+ } ;
37+
38+ const validateIncrementalFieldConfiguration = ( config ) => {
39+ const incrementalFields = Array . isArray ( config . INCREMENTAL_FIELDS ) ? config . INCREMENTAL_FIELDS : [ ] ;
40+ const isIncrementalMode = config . MIGRATION_MODE === 'incremental' ;
41+ const knownFields = collectKnownSchemaFields ( config . migrator ) ;
42+
43+ const result = {
44+ warnings : [ ] ,
45+ errors : [ ]
46+ } ;
47+
48+ if ( isIncrementalMode && incrementalFields . length === 0 ) {
49+ result . errors . push ( 'INCREMENTAL_FIELDS must be configured when running incremental migrations.' ) ;
50+ }
51+
52+ incrementalFields . forEach ( field => {
53+ if ( ! knownFields . has ( field ) ) {
54+ result . warnings . push ( `Incremental field "${ field } " is not present in any migrator schema and will be ignored.` ) ;
55+ }
56+ } ) ;
57+
58+ const recommendedMissing = RECOMMENDED_INCREMENTAL_FIELDS . filter ( field => incrementalFields . length > 0 && ! incrementalFields . includes ( field ) ) ;
59+ if ( recommendedMissing . length ) {
60+ result . warnings . push ( `Consider including ${ recommendedMissing . join ( ', ' ) } in INCREMENTAL_FIELDS to preserve audit columns during updates.` ) ;
61+ }
62+
63+ if ( incrementalFields . length ) {
64+ console . info ( `[config] Incremental fields configured: ${ incrementalFields . join ( ', ' ) } ` ) ;
65+ }
66+
67+ result . warnings . forEach ( message => console . warn ( `[config] ${ message } ` ) ) ;
68+ result . errors . forEach ( message => console . error ( `[config] ${ message } ` ) ) ;
69+
70+ return result ;
71+ } ;
72+
73+ /**
74+ * Migration configuration with environment-driven overrides.
75+ *
76+ * Environment variables:
77+ * - DATABASE_URL: Postgres connection string.
78+ * - DATA_DIRECTORY: Root directory where migration payloads are stored.
79+ * - MIGRATION_MODE: 'full' or 'incremental' (defaults to 'full').
80+ * - INCREMENTAL_SINCE_DATE: ISO-8601 date used to filter source records.
81+ * - INCREMENTAL_FIELDS: Comma-separated list of fields updated during incremental runs.
82+ * - INCREMENTAL_FIELDS examples:
83+ * INCREMENTAL_FIELDS=updatedAt,updatedBy,status
84+ * INCREMENTAL_FIELDS=updatedAt,updatedBy,status,currentPhaseNames
85+ * Fields not listed remain unchanged during incremental updates.
86+ * - INCREMENTAL_DATE_FIELDS: Ordered list of timestamp fields to evaluate for incremental filtering (default: updatedAt,updated).
87+ * - MISSING_DATE_FIELD_BEHAVIOR: Behaviour when timestamps are missing (skip, include, warn-and-skip, warn-and-include).
88+ * - INVALID_DATE_FIELD_BEHAVIOR: Behaviour when timestamps are invalid or suspicious (same options as above).
89+ * - CREATED_BY / UPDATED_BY: Attribution columns written during migration.
90+ */
91+ const config = {
1992 // Database connection
2093 DATABASE_URL : process . env . DATABASE_URL ,
2194
@@ -24,6 +97,9 @@ module.exports = {
2497 BATCH_SIZE : parseInt ( process . env . BATCH_SIZE || '100' , 10 ) ,
2598 CONCURRENCY_LIMIT : parseInt ( process . env . CONCURRENCY_LIMIT || '10' , 10 ) ,
2699 LOG_LEVEL : process . env . LOG_LEVEL || 'info' ,
100+ LOG_VERBOSITY : ( process . env . LOG_VERBOSITY || 'normal' ) . toLowerCase ( ) ,
101+ SUMMARY_LOG_LIMIT : parseInt ( process . env . SUMMARY_LOG_LIMIT || '5' , 10 ) ,
102+ COLLECT_UPSERT_STATS : process . env . COLLECT_UPSERT_STATS === 'true' ,
27103
28104 // Migration behavior
29105 SKIP_MISSING_REQUIRED : process . env . SKIP_MISSING_REQUIRED === 'true' ,
@@ -34,7 +110,15 @@ module.exports = {
34110 // Incremental migration settings
35111 MIGRATION_MODE : parseMigrationMode ( process . env . MIGRATION_MODE ) ,
36112 INCREMENTAL_SINCE_DATE : process . env . INCREMENTAL_SINCE_DATE || null ,
113+ /**
114+ * Fields that should be mutated when MIGRATION_MODE=incremental.
115+ * Only columns listed here are included in UPDATE operations; omitted columns retain their existing values.
116+ * Works in tandem with INCREMENTAL_SINCE_DATE to scope the incremental window.
117+ */
37118 INCREMENTAL_FIELDS : parseListEnv ( process . env . INCREMENTAL_FIELDS ) ,
119+ INCREMENTAL_DATE_FIELDS : parseListEnv ( process . env . INCREMENTAL_DATE_FIELDS ) || [ 'updatedAt' , 'updated' ] ,
120+ MISSING_DATE_FIELD_BEHAVIOR : ( process . env . MISSING_DATE_FIELD_BEHAVIOR || 'warn-and-skip' ) . toLowerCase ( ) ,
121+ INVALID_DATE_FIELD_BEHAVIOR : ( process . env . INVALID_DATE_FIELD_BEHAVIOR || 'warn-and-skip' ) . toLowerCase ( ) ,
38122
39123 // Migration attribution
40124 CREATED_BY : process . env . CREATED_BY || 'migration' ,
@@ -424,7 +508,16 @@ module.exports = {
424508 } ,
425509} ;
426510
511+ config . incrementalFieldValidation = validateIncrementalFieldConfiguration ( config ) ;
512+
513+ module . exports = config ;
514+
427515Object . defineProperty ( module . exports , 'parseMigrationMode' , {
428516 value : parseMigrationMode ,
429517 enumerable : false
430518} ) ;
519+
520+ Object . defineProperty ( module . exports , 'validateIncrementalFieldConfiguration' , {
521+ value : validateIncrementalFieldConfiguration ,
522+ enumerable : false
523+ } ) ;
0 commit comments