Skip to content

Commit ee8a532

Browse files
authored
Add migration 183 to fix property name mismatches in templates and entities (#8839)
This migration corrects property names in templates and entity metadata that do not match their labels according to the generateNewSafeName algorithm. Only runs when newNameGeneration setting is enabled. - Analyzes template properties to detect name/label mismatches - Updates template property names using safeName generation rules - Handles special type suffixes for geolocation and nested properties - Uses bulk MongoDB operations to efficiently rename entity metadata keys - Skips commonProperties (system-defined properties) - Triggers reindex when changes are made - Includes comprehensive test coverage with 15 test cases
1 parent ee711dd commit ee8a532

File tree

4 files changed

+670
-0
lines changed

4 files changed

+670
-0
lines changed
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/* eslint-disable no-await-in-loop, import/no-default-export */
2+
import { Db, ObjectId } from 'mongodb';
3+
import { PropertyChange, Template, Property, Settings } from './types';
4+
5+
// Safe name generation function - matches the new generation algorithm
6+
// From: app/api/core/domain/template/utils/propertyNameGeneration.ts
7+
const generateNewSafeName = (label: string): string => {
8+
if (!label || typeof label !== 'string') {
9+
return '';
10+
}
11+
12+
return label
13+
.trim()
14+
.replace(/[# \\ / | * ? " < > = \s : . [ \] % ! \- & ^ + ( ) { } [ \] ~]/gi, '_')
15+
.replace(/^[ _ \- + $]/, '')
16+
.toLowerCase();
17+
};
18+
19+
export default {
20+
delta: 183,
21+
22+
name: 'fix_property_name_mismatches',
23+
24+
description:
25+
'Fix property names in templates and entity metadata that do not match their labels using new safeName generation',
26+
27+
reindex: false,
28+
29+
generateExpectedName(property: Property): string {
30+
const safeName = generateNewSafeName(property.label);
31+
32+
if (property.type === 'geolocation' || property.type === 'nested') {
33+
return `${safeName}_${property.type}`;
34+
}
35+
36+
return safeName;
37+
},
38+
39+
analyzeTemplate(template: Template): PropertyChange[] {
40+
const changes: PropertyChange[] = [];
41+
42+
if (template.properties) {
43+
template.properties.forEach(property => {
44+
if (property.label && property.name) {
45+
const expectedName = this.generateExpectedName(property);
46+
47+
if (property.name !== expectedName) {
48+
changes.push({
49+
oldName: property.name,
50+
newName: expectedName,
51+
label: property.label,
52+
type: property.type,
53+
});
54+
}
55+
}
56+
});
57+
}
58+
59+
return changes;
60+
},
61+
62+
async updateTemplate(db: Db, templateId: ObjectId, changes: PropertyChange[]): Promise<void> {
63+
const template = await db.collection<Template>('templates').findOne({ _id: templateId });
64+
65+
if (!template) {
66+
return;
67+
}
68+
69+
const changeMap = new Map(changes.map(c => [c.oldName, c.newName]));
70+
71+
const updatedProperties = template.properties?.map(prop => {
72+
const newName = changeMap.get(prop.name);
73+
return newName ? { ...prop, name: newName } : prop;
74+
});
75+
76+
await db.collection('templates').updateOne(
77+
{ _id: templateId },
78+
{
79+
$set: {
80+
properties: updatedProperties,
81+
},
82+
}
83+
);
84+
},
85+
86+
async updateEntitiesForTemplate(
87+
db: Db,
88+
templateId: ObjectId,
89+
changes: PropertyChange[]
90+
): Promise<number> {
91+
const renameOps: Record<string, string> = {};
92+
93+
changes.forEach(change => {
94+
renameOps[`metadata.${change.oldName}`] = `metadata.${change.newName}`;
95+
});
96+
97+
const result = await db
98+
.collection('entities')
99+
.updateMany({ template: templateId }, { $rename: renameOps });
100+
101+
if (result.modifiedCount > 0) {
102+
process.stdout.write(` Updated ${result.modifiedCount} entities\n`);
103+
}
104+
105+
return result.modifiedCount;
106+
},
107+
108+
async up(db: Db): Promise<boolean> {
109+
process.stdout.write(`${this.name}...\r\n`);
110+
111+
process.stdout.write('Checking newNameGeneration setting...\n');
112+
const settings = await db.collection<Settings>('settings').findOne({});
113+
if (!settings?.newNameGeneration) {
114+
process.stdout.write('Skipping: newNameGeneration not enabled\n');
115+
this.reindex = false;
116+
return this.reindex;
117+
}
118+
119+
const templates = await db.collection<Template>('templates').find({}).toArray();
120+
const propertyNameChanges = new Map<string, PropertyChange[]>();
121+
122+
for (const template of templates) {
123+
const changes = this.analyzeTemplate(template);
124+
if (changes.length > 0) {
125+
propertyNameChanges.set(template._id.toString(), changes);
126+
}
127+
}
128+
129+
if (propertyNameChanges.size === 0) {
130+
process.stdout.write('No property name mismatches found.\n');
131+
this.reindex = false;
132+
return this.reindex;
133+
}
134+
135+
// Update templates using bulkWrite
136+
process.stdout.write('\nUpdating templates...\n');
137+
const templateMap = new Map(templates.map(t => [t._id.toString(), t]));
138+
const bulkOps = Array.from(propertyNameChanges.entries())
139+
.map(([templateId, changes]) => {
140+
const template = templateMap.get(templateId);
141+
if (!template) {
142+
return null;
143+
}
144+
145+
const changeMap = new Map(changes.map(c => [c.oldName, c.newName]));
146+
const updatedProperties = template.properties?.map(prop => {
147+
const newName = changeMap.get(prop.name);
148+
return newName ? { ...prop, name: newName } : prop;
149+
});
150+
151+
return {
152+
updateOne: {
153+
filter: { _id: new ObjectId(templateId) },
154+
update: {
155+
$set: {
156+
properties: updatedProperties,
157+
},
158+
},
159+
},
160+
};
161+
})
162+
.filter(op => op !== null);
163+
164+
if (bulkOps.length > 0) {
165+
await db.collection('templates').bulkWrite(bulkOps as any);
166+
process.stdout.write(` Updated ${bulkOps.length} template(s)\n`);
167+
}
168+
169+
// Update entities for each affected template
170+
process.stdout.write('\nUpdating entities...\n');
171+
let totalEntitiesUpdated = 0;
172+
for (const [templateId, changes] of propertyNameChanges.entries()) {
173+
const count = await this.updateEntitiesForTemplate(db, new ObjectId(templateId), changes);
174+
totalEntitiesUpdated += count;
175+
}
176+
177+
process.stdout.write('\nMigration complete:\n');
178+
process.stdout.write(` - ${propertyNameChanges.size} template(s) fixed\n`);
179+
process.stdout.write(` - ${totalEntitiesUpdated} entity/entities updated\n`);
180+
181+
this.reindex = true;
182+
return this.reindex;
183+
},
184+
};
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import testingDB from 'api/utils/testing_db';
2+
import { Db } from 'mongodb';
3+
import migration from '../index';
4+
import { fixtures } from './fixtures';
5+
6+
let db: Db | null;
7+
8+
beforeAll(async () => {
9+
jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
10+
});
11+
12+
afterAll(async () => {
13+
await testingDB.tearDown();
14+
});
15+
16+
describe('migration fix_property_name_mismatches', () => {
17+
beforeEach(async () => {
18+
await testingDB.setupFixturesAndContext(fixtures);
19+
db = testingDB.mongodb;
20+
});
21+
22+
it('should have a delta number', () => {
23+
expect(migration.delta).toBe(183);
24+
});
25+
26+
it('should skip migration when newNameGeneration setting is false', async () => {
27+
await db?.collection('settings').updateOne({}, { $set: { newNameGeneration: false } });
28+
29+
await migration.up(db!);
30+
31+
const templates = await db?.collection('templates').find({}).toArray();
32+
const templateWithMismatch = templates?.find(t => t.name === 'Template with Mismatches');
33+
34+
expect(templateWithMismatch?.properties?.[0].name).toBe('text');
35+
expect(migration.reindex).toBe(false);
36+
});
37+
38+
it('should not modify templates that already have correct property names', async () => {
39+
await migration.up(db!);
40+
41+
const templates = await db?.collection('templates').find({}).toArray();
42+
const correctTemplate = templates?.find(t => t.name === 'Template Already Correct');
43+
44+
expect(correctTemplate?.properties?.[0].name).toBe('text_field');
45+
expect(correctTemplate?.properties?.[1].name).toBe('simple_name');
46+
});
47+
48+
it('should fix template property names that do not match their labels', async () => {
49+
await migration.up(db!);
50+
51+
const templates = await db?.collection('templates').find({}).toArray();
52+
const fixedTemplate = templates?.find(t => t.name === 'Template with Mismatches');
53+
54+
expect(fixedTemplate?.properties?.[0].name).toBe('text_field_');
55+
expect(fixedTemplate?.properties?.[1].name).toBe('email_address_');
56+
});
57+
58+
it('should update entity metadata keys to match new template property names', async () => {
59+
await migration.up(db!);
60+
61+
const entities = await db?.collection('entities').find({}).toArray();
62+
const entityEN = entities?.find(e => e.title === 'Entity 1 EN');
63+
const entityES = entities?.find(e => e.title === 'Entity 1 ES');
64+
const entityPT = entities?.find(e => e.title === 'Entity 1 PT');
65+
66+
// Check EN entity
67+
expect(entityEN?.metadata?.text).toBeUndefined();
68+
expect(entityEN?.metadata?.text_field_).toBeDefined();
69+
expect(entityEN?.metadata?.text_field_?.[0].value).toBe('some text');
70+
71+
expect(entityEN?.metadata?.emailaddress).toBeUndefined();
72+
expect(entityEN?.metadata?.email_address_).toBeDefined();
73+
expect(entityEN?.metadata?.email_address_?.[0].value).toBe('test@example.com');
74+
75+
// Check ES entity
76+
expect(entityES?.metadata?.text).toBeUndefined();
77+
expect(entityES?.metadata?.text_field_).toBeDefined();
78+
expect(entityES?.metadata?.text_field_?.[0].value).toBe('algún texto');
79+
80+
// Check PT entity
81+
expect(entityPT?.metadata?.text).toBeUndefined();
82+
expect(entityPT?.metadata?.text_field_).toBeDefined();
83+
expect(entityPT?.metadata?.text_field_?.[0].value).toBe('algum texto');
84+
});
85+
86+
it('should handle geolocation properties with _geolocation suffix', async () => {
87+
await migration.up(db!);
88+
89+
const templates = await db?.collection('templates').find({}).toArray();
90+
const geoTemplate = templates?.find(t => t.name === 'Template with Geolocation');
91+
92+
expect(geoTemplate?.properties?.[0].name).toBe('location_geolocation');
93+
94+
const entities = await db?.collection('entities').find({}).toArray();
95+
const geoEntity = entities?.find(e => e.title === 'Entity 3');
96+
97+
expect(geoEntity?.metadata?.location).toBeUndefined();
98+
expect(geoEntity?.metadata?.location_geolocation).toBeDefined();
99+
expect(geoEntity?.metadata?.location_geolocation?.[0].value).toEqual({
100+
lat: 40.7128,
101+
lon: -74.006,
102+
});
103+
});
104+
105+
it('should not modify commonProperties array', async () => {
106+
await migration.up(db!);
107+
108+
const templates = await db?.collection('templates').find({}).toArray();
109+
110+
templates?.forEach(template => {
111+
if (template.commonProperties) {
112+
template.commonProperties.forEach((prop: any) => {
113+
// Common properties should remain unchanged
114+
if (prop.label === 'Date added') {
115+
expect(prop.name).toBe('creationDate');
116+
}
117+
});
118+
}
119+
});
120+
});
121+
122+
it('should handle templates with multiple properties needing fixes', async () => {
123+
await migration.up(db!);
124+
125+
const templates = await db?.collection('templates').find({}).toArray();
126+
const multiTemplate = templates?.find(t => t.name === 'Template Multiple Mismatches');
127+
128+
expect(multiTemplate?.properties?.[0].name).toBe('property_one');
129+
expect(multiTemplate?.properties?.[1].name).toBe('property_two_');
130+
expect(multiTemplate?.properties?.[2].name).toBe('property_three');
131+
});
132+
133+
it('should update all entities for a template in a single operation', async () => {
134+
await migration.up(db!);
135+
136+
const entities = await db?.collection('entities').find({}).toArray();
137+
const entity4EN = entities?.find(e => e.title === 'Entity 4 EN');
138+
const entity4ES = entities?.find(e => e.title === 'Entity 4 ES');
139+
140+
// Both entities should have updated property names
141+
expect(entity4EN?.metadata?.prop1).toBeUndefined();
142+
expect(entity4EN?.metadata?.property_one).toBeDefined();
143+
expect(entity4EN?.metadata?.property_one?.[0].value).toBe('value one');
144+
145+
expect(entity4ES?.metadata?.prop1).toBeUndefined();
146+
expect(entity4ES?.metadata?.property_one).toBeDefined();
147+
expect(entity4ES?.metadata?.property_one?.[0].value).toBe('valor uno');
148+
});
149+
150+
it('should handle entities with empty metadata gracefully', async () => {
151+
await migration.up(db!);
152+
153+
const entities = await db?.collection('entities').find({}).toArray();
154+
const entityEmpty = entities?.find(e => e.title === 'Entity 5 Empty');
155+
156+
expect(entityEmpty?.metadata).toEqual({});
157+
});
158+
159+
it('should handle entities with partial metadata (not all template properties present)', async () => {
160+
await migration.up(db!);
161+
162+
const entities = await db?.collection('entities').find({}).toArray();
163+
const partialEntity = entities?.find(e => e.title === 'Entity 6 Partial');
164+
165+
// Only prop1 was present, so only it should be renamed
166+
expect(partialEntity?.metadata?.prop1).toBeUndefined();
167+
expect(partialEntity?.metadata?.property_one).toBeDefined();
168+
expect(partialEntity?.metadata?.property_one?.[0].value).toBe('only first property');
169+
170+
// Other properties should not exist
171+
expect(partialEntity?.metadata?.prop2).toBeUndefined();
172+
expect(partialEntity?.metadata?.property_two_).toBeUndefined();
173+
});
174+
175+
it('should set reindex flag to true when changes are made', async () => {
176+
const reindexAfterMigration = await migration.up(db!);
177+
expect(reindexAfterMigration).toBe(true);
178+
});
179+
180+
it('should set reindex flag to false when no changes are needed', async () => {
181+
// First, run the migration to fix everything
182+
await migration.up(db!);
183+
expect(migration.reindex).toBe(true);
184+
185+
// Then run it again - should return false since nothing needs changing
186+
await migration.up(db!);
187+
expect(migration.reindex).toBe(false);
188+
});
189+
});

0 commit comments

Comments
 (0)