Skip to content

Commit e96cb63

Browse files
committed
feat(input_schema): Replace patternKey and patternValue with propertyNames and patternProperties for object property
1 parent 5ae454f commit e96cb63

File tree

6 files changed

+379
-11
lines changed

6 files changed

+379
-11
lines changed

packages/input_schema/src/input_schema.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@ import { inputSchema as schema } from '@apify/json_schemas';
55

66
import { m } from './intl';
77
import type {
8-
ArrayFieldDefinition,
98
CommonResourceFieldDefinition,
109
FieldDefinition,
1110
InputSchema,
1211
InputSchemaBaseChecked,
13-
ObjectFieldDefinition,
1412
StringFieldDefinition,
1513
} from './types';
1614
import { ensureAjvSupportsDraft2019, validateRegexpPattern } from './utilities';
@@ -93,6 +91,9 @@ export function parseAjvError(
9391
} else if (error.keyword === 'const') {
9492
fieldKey = cleanPropertyName(error.instancePath);
9593
message = m('inputSchema.validation.generic', { rootName, fieldKey, message: error.message });
94+
} else if (error.keyword === 'pattern' && error.propertyName && error.params?.pattern) {
95+
fieldKey = cleanPropertyName(`${error.instancePath}/${error.propertyName}`);
96+
message = m('inputSchema.validation.propertyName', { rootName, fieldKey, pattern: error.params.pattern });
9697
} else {
9798
fieldKey = cleanPropertyName(error.instancePath);
9899
message = m('inputSchema.validation.generic', { rootName, fieldKey, message: error.message });
@@ -207,12 +208,21 @@ function validateField(validator: Ajv, fieldSchema: Record<string, unknown>, fie
207208
validateFieldAgainstSchemaDefinition(validator, fieldSchema, fieldKey, isSubField);
208209

209210
// Validate regex patterns if defined.
210-
const { pattern } = fieldSchema as Partial<StringFieldDefinition>;
211-
const { patternKey, patternValue } = fieldSchema as Partial<ObjectFieldDefinition & ArrayFieldDefinition>;
212-
213-
if (pattern) validateRegexpPattern(pattern, `${fieldKey}.pattern`);
214-
if (patternKey) validateRegexpPattern(patternKey, `${fieldKey}.patternKey`);
215-
if (patternValue) validateRegexpPattern(patternValue, `${fieldKey}.patternValue`);
211+
if ('pattern' in fieldSchema && fieldSchema.pattern) {
212+
validateRegexpPattern(fieldSchema.pattern, `${fieldKey}.pattern`);
213+
}
214+
if ('patternKey' in fieldSchema && fieldSchema.patternKey) {
215+
validateRegexpPattern(fieldSchema.patternKey, `${fieldKey}.patternKey`);
216+
}
217+
if ('patternValue' in fieldSchema && fieldSchema.patternValue) {
218+
validateRegexpPattern(fieldSchema.patternValue, `${fieldKey}.patternValue`);
219+
}
220+
if ('propertyNames' in fieldSchema && fieldSchema.propertyNames?.pattern) {
221+
validateRegexpPattern(fieldSchema.propertyNames.pattern, `${fieldKey}.propertyNames.pattern`);
222+
}
223+
if ('patternProperties' in fieldSchema && fieldSchema.patternProperties?.['.*'].pattern) {
224+
validateRegexpPattern(fieldSchema.patternProperties['.*'].pattern, `${fieldKey}.patternProperties.*.pattern`);
225+
}
216226
}
217227

218228
/**

packages/input_schema/src/intl.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ const intlStrings = {
3939
'The regular expression "{pattern}" in field schema.properties.{fieldKey} must be valid.',
4040
'inputSchema.validation.regexpNotSafe':
4141
'The regular expression "{pattern}" in field schema.properties.{fieldKey} may cause excessive backtracking or be unsafe to execute.',
42+
'inputSchema.validation.propertyName':
43+
'Property name of {rootName}.{fieldKey} must match pattern "{pattern}".',
4244
};
4345

4446
/**

packages/input_schema/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ export type ObjectFieldDefinition = CommonFieldDefinition<object> & {
5656
properties?: Record<string, unknown>;
5757
required?: string[];
5858
additionalProperties?: boolean;
59+
propertyNames?: { pattern: string };
60+
patternProperties?: { '.*': { type: 'string', pattern: string } };
5961
}
6062

6163
export type ArrayFieldDefinition = CommonFieldDefinition<unknown[]> & {

packages/json_schemas/schemas/input.schema.json

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,9 @@
367367
},
368368
"additionalProperties": {
369369
"type": "boolean"
370-
}
370+
},
371+
"propertyNames": { "$ref": "#/definitions/propertyNamesDefinition" },
372+
"patternProperties": { "$ref": "#/definitions/patternPropertiesDefinition" }
371373
},
372374
"unevaluatedProperties": false,
373375
"allOf": [
@@ -422,7 +424,9 @@
422424
},
423425
"additionalProperties": {
424426
"type": "boolean"
425-
}
427+
},
428+
"propertyNames": { "$ref": "#/definitions/propertyNamesDefinition" },
429+
"patternProperties": { "$ref": "#/definitions/patternPropertiesDefinition" }
426430
}
427431
}
428432
},
@@ -977,7 +981,9 @@
977981
},
978982
"additionalProperties": {
979983
"type": "boolean"
980-
}
984+
},
985+
"propertyNames": { "$ref": "#/definitions/propertyNamesDefinition" },
986+
"patternProperties": { "$ref": "#/definitions/patternPropertiesDefinition" }
981987
},
982988
"required": ["type", "title", "description"],
983989
"if": {
@@ -1601,6 +1607,32 @@
16011607
"required": ["type"]
16021608
}
16031609
}
1610+
},
1611+
"propertyNamesDefinition": {
1612+
"title": "Utils: Property names definition",
1613+
"type": "object",
1614+
"properties": {
1615+
"pattern": { "type": "string" }
1616+
},
1617+
"additionalProperties": false,
1618+
"required": ["pattern"]
1619+
},
1620+
"patternPropertiesDefinition": {
1621+
"title": "Utils: Pattern properties definition",
1622+
"type": "object",
1623+
"properties": {
1624+
".*": {
1625+
"type": "object",
1626+
"properties": {
1627+
"type": { "enum": ["string"] },
1628+
"pattern": { "type": "string" }
1629+
},
1630+
"additionalProperties": false,
1631+
"required": ["type", "pattern"]
1632+
}
1633+
},
1634+
"additionalProperties": false,
1635+
"required": [".*"]
16041636
}
16051637
}
16061638
}

test/input_schema.test.ts

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,6 +1256,15 @@ describe('input_schema.json', () => {
12561256
editor: 'json',
12571257
patternKey: '^[a-z]+$',
12581258
patternValue: '^[0-9]+$',
1259+
propertyNames: {
1260+
pattern: '^[a-zA-Z_][a-zA-Z0-9_]*$',
1261+
},
1262+
patternProperties: {
1263+
'.*': {
1264+
type: 'string',
1265+
pattern: '^[0-9]+$',
1266+
},
1267+
},
12591268
},
12601269
arrayField: {
12611270
title: 'Array field',
@@ -1335,6 +1344,240 @@ describe('input_schema.json', () => {
13351344
'Input schema is not valid (The regular expression "^[0-9+$" in field schema.properties.objectField.patternValue must be valid.)',
13361345
);
13371346
});
1347+
1348+
it('should throw error on invalid propertyNames regexp', () => {
1349+
const schema = {
1350+
title: 'Test input schema',
1351+
type: 'object',
1352+
schemaVersion: 1,
1353+
properties: {
1354+
objectField: {
1355+
title: 'Object field',
1356+
type: 'object',
1357+
description: 'Some description ...',
1358+
editor: 'json',
1359+
propertyNames: {
1360+
pattern: '^[0-9+$', // invalid regexp
1361+
},
1362+
},
1363+
},
1364+
};
1365+
1366+
expect(() => validateInputSchema(validator, schema)).toThrow(
1367+
'Input schema is not valid (The regular expression "^[0-9+$" in field schema.properties.objectField.propertyNames.pattern must be valid.)',
1368+
);
1369+
});
1370+
1371+
it('should throw error on invalid patternProperties regexp', () => {
1372+
const schema = {
1373+
title: 'Test input schema',
1374+
type: 'object',
1375+
schemaVersion: 1,
1376+
properties: {
1377+
objectField: {
1378+
title: 'Object field',
1379+
type: 'object',
1380+
description: 'Some description ...',
1381+
editor: 'json',
1382+
patternProperties: {
1383+
'.*': {
1384+
type: 'string',
1385+
pattern: '^[0-9+$', // invalid regexp
1386+
},
1387+
},
1388+
},
1389+
},
1390+
};
1391+
1392+
expect(() => validateInputSchema(validator, schema)).toThrow(
1393+
// eslint-disable-next-line max-len
1394+
'Input schema is not valid (The regular expression "^[0-9+$" in field schema.properties.objectField.patternProperties.*.pattern must be valid.)',
1395+
);
1396+
});
1397+
});
1398+
1399+
describe('propertyNames working correctly', () => {
1400+
it('should accept valid property names', () => {
1401+
const schema = {
1402+
title: 'Test input schema',
1403+
type: 'object',
1404+
schemaVersion: 1,
1405+
properties: {
1406+
myField: {
1407+
title: 'Field title',
1408+
type: 'object',
1409+
description: 'My test field',
1410+
editor: 'json',
1411+
propertyNames: {
1412+
pattern: '^[a-zA-Z_][a-zA-Z0-9_]*$',
1413+
},
1414+
},
1415+
},
1416+
};
1417+
expect(() => validateInputSchema(validator, schema)).not.toThrow();
1418+
});
1419+
1420+
it('should throw if pattern is missing', () => {
1421+
const schema = {
1422+
title: 'Test input schema',
1423+
type: 'object',
1424+
schemaVersion: 1,
1425+
properties: {
1426+
myField: {
1427+
title: 'Field title',
1428+
type: 'object',
1429+
description: 'My test field',
1430+
editor: 'json',
1431+
propertyNames: {
1432+
// missing pattern
1433+
},
1434+
},
1435+
},
1436+
};
1437+
expect(() => validateInputSchema(validator, schema)).toThrow(
1438+
'Input schema is not valid (Field schema.properties.myField.propertyNames.pattern is required)',
1439+
);
1440+
});
1441+
1442+
it('should not allow propertyNames for other than object type', () => {
1443+
const types = {
1444+
string: 'textfield',
1445+
integer: 'number',
1446+
number: 'number',
1447+
boolean: 'checkbox',
1448+
array: 'json',
1449+
};
1450+
Object.entries(types).forEach(([type, editor]) => {
1451+
const schema = {
1452+
title: 'Test input schema',
1453+
type: 'object',
1454+
schemaVersion: 1,
1455+
properties: {
1456+
myField: {
1457+
title: 'Field title',
1458+
type,
1459+
description: 'My test field',
1460+
editor,
1461+
propertyNames: {
1462+
pattern: '^[a-zA-Z_][a-zA-Z0-9_]*$',
1463+
},
1464+
},
1465+
},
1466+
};
1467+
expect(() => validateInputSchema(validator, schema)).toThrow(
1468+
`Input schema is not valid (Property schema.properties.myField.propertyNames is not allowed.)`,
1469+
);
1470+
});
1471+
});
1472+
});
1473+
1474+
describe('patternProperties working correctly', () => {
1475+
it('should accept valid patternProperties', () => {
1476+
const schema = {
1477+
title: 'Test input schema',
1478+
type: 'object',
1479+
schemaVersion: 1,
1480+
properties: {
1481+
myField: {
1482+
title: 'Field title',
1483+
type: 'object',
1484+
description: 'My test field',
1485+
editor: 'json',
1486+
patternProperties: {
1487+
'.*': {
1488+
type: 'string',
1489+
pattern: '^[0-9]+$',
1490+
},
1491+
},
1492+
},
1493+
},
1494+
};
1495+
expect(() => validateInputSchema(validator, schema)).not.toThrow();
1496+
});
1497+
1498+
it('should throw if patternProperties value is missing type', () => {
1499+
const schema = {
1500+
title: 'Test input schema',
1501+
type: 'object',
1502+
schemaVersion: 1,
1503+
properties: {
1504+
myField: {
1505+
title: 'Field title',
1506+
type: 'object',
1507+
description: 'My test field',
1508+
editor: 'json',
1509+
patternProperties: {
1510+
'.*': {
1511+
// missing type
1512+
},
1513+
},
1514+
},
1515+
},
1516+
};
1517+
expect(() => validateInputSchema(validator, schema)).toThrow(
1518+
'Input schema is not valid (Field schema.properties.myField.patternProperties..*.type is required)',
1519+
);
1520+
});
1521+
1522+
it('should not allow additional properties in patternProperties value', () => {
1523+
const schema = {
1524+
title: 'Test input schema',
1525+
type: 'object',
1526+
schemaVersion: 1,
1527+
properties: {
1528+
myField: {
1529+
title: 'Field title',
1530+
type: 'object',
1531+
description: 'My test field',
1532+
editor: 'json',
1533+
patternProperties: {
1534+
'.*': {
1535+
type: 'string',
1536+
pattern: '^[0-9]+$',
1537+
extraProperty: 'not allowed',
1538+
},
1539+
},
1540+
},
1541+
},
1542+
};
1543+
expect(() => validateInputSchema(validator, schema)).toThrow(
1544+
'Input schema is not valid (Property schema.properties.myField.patternProperties..*.extraProperty is not allowed.)',
1545+
);
1546+
});
1547+
1548+
it('should not allow patternProperties for other than object type', () => {
1549+
const types = {
1550+
string: 'textfield',
1551+
integer: 'number',
1552+
number: 'number',
1553+
boolean: 'checkbox',
1554+
array: 'json',
1555+
};
1556+
Object.entries(types).forEach(([type, editor]) => {
1557+
const schema = {
1558+
title: 'Test input schema',
1559+
type: 'object',
1560+
schemaVersion: 1,
1561+
properties: {
1562+
myField: {
1563+
title: 'Field title',
1564+
type,
1565+
description: 'My test field',
1566+
editor,
1567+
patternProperties: {
1568+
'^[a-zA-Z_][a-zA-Z0-9_]*$': {
1569+
type: 'string',
1570+
regex: '^[0-9]+$',
1571+
},
1572+
},
1573+
},
1574+
},
1575+
};
1576+
expect(() => validateInputSchema(validator, schema)).toThrow(
1577+
`Input schema is not valid (Property schema.properties.myField.patternProperties is not allowed.)`,
1578+
);
1579+
});
1580+
});
13381581
});
13391582
});
13401583
});

0 commit comments

Comments
 (0)