Skip to content

Commit 1c69c38

Browse files
committed
Incremental update tweaks
1 parent 6486a94 commit 1c69c38

File tree

3 files changed

+526
-70
lines changed

3 files changed

+526
-70
lines changed

data-migration/src/config.js

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
427515
Object.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

Comments
 (0)