Skip to content

Commit a8f6aac

Browse files
committed
More Validation
1 parent 662cf63 commit a8f6aac

File tree

2 files changed

+173
-14
lines changed

2 files changed

+173
-14
lines changed

packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.spec.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,4 +839,117 @@ describe('Script Generation', () => {
839839
}
840840
});
841841
});
842+
843+
describe('Graceful Handling', () => {
844+
it('should default invalid probability to 1.0', () => {
845+
const schema = {
846+
field1: {
847+
mongoType: 'string',
848+
fakerMethod: 'lorem.word',
849+
fakerArgs: [],
850+
probability: 1.5, // Invalid - should default to 1.0
851+
},
852+
field2: {
853+
mongoType: 'string',
854+
fakerMethod: 'lorem.word',
855+
fakerArgs: [],
856+
probability: -0.5, // Invalid - should default to 1.0
857+
},
858+
field3: {
859+
mongoType: 'string',
860+
fakerMethod: 'lorem.word',
861+
fakerArgs: [],
862+
probability: 'invalid' as any, // Invalid - should default to 1.0
863+
},
864+
};
865+
866+
const result = generateScript(schema, {
867+
databaseName: 'test',
868+
collectionName: 'test',
869+
documentCount: 1,
870+
});
871+
872+
expect(result.success).to.equal(true);
873+
if (result.success) {
874+
// All fields should be treated as probability 1.0 (always present)
875+
expect(result.script).to.contain('field1: faker.lorem.word()');
876+
expect(result.script).to.contain('field2: faker.lorem.word()');
877+
expect(result.script).to.contain('field3: faker.lorem.word()');
878+
expect(result.script).not.to.contain('Math.random()');
879+
}
880+
});
881+
882+
it('should handle field names with brackets (non-array)', () => {
883+
const schema = {
884+
'settings[theme]': createFieldMapping('lorem.word'),
885+
'data[0]': createFieldMapping('lorem.word'),
886+
'bracket]field': createFieldMapping('lorem.word'),
887+
'[metadata': createFieldMapping('lorem.word'),
888+
};
889+
890+
const result = generateScript(schema, {
891+
databaseName: 'test',
892+
collectionName: 'test',
893+
documentCount: 1,
894+
});
895+
896+
expect(result.success).to.equal(true);
897+
if (result.success) {
898+
// All fields should be treated as regular field names, not arrays
899+
expect(result.script).to.contain('settings[theme]: faker.lorem.word()');
900+
expect(result.script).to.contain('data[0]: faker.lorem.word()');
901+
expect(result.script).to.contain('bracket]field: faker.lorem.word()');
902+
expect(result.script).to.contain('[metadata: faker.lorem.word()');
903+
expect(result.script).not.to.contain('Array.from');
904+
}
905+
});
906+
907+
it('should handle field names with [] in middle (not array notation)', () => {
908+
const schema = {
909+
'squareBrackets[]InMiddle': createFieldMapping('lorem.word'),
910+
'field[]WithMore': createFieldMapping('lorem.word'),
911+
'start[]middle[]end': createFieldMapping('lorem.word'),
912+
};
913+
914+
const result = generateScript(schema, {
915+
databaseName: 'test',
916+
collectionName: 'test',
917+
documentCount: 1,
918+
});
919+
920+
expect(result.success).to.equal(true);
921+
if (result.success) {
922+
// These should be treated as regular field names, not arrays
923+
expect(result.script).to.contain(
924+
'squareBrackets[]InMiddle: faker.lorem.word()'
925+
);
926+
expect(result.script).to.contain('field[]WithMore: faker.lorem.word()');
927+
expect(result.script).to.contain(
928+
'start[]middle[]end: faker.lorem.word()'
929+
);
930+
expect(result.script).not.to.contain('Array.from');
931+
}
932+
});
933+
934+
it('should still handle real array notation correctly', () => {
935+
const schema = {
936+
'realArray[]': createFieldMapping('lorem.word'),
937+
'nestedArray[].field': createFieldMapping('lorem.word'),
938+
};
939+
940+
const result = generateScript(schema, {
941+
databaseName: 'test',
942+
collectionName: 'test',
943+
documentCount: 1,
944+
});
945+
946+
expect(result.success).to.equal(true);
947+
if (result.success) {
948+
// These should be treated as arrays
949+
expect(result.script).to.contain('Array.from');
950+
expect(result.script).to.contain('realArray: Array.from');
951+
expect(result.script).to.contain('nestedArray: Array.from');
952+
}
953+
});
954+
});
842955
});

packages/compass-collection/src/components/mock-data-generator-modal/script-generation-utils.ts

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,34 @@ function parseFieldPath(fieldPath: string): string[] {
5858
if (current) {
5959
parts.push(current);
6060
current = '';
61+
} else if (parts.length > 0 && parts[parts.length - 1] === '[]') {
62+
// This is valid: "users[].name" - dot after array notation
63+
// Continue parsing
64+
} else {
65+
throw new Error(
66+
`Invalid field path "${fieldPath}": empty field name before dot`
67+
);
6168
}
6269
} else if (char === '[' && fieldPath[i + 1] === ']') {
63-
if (current) {
64-
parts.push(current);
65-
current = '';
70+
// Only treat [] as array notation if it's at the end, followed by a dot, or followed by another [
71+
const isAtEnd = i + 2 >= fieldPath.length;
72+
const isFollowedByDot =
73+
i + 2 < fieldPath.length && fieldPath[i + 2] === '.';
74+
const isFollowedByBracket =
75+
i + 2 < fieldPath.length && fieldPath[i + 2] === '[';
76+
77+
if (isAtEnd || isFollowedByDot || isFollowedByBracket) {
78+
// This is array notation
79+
if (current) {
80+
parts.push(current);
81+
current = '';
82+
}
83+
parts.push('[]');
84+
i++; // Skip the ]
85+
} else {
86+
// This is just part of the field name
87+
current += char;
6688
}
67-
parts.push('[]');
68-
i++; // Skip the ]
6989
} else {
7090
current += char;
7191
}
@@ -75,6 +95,12 @@ function parseFieldPath(fieldPath: string): string[] {
7595
parts.push(current);
7696
}
7797

98+
if (parts.length === 0) {
99+
throw new Error(
100+
`Invalid field path "${fieldPath}": no valid field names found`
101+
);
102+
}
103+
78104
return parts;
79105
}
80106
/**
@@ -103,18 +129,16 @@ function insertIntoStructure(
103129
mapping: FieldMapping
104130
): void {
105131
if (pathParts.length === 0) {
106-
// This shouldn't happen
107-
// TODO: log error
108-
return;
132+
throw new Error('Cannot insert field mapping: empty path parts array');
109133
}
110134

111135
// Base case: insert root-level field mapping
112136
if (pathParts.length === 1) {
113137
const part = pathParts[0];
114138
if (part === '[]') {
115-
// This shouldn't happen - array without field name
116-
// TODO: log error
117-
return;
139+
throw new Error(
140+
'Invalid field path: array notation "[]" cannot be used without a field name'
141+
);
118142
}
119143
structure[part] = mapping;
120144
return;
@@ -302,7 +326,16 @@ function generateDocumentCode(
302326
// It's a field mapping
303327
const mapping = value as FieldMapping;
304328
const fakerCall = generateFakerCall(mapping);
305-
const probability = mapping.probability ?? 1.0;
329+
// Default to 1.0 for invalid probability values
330+
let probability = 1.0;
331+
if (
332+
mapping.probability !== undefined &&
333+
typeof mapping.probability === 'number' &&
334+
mapping.probability >= 0 &&
335+
mapping.probability <= 1
336+
) {
337+
probability = mapping.probability;
338+
}
306339

307340
if (probability < 1.0) {
308341
// Use Math.random for conditional field inclusion
@@ -439,17 +472,30 @@ export function getDefaultFakerMethod(mongoType: string): string {
439472
export function formatFakerArgs(fakerArgs: FakerArg[]): string {
440473
const stringifiedArgs: string[] = [];
441474

442-
for (const arg of fakerArgs) {
475+
for (let i = 0; i < fakerArgs.length; i++) {
476+
const arg = fakerArgs[i];
477+
443478
if (typeof arg === 'string') {
444479
// Escape single quotes for JS strings (and backticks for security)
445480
const escapedArg = arg.replace(/[`']/g, '\\$&');
446481
stringifiedArgs.push(`'${escapedArg}'`);
447-
} else if (typeof arg === 'number' || typeof arg === 'boolean') {
482+
} else if (typeof arg === 'number') {
483+
if (!Number.isFinite(arg)) {
484+
throw new Error(
485+
`Invalid number argument at index ${i}: must be a finite number`
486+
);
487+
}
488+
stringifiedArgs.push(`${arg}`);
489+
} else if (typeof arg === 'boolean') {
448490
stringifiedArgs.push(`${arg}`);
449491
} else if (typeof arg === 'object' && arg !== null && 'json' in arg) {
450492
// Pre-serialized JSON objects
451493
const jsonArg = arg as { json: string };
452494
stringifiedArgs.push(jsonArg.json);
495+
} else {
496+
throw new Error(
497+
`Invalid argument type at index ${i}: expected string, number, boolean, or {json: string}`
498+
);
453499
}
454500
}
455501

0 commit comments

Comments
 (0)