@portel/photon-core 2.9.4 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +23 -1
- package/dist/compiler.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mixins.d.ts +42 -0
- package/dist/mixins.d.ts.map +1 -0
- package/dist/mixins.js +156 -0
- package/dist/mixins.js.map +1 -0
- package/dist/schema-extractor.d.ts +4 -0
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +255 -16
- package/dist/schema-extractor.js.map +1 -1
- package/package.json +6 -4
- package/src/compiler.ts +26 -1
- package/src/index.ts +3 -0
- package/src/mixins.ts +187 -0
- package/src/schema-extractor.ts +323 -16
package/dist/schema-extractor.js
CHANGED
|
@@ -103,10 +103,14 @@ export class SchemaExtractor {
|
|
|
103
103
|
// Extract descriptions from JSDoc
|
|
104
104
|
const paramDocs = this.extractParamDocs(jsdoc);
|
|
105
105
|
const paramConstraints = this.extractParamConstraints(jsdoc);
|
|
106
|
+
// Track which paramDocs/constraints were matched for fail-safe handling
|
|
107
|
+
const matchedDocs = new Set();
|
|
108
|
+
const matchedConstraints = new Set();
|
|
106
109
|
// Merge descriptions and constraints into properties
|
|
107
110
|
Object.keys(properties).forEach(key => {
|
|
108
111
|
if (paramDocs.has(key)) {
|
|
109
112
|
properties[key].description = paramDocs.get(key);
|
|
113
|
+
matchedDocs.add(key);
|
|
110
114
|
}
|
|
111
115
|
// Apply TypeScript-extracted metadata (before JSDoc, so JSDoc can override)
|
|
112
116
|
if (properties[key]._tsReadOnly) {
|
|
@@ -117,8 +121,44 @@ export class SchemaExtractor {
|
|
|
117
121
|
if (paramConstraints.has(key)) {
|
|
118
122
|
const constraints = paramConstraints.get(key);
|
|
119
123
|
this.applyConstraints(properties[key], constraints);
|
|
124
|
+
matchedConstraints.add(key);
|
|
120
125
|
}
|
|
121
126
|
});
|
|
127
|
+
// Fail-safe: Handle mismatched parameter names from JSDoc
|
|
128
|
+
// If a @param name doesn't match any actual parameter, try fuzzy matching
|
|
129
|
+
const unmatchedDocs = Array.from(paramDocs.entries()).filter(([name]) => !matchedDocs.has(name));
|
|
130
|
+
if (unmatchedDocs.length > 0) {
|
|
131
|
+
const propKeys = Object.keys(properties);
|
|
132
|
+
// If there's only one parameter and one unmatched doc, assume it belongs to that parameter
|
|
133
|
+
if (propKeys.length === 1 && unmatchedDocs.length === 1) {
|
|
134
|
+
const paramKey = propKeys[0];
|
|
135
|
+
const [docName, docValue] = unmatchedDocs[0];
|
|
136
|
+
if (!properties[paramKey].description) {
|
|
137
|
+
properties[paramKey].description = `${docName}: ${docValue}`;
|
|
138
|
+
console.warn(`Parameter name mismatch in JSDoc: @param ${docName} doesn't match ` +
|
|
139
|
+
`function parameter "${paramKey}". Using description from @param ${docName}.`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (unmatchedDocs.length > 0) {
|
|
143
|
+
// Log warning for other mismatches (multiple parameters or multiple unmatched docs)
|
|
144
|
+
unmatchedDocs.forEach(([docName]) => {
|
|
145
|
+
console.warn(`Parameter name mismatch in JSDoc: @param ${docName} doesn't match any function parameter. ` +
|
|
146
|
+
`Available parameters: ${propKeys.join(', ')}. Consider updating @param tag.`);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Similar fail-safe for constraints
|
|
151
|
+
const unmatchedConstraints = Array.from(paramConstraints.entries()).filter(([name]) => !matchedConstraints.has(name));
|
|
152
|
+
if (unmatchedConstraints.length > 0) {
|
|
153
|
+
const propKeys = Object.keys(properties);
|
|
154
|
+
if (propKeys.length === 1 && unmatchedConstraints.length === 1) {
|
|
155
|
+
const paramKey = propKeys[0];
|
|
156
|
+
const [constraintName, constraintValue] = unmatchedConstraints[0];
|
|
157
|
+
this.applyConstraints(properties[paramKey], constraintValue);
|
|
158
|
+
console.warn(`Parameter name mismatch in JSDoc: constraint tag for ${constraintName} doesn't match ` +
|
|
159
|
+
`function parameter "${paramKey}". Applying constraint to "${paramKey}".`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
122
162
|
const description = this.extractDescription(jsdoc);
|
|
123
163
|
const inputSchema = {
|
|
124
164
|
type: 'object',
|
|
@@ -169,7 +209,7 @@ export class SchemaExtractor {
|
|
|
169
209
|
const throttled = this.extractThrottled(jsdoc);
|
|
170
210
|
const debounced = this.extractDebounced(jsdoc);
|
|
171
211
|
const queued = this.extractQueued(jsdoc);
|
|
172
|
-
const validations = this.extractValidations(jsdoc);
|
|
212
|
+
const validations = this.extractValidations(jsdoc, Object.keys(properties));
|
|
173
213
|
const deprecated = this.extractDeprecated(jsdoc);
|
|
174
214
|
// Build unified middleware declarations (single source of truth for runtime)
|
|
175
215
|
const middleware = this.buildMiddlewareDeclarations(jsdoc);
|
|
@@ -450,6 +490,21 @@ export class SchemaExtractor {
|
|
|
450
490
|
else {
|
|
451
491
|
properties[paramName] = { type: 'string' };
|
|
452
492
|
}
|
|
493
|
+
// Extract default value if initializer exists
|
|
494
|
+
if (param.initializer) {
|
|
495
|
+
const defaultValue = this.extractDefaultValue(param.initializer, sourceFile);
|
|
496
|
+
if (defaultValue !== undefined) {
|
|
497
|
+
// Validate default value type matches parameter type
|
|
498
|
+
const paramType = properties[paramName].type;
|
|
499
|
+
if (!this.isDefaultValueTypeCompatible(defaultValue, paramType)) {
|
|
500
|
+
const defaultType = typeof defaultValue;
|
|
501
|
+
console.warn(`Default value type mismatch: parameter "${paramName}" is type "${paramType}" ` +
|
|
502
|
+
`but default value is type "${defaultType}" (${JSON.stringify(defaultValue)}). ` +
|
|
503
|
+
`This may cause runtime errors.`);
|
|
504
|
+
}
|
|
505
|
+
properties[paramName].default = defaultValue;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
453
508
|
}
|
|
454
509
|
return { properties, required, simpleParams: true };
|
|
455
510
|
}
|
|
@@ -756,6 +811,28 @@ export class SchemaExtractor {
|
|
|
756
811
|
/**
|
|
757
812
|
* Extract default value from initializer
|
|
758
813
|
*/
|
|
814
|
+
/**
|
|
815
|
+
* Check if a default value's type is compatible with the parameter type
|
|
816
|
+
*/
|
|
817
|
+
isDefaultValueTypeCompatible(defaultValue, paramType) {
|
|
818
|
+
const valueType = typeof defaultValue;
|
|
819
|
+
// Type must match (number → number, boolean → boolean, etc.)
|
|
820
|
+
switch (paramType) {
|
|
821
|
+
case 'string':
|
|
822
|
+
return valueType === 'string';
|
|
823
|
+
case 'number':
|
|
824
|
+
// Number params need number defaults, not strings
|
|
825
|
+
return valueType === 'number';
|
|
826
|
+
case 'boolean':
|
|
827
|
+
return valueType === 'boolean';
|
|
828
|
+
case 'array':
|
|
829
|
+
return Array.isArray(defaultValue);
|
|
830
|
+
case 'object':
|
|
831
|
+
return valueType === 'object';
|
|
832
|
+
default:
|
|
833
|
+
return true; // Unknown types are assumed compatible
|
|
834
|
+
}
|
|
835
|
+
}
|
|
759
836
|
extractDefaultValue(initializer, sourceFile) {
|
|
760
837
|
// String literals
|
|
761
838
|
if (ts.isStringLiteral(initializer)) {
|
|
@@ -772,8 +849,20 @@ export class SchemaExtractor {
|
|
|
772
849
|
if (initializer.kind === ts.SyntaxKind.FalseKeyword) {
|
|
773
850
|
return false;
|
|
774
851
|
}
|
|
775
|
-
//
|
|
776
|
-
|
|
852
|
+
// Detect complex expressions
|
|
853
|
+
const expressionText = initializer.getText(sourceFile);
|
|
854
|
+
const isComplexExpression = ts.isCallExpression(initializer) || // Function calls: Math.max(10, 100)
|
|
855
|
+
ts.isObjectLiteralExpression(initializer) || // Objects: { key: 'value' }
|
|
856
|
+
ts.isArrayLiteralExpression(initializer) || // Arrays: [1, 2, 3]
|
|
857
|
+
ts.isBinaryExpression(initializer) || // Binary ops: 10 + 20
|
|
858
|
+
ts.isConditionalExpression(initializer); // Ternary: x ? a : b
|
|
859
|
+
if (isComplexExpression) {
|
|
860
|
+
console.warn(`Complex default value cannot be reliably serialized: "${expressionText}". ` +
|
|
861
|
+
`Default will not be applied to schema. Consider using a simple literal value instead.`);
|
|
862
|
+
return undefined;
|
|
863
|
+
}
|
|
864
|
+
// For other expressions, return as string
|
|
865
|
+
return expressionText;
|
|
777
866
|
}
|
|
778
867
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
779
868
|
// INLINE CONFIG + @use PARSING
|
|
@@ -1061,7 +1150,16 @@ export class SchemaExtractor {
|
|
|
1061
1150
|
// Extract {@format formatName} - use lookahead to match until tag-closing }
|
|
1062
1151
|
const formatMatch = description.match(/\{@format\s+(.+?)\}(?=\s|$|{@)/);
|
|
1063
1152
|
if (formatMatch) {
|
|
1064
|
-
|
|
1153
|
+
const format = formatMatch[1].trim();
|
|
1154
|
+
// Validate format is in whitelist (JSON Schema + custom formats)
|
|
1155
|
+
const validFormats = ['email', 'date', 'date-time', 'time', 'duration', 'uri', 'uri-reference', 'uuid', 'ipv4', 'ipv6', 'hostname', 'json', 'table'];
|
|
1156
|
+
if (!validFormats.includes(format)) {
|
|
1157
|
+
console.warn(`Invalid @format value: "${format}". ` +
|
|
1158
|
+
`Valid formats: ${validFormats.join(', ')}. Format not applied.`);
|
|
1159
|
+
}
|
|
1160
|
+
else {
|
|
1161
|
+
paramConstraints.format = format;
|
|
1162
|
+
}
|
|
1065
1163
|
}
|
|
1066
1164
|
// Extract {@choice value1,value2,...} - converts to enum in schema
|
|
1067
1165
|
const choiceMatch = description.match(/\{@choice\s+([^}]+)\}/);
|
|
@@ -1087,6 +1185,16 @@ export class SchemaExtractor {
|
|
|
1087
1185
|
paramConstraints.default = defaultValue;
|
|
1088
1186
|
}
|
|
1089
1187
|
}
|
|
1188
|
+
// Extract {@minItems value} - for arrays
|
|
1189
|
+
const minItemsMatch = description.match(/\{@minItems\s+(-?\d+(?:\.\d+)?)\}/);
|
|
1190
|
+
if (minItemsMatch) {
|
|
1191
|
+
paramConstraints.minItems = parseInt(minItemsMatch[1], 10);
|
|
1192
|
+
}
|
|
1193
|
+
// Extract {@maxItems value} - for arrays
|
|
1194
|
+
const maxItemsMatch = description.match(/\{@maxItems\s+(-?\d+(?:\.\d+)?)\}/);
|
|
1195
|
+
if (maxItemsMatch) {
|
|
1196
|
+
paramConstraints.maxItems = parseInt(maxItemsMatch[1], 10);
|
|
1197
|
+
}
|
|
1090
1198
|
// Extract {@unique} or {@uniqueItems} - for arrays
|
|
1091
1199
|
if (description.match(/\{@unique(?:Items)?\s*\}/)) {
|
|
1092
1200
|
paramConstraints.unique = true;
|
|
@@ -1118,10 +1226,15 @@ export class SchemaExtractor {
|
|
|
1118
1226
|
if (deprecatedMatch) {
|
|
1119
1227
|
paramConstraints.deprecated = deprecatedMatch[1]?.trim() || true;
|
|
1120
1228
|
}
|
|
1121
|
-
// Extract {@readOnly} and {@writeOnly} -
|
|
1122
|
-
// They are mutually exclusive, so
|
|
1229
|
+
// Extract {@readOnly} and {@writeOnly} - detect conflicts
|
|
1230
|
+
// They are mutually exclusive, so warn if both present
|
|
1123
1231
|
const readOnlyMatch = description.match(/\{@readOnly\s*\}/);
|
|
1124
1232
|
const writeOnlyMatch = description.match(/\{@writeOnly\s*\}/);
|
|
1233
|
+
if (readOnlyMatch && writeOnlyMatch) {
|
|
1234
|
+
// Warn about conflict
|
|
1235
|
+
console.warn(`Conflicting constraints: @readOnly and @writeOnly cannot both be applied. ` +
|
|
1236
|
+
`Keeping @${readOnlyMatch && writeOnlyMatch ? (description.lastIndexOf('@readOnly') > description.lastIndexOf('@writeOnly') ? 'readOnly' : 'writeOnly') : 'readOnly'}.`);
|
|
1237
|
+
}
|
|
1125
1238
|
if (readOnlyMatch || writeOnlyMatch) {
|
|
1126
1239
|
// Find positions to determine which comes last
|
|
1127
1240
|
const readOnlyPos = readOnlyMatch ? description.indexOf(readOnlyMatch[0]) : -1;
|
|
@@ -1159,6 +1272,19 @@ export class SchemaExtractor {
|
|
|
1159
1272
|
if (acceptMatch) {
|
|
1160
1273
|
paramConstraints.accept = acceptMatch[1].trim();
|
|
1161
1274
|
}
|
|
1275
|
+
// Validate no unknown {@...} tags (typos in constraint names)
|
|
1276
|
+
const allKnownTags = ['min', 'max', 'pattern', 'format', 'choice', 'field', 'default', 'unique', 'uniqueItems',
|
|
1277
|
+
'example', 'multipleOf', 'deprecated', 'readOnly', 'writeOnly', 'label', 'placeholder',
|
|
1278
|
+
'hint', 'hidden', 'accept', 'minItems', 'maxItems'];
|
|
1279
|
+
const unknownTagRegex = /\{@([\w-]+)\s*(?:\s+[^}]*)?\}/g;
|
|
1280
|
+
let unknownMatch;
|
|
1281
|
+
while ((unknownMatch = unknownTagRegex.exec(description)) !== null) {
|
|
1282
|
+
const tagName = unknownMatch[1];
|
|
1283
|
+
if (!allKnownTags.includes(tagName)) {
|
|
1284
|
+
console.warn(`unknown constraint/hint: @${tagName}. ` +
|
|
1285
|
+
`Valid hints: ${allKnownTags.slice(0, 8).join(', ')}, etc. This tag will be ignored.`);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1162
1288
|
if (Object.keys(paramConstraints).length > 0) {
|
|
1163
1289
|
constraints.set(paramName, paramConstraints);
|
|
1164
1290
|
}
|
|
@@ -1172,8 +1298,39 @@ export class SchemaExtractor {
|
|
|
1172
1298
|
* Works with both simple types and anyOf schemas
|
|
1173
1299
|
*/
|
|
1174
1300
|
applyConstraints(schema, constraints) {
|
|
1301
|
+
// Validate constraint values before applying (fail-safe)
|
|
1302
|
+
if (constraints.min !== undefined && constraints.max !== undefined) {
|
|
1303
|
+
if (constraints.min > constraints.max) {
|
|
1304
|
+
console.warn(`Invalid constraint: @min (${constraints.min}) > @max (${constraints.max}). ` +
|
|
1305
|
+
`Using only @min. Remove @max or use a larger value.`);
|
|
1306
|
+
delete constraints.max;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1175
1309
|
// Helper to apply constraints to a single schema based on type
|
|
1176
1310
|
const applyToSchema = (s) => {
|
|
1311
|
+
// Validate constraint-type compatibility
|
|
1312
|
+
if (!s.enum) {
|
|
1313
|
+
// Warn for incompatible constraints
|
|
1314
|
+
if ((constraints.min !== undefined || constraints.max !== undefined) &&
|
|
1315
|
+
s.type !== 'number' && s.type !== 'string' && s.type !== 'array') {
|
|
1316
|
+
const constraintType = constraints.min !== undefined ? '@min' : '@max';
|
|
1317
|
+
console.warn(`Constraint ${constraintType} not applicable to type "${s.type || 'unknown'}". ` +
|
|
1318
|
+
`This constraint applies to number, string, or array types only.`);
|
|
1319
|
+
}
|
|
1320
|
+
if (constraints.pattern !== undefined && s.type !== 'string') {
|
|
1321
|
+
console.warn(`Constraint @pattern not applicable to type "${s.type || 'unknown'}". ` +
|
|
1322
|
+
`Pattern only applies to string types.`);
|
|
1323
|
+
}
|
|
1324
|
+
if ((constraints.minItems !== undefined || constraints.maxItems !== undefined) && s.type !== 'array') {
|
|
1325
|
+
const constraintType = constraints.minItems !== undefined ? '@minItems' : '@maxItems';
|
|
1326
|
+
console.warn(`Constraint ${constraintType} not applicable to type "${s.type || 'unknown'}". ` +
|
|
1327
|
+
`This constraint applies to array types only.`);
|
|
1328
|
+
}
|
|
1329
|
+
if (constraints.multipleOf !== undefined && s.type !== 'number') {
|
|
1330
|
+
console.warn(`Constraint @multipleOf not applicable to type "${s.type || 'unknown'}". ` +
|
|
1331
|
+
`This constraint applies to number types only.`);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1177
1334
|
if (s.enum) {
|
|
1178
1335
|
// Skip enum types for most constraints (but still apply deprecated, examples, etc.)
|
|
1179
1336
|
if (constraints.examples !== undefined) {
|
|
@@ -1184,7 +1341,7 @@ export class SchemaExtractor {
|
|
|
1184
1341
|
}
|
|
1185
1342
|
return;
|
|
1186
1343
|
}
|
|
1187
|
-
// Apply min/max based on type
|
|
1344
|
+
// Apply min/max based on type (skip if type is incompatible)
|
|
1188
1345
|
if (s.type === 'number') {
|
|
1189
1346
|
if (constraints.min !== undefined) {
|
|
1190
1347
|
s.minimum = constraints.min;
|
|
@@ -1193,7 +1350,14 @@ export class SchemaExtractor {
|
|
|
1193
1350
|
s.maximum = constraints.max;
|
|
1194
1351
|
}
|
|
1195
1352
|
if (constraints.multipleOf !== undefined) {
|
|
1196
|
-
|
|
1353
|
+
// Validate multipleOf is positive (JSON schema requires > 0)
|
|
1354
|
+
if (constraints.multipleOf <= 0) {
|
|
1355
|
+
console.warn(`Invalid @multipleOf value: ${constraints.multipleOf}. ` +
|
|
1356
|
+
`Must be positive and non-zero. Constraint not applied.`);
|
|
1357
|
+
}
|
|
1358
|
+
else {
|
|
1359
|
+
s.multipleOf = constraints.multipleOf;
|
|
1360
|
+
}
|
|
1197
1361
|
}
|
|
1198
1362
|
}
|
|
1199
1363
|
else if (s.type === 'string') {
|
|
@@ -1204,7 +1368,22 @@ export class SchemaExtractor {
|
|
|
1204
1368
|
s.maxLength = constraints.max;
|
|
1205
1369
|
}
|
|
1206
1370
|
if (constraints.pattern !== undefined) {
|
|
1207
|
-
|
|
1371
|
+
// Check for pattern+enum conflict
|
|
1372
|
+
if (s.enum || constraints.enum) {
|
|
1373
|
+
console.warn(`Conflicting constraints: @pattern cannot be used with enum/choices. ` +
|
|
1374
|
+
`Pattern is ignored when specific values are defined via enum or @choice.`);
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
// Validate pattern is valid regex before applying (fail-safe)
|
|
1378
|
+
try {
|
|
1379
|
+
new RegExp(constraints.pattern);
|
|
1380
|
+
s.pattern = constraints.pattern;
|
|
1381
|
+
}
|
|
1382
|
+
catch (e) {
|
|
1383
|
+
console.warn(`Invalid regex in @pattern constraint: "${constraints.pattern}". ` +
|
|
1384
|
+
`${e instanceof Error ? e.message : String(e)}. Pattern not applied.`);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1208
1387
|
}
|
|
1209
1388
|
}
|
|
1210
1389
|
else if (s.type === 'array') {
|
|
@@ -1214,10 +1393,28 @@ export class SchemaExtractor {
|
|
|
1214
1393
|
if (constraints.max !== undefined) {
|
|
1215
1394
|
s.maxItems = constraints.max;
|
|
1216
1395
|
}
|
|
1396
|
+
if (constraints.minItems !== undefined) {
|
|
1397
|
+
s.minItems = constraints.minItems;
|
|
1398
|
+
}
|
|
1399
|
+
if (constraints.maxItems !== undefined) {
|
|
1400
|
+
s.maxItems = constraints.maxItems;
|
|
1401
|
+
}
|
|
1217
1402
|
if (constraints.unique === true) {
|
|
1218
1403
|
s.uniqueItems = true;
|
|
1219
1404
|
}
|
|
1220
1405
|
}
|
|
1406
|
+
else if (s.type && s.type !== 'boolean' && s.type !== 'object') {
|
|
1407
|
+
// For other compatible types, apply min/max as needed
|
|
1408
|
+
if (['integer', 'number'].includes(s.type)) {
|
|
1409
|
+
if (constraints.min !== undefined) {
|
|
1410
|
+
s.minimum = constraints.min;
|
|
1411
|
+
}
|
|
1412
|
+
if (constraints.max !== undefined) {
|
|
1413
|
+
s.maximum = constraints.max;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
// Note: For boolean and object types, we skip min/max constraints (already warned above)
|
|
1221
1418
|
// Apply type-agnostic constraints
|
|
1222
1419
|
if (constraints.format !== undefined) {
|
|
1223
1420
|
s.format = constraints.format;
|
|
@@ -1471,11 +1668,22 @@ export class SchemaExtractor {
|
|
|
1471
1668
|
* - @retryable 3 2s → 3 retries, 2s delay
|
|
1472
1669
|
*/
|
|
1473
1670
|
extractRetryable(jsdocContent) {
|
|
1474
|
-
const match = jsdocContent.match(/@retryable(?:\s+(
|
|
1671
|
+
const match = jsdocContent.match(/@retryable(?:\s+([-\d]+))?(?:\s+([-\d]+(?:ms|s|sec|m|min|h|hr|d|day)))?/i);
|
|
1475
1672
|
if (!match)
|
|
1476
1673
|
return undefined;
|
|
1477
|
-
|
|
1478
|
-
|
|
1674
|
+
let count = match[1] ? parseInt(match[1], 10) : 3;
|
|
1675
|
+
let delay = match[2] ? parseDuration(match[2]) : 1_000;
|
|
1676
|
+
// Validate retryable configuration
|
|
1677
|
+
if (count <= 0) {
|
|
1678
|
+
console.warn(`Invalid @retryable count: ${count}. ` +
|
|
1679
|
+
`Count must be positive (> 0). Using default count 3.`);
|
|
1680
|
+
count = 3;
|
|
1681
|
+
}
|
|
1682
|
+
if (delay <= 0) {
|
|
1683
|
+
console.warn(`Invalid @retryable delay: ${delay}ms. ` +
|
|
1684
|
+
`Delay must be positive (> 0). Using default delay 1000ms.`);
|
|
1685
|
+
delay = 1_000;
|
|
1686
|
+
}
|
|
1479
1687
|
return { count, delay };
|
|
1480
1688
|
}
|
|
1481
1689
|
/**
|
|
@@ -1484,10 +1692,34 @@ export class SchemaExtractor {
|
|
|
1484
1692
|
* - @throttled 100/h → 100 calls per hour
|
|
1485
1693
|
*/
|
|
1486
1694
|
extractThrottled(jsdocContent) {
|
|
1695
|
+
// First check if @throttled is present at all
|
|
1696
|
+
const hasThrottled = /@throttled\b/i.test(jsdocContent);
|
|
1487
1697
|
const match = jsdocContent.match(/@throttled\s+(\d+\/(?:s|sec|m|min|h|hr|d|day))/i);
|
|
1488
|
-
if (!match)
|
|
1698
|
+
if (!match) {
|
|
1699
|
+
if (hasThrottled) {
|
|
1700
|
+
// @throttled is present but format is invalid
|
|
1701
|
+
const invalidMatch = jsdocContent.match(/@throttled\s+([^\s]+)/i);
|
|
1702
|
+
if (invalidMatch) {
|
|
1703
|
+
console.warn(`Invalid @throttled rate format: "${invalidMatch[1]}". ` +
|
|
1704
|
+
`Expected format: count/unit (e.g., 10/min, 100/h). Rate not applied.`);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1489
1707
|
return undefined;
|
|
1490
|
-
|
|
1708
|
+
}
|
|
1709
|
+
const rateStr = match[1];
|
|
1710
|
+
const rate = parseRate(rateStr);
|
|
1711
|
+
// Validate throttled configuration
|
|
1712
|
+
if (!rate) {
|
|
1713
|
+
console.warn(`Invalid @throttled rate format: "${rateStr}". ` +
|
|
1714
|
+
`Expected format: count/unit (e.g., 10/min, 100/h). Rate not applied.`);
|
|
1715
|
+
return undefined;
|
|
1716
|
+
}
|
|
1717
|
+
if (rate.count <= 0) {
|
|
1718
|
+
console.warn(`Invalid @throttled count: ${rate.count}. ` +
|
|
1719
|
+
`Count must be positive (> 0). Rate not applied.`);
|
|
1720
|
+
return undefined;
|
|
1721
|
+
}
|
|
1722
|
+
return rate;
|
|
1491
1723
|
}
|
|
1492
1724
|
/**
|
|
1493
1725
|
* Extract debounce configuration from @debounced tag
|
|
@@ -1519,13 +1751,20 @@ export class SchemaExtractor {
|
|
|
1519
1751
|
* - @validate params.email must be a valid email
|
|
1520
1752
|
* - @validate params.amount must be positive
|
|
1521
1753
|
*/
|
|
1522
|
-
extractValidations(jsdocContent) {
|
|
1754
|
+
extractValidations(jsdocContent, validParamNames) {
|
|
1523
1755
|
const validations = [];
|
|
1524
1756
|
const regex = /@validate\s+([\w.]+)\s+(.+)/g;
|
|
1525
1757
|
let match;
|
|
1526
1758
|
while ((match = regex.exec(jsdocContent)) !== null) {
|
|
1759
|
+
const fieldName = match[1].replace(/^params\./, ''); // strip params. prefix
|
|
1760
|
+
// Fail-safe: Validate that referenced field exists (if validParamNames provided)
|
|
1761
|
+
if (validParamNames && !validParamNames.includes(fieldName)) {
|
|
1762
|
+
console.warn(`@validate references non-existent parameter "${fieldName}". ` +
|
|
1763
|
+
`Available parameters: ${validParamNames.join(', ')}. Validation rule ignored.`);
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1527
1766
|
validations.push({
|
|
1528
|
-
field:
|
|
1767
|
+
field: fieldName,
|
|
1529
1768
|
rule: match[2].trim().replace(/\*\/$/, '').trim(), // strip JSDoc closing
|
|
1530
1769
|
});
|
|
1531
1770
|
}
|