@f5-sales-demo/pi-resource-management 22.8.2 → 22.8.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/pi-resource-management",
4
- "version": "22.8.2",
4
+ "version": "22.8.3",
5
5
  "description": "Shared resource management for F5 XC manifest parsing, validation, diff, and CRUD operations",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Robin Mordasiewicz",
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export type { ExportedManifest, ManifestOutputFormat, MinimalExportFilter } from
9
9
  export { applyMinimalExportFilter, formatManifestOutput, toManifest, toManifestList } from "./manifest-export";
10
10
  export { ManifestParseError, parseManifests } from "./manifest-parser";
11
11
  export { formatValidationErrors, validateManifest, validateManifests } from "./manifest-validator";
12
+ export * from "./map-constraints";
12
13
  export type { RunResourceOperationOptions } from "./operation-runner";
13
14
  export { formatResourceOperationReport, readManifestInputs, runResourceOperation } from "./operation-runner";
14
15
  export {
@@ -72,6 +72,10 @@ export function validateManifest(
72
72
  }
73
73
  }
74
74
 
75
+ if (resolved?.validateFields && options.operation !== "identity") {
76
+ errors.push(...resolved.validateFields(manifest.rawObject, options.operation === "update" ? "update" : "create"));
77
+ }
78
+
75
79
  return { result: { valid: errors.length === 0, errors, warnings }, resolved };
76
80
  }
77
81
 
@@ -0,0 +1,100 @@
1
+ /** Structured map contract rendering and local extension validation. */
2
+ import { isIP } from "node:net";
3
+
4
+ export interface MapScope {
5
+ type?: string;
6
+ minLength?: number;
7
+ maxLength?: number;
8
+ pattern?: string;
9
+ format?: string;
10
+ minimum?: number;
11
+ maximum?: number;
12
+ ranges?: number[][];
13
+ }
14
+
15
+ export interface MapConstraints {
16
+ constraintType: string;
17
+ keys?: MapScope;
18
+ values?: MapScope;
19
+ cardinality?: { minProperties?: number; maxProperties?: number };
20
+ crossEntry?: { uniqueValues?: boolean };
21
+ originalRules?: Record<string, unknown>;
22
+ }
23
+
24
+ export function formatMapConstraints(constraints: MapConstraints): string {
25
+ const parts: string[] = [];
26
+ for (const scope of ["keys", "values", "cardinality", "crossEntry"] as const) {
27
+ for (const [key, value] of Object.entries(constraints[scope] ?? {})) {
28
+ parts.push(`${scope}.${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
29
+ }
30
+ }
31
+ return parts.join(", ");
32
+ }
33
+
34
+ function validateText(value: unknown, scope: MapScope, label: string): string[] {
35
+ const errors: string[] = [];
36
+ if (typeof value !== "string") return [`${label} must be a string`];
37
+ const length = Array.from(value).length;
38
+ if (scope.minLength !== undefined && length < scope.minLength) errors.push(`${label} below minimum length`);
39
+ if (scope.maxLength !== undefined && length > scope.maxLength) errors.push(`${label} exceeds maximum length`);
40
+ if (scope.pattern && !new RegExp(scope.pattern).test(value)) errors.push(`${label} does not match pattern`);
41
+ if (scope.type === "uint32-string") {
42
+ const number = Number(value);
43
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(number) || number > 4294967295)
44
+ errors.push(`${label} must be uint32`);
45
+ if (scope.minimum !== undefined && number < scope.minimum) errors.push(`${label} below minimum`);
46
+ if (scope.maximum !== undefined && number > scope.maximum) errors.push(`${label} exceeds maximum`);
47
+ if (scope.ranges && !scope.ranges.some(([lo, hi]) => number >= lo! && number <= hi!))
48
+ errors.push(`${label} outside ranges`);
49
+ }
50
+ switch (scope.format) {
51
+ case "ipv4":
52
+ if (isIP(value) !== 4) errors.push(`${label} must be IPv4`);
53
+ break;
54
+ case "ipv6":
55
+ if (isIP(value) !== 6) errors.push(`${label} must be IPv6`);
56
+ break;
57
+ case "ip-address":
58
+ if (!isIP(value)) errors.push(`${label} must be IP`);
59
+ break;
60
+ case "mac-address":
61
+ if (!/^(?:[\da-f]{2}:){5}[\da-f]{2}$/i.test(value)) errors.push(`${label} must be MAC`);
62
+ break;
63
+ case "regex":
64
+ try {
65
+ new RegExp(value);
66
+ } catch {
67
+ errors.push(`${label} must be a valid regex`);
68
+ }
69
+ break;
70
+ case "k8s-label-value":
71
+ if (value.length > 63 || !/^(?:[A-Za-z0-9](?:[A-Za-z0-9_.-]*[A-Za-z0-9])?)?$/.test(value))
72
+ errors.push(`${label} must be a Kubernetes label value`);
73
+ break;
74
+ case "uri-reference":
75
+ if (/[^\x21-\x7e]|%(?![\da-f]{2})/i.test(value)) errors.push(`${label} must be a URI-reference`);
76
+ break;
77
+ }
78
+ return errors;
79
+ }
80
+
81
+ export function validateMapConstraints(value: unknown, constraints: MapConstraints): string[] {
82
+ if (!value || typeof value !== "object" || Array.isArray(value)) return ["Expected a map object"];
83
+ const entries = Object.entries(value);
84
+ const errors: string[] = [];
85
+ const cardinality = constraints.cardinality;
86
+ if (cardinality?.minProperties !== undefined && entries.length < cardinality.minProperties)
87
+ errors.push("Too few map pairs");
88
+ if (cardinality?.maxProperties !== undefined && entries.length > cardinality.maxProperties)
89
+ errors.push("Too many map pairs");
90
+ for (const [key, item] of entries) {
91
+ if (constraints.keys) errors.push(...validateText(key, constraints.keys, `key ${key}`));
92
+ if (constraints.values) errors.push(...validateText(item, constraints.values, `value at ${key}`));
93
+ }
94
+ if (
95
+ constraints.crossEntry?.uniqueValues &&
96
+ new Set(entries.map(([, item]) => JSON.stringify(item))).size !== entries.length
97
+ )
98
+ errors.push("Map values must be unique");
99
+ return errors;
100
+ }
package/src/types.ts CHANGED
@@ -85,9 +85,16 @@ export interface ResolvedKind {
85
85
  delete: string;
86
86
  };
87
87
  validation?: ApiSpecValidationResourceEntry;
88
+ validateFields?: (object: Record<string, unknown>, operation: "create" | "update") => ValidationError[];
88
89
  }
89
90
 
90
- export type ValidationErrorCode = "MISSING_FIELD" | "UNKNOWN_KIND" | "INVALID_TYPE" | "PARSE_ERROR" | "DUPLICATE_NAME";
91
+ export type ValidationErrorCode =
92
+ | "MISSING_FIELD"
93
+ | "UNKNOWN_KIND"
94
+ | "INVALID_TYPE"
95
+ | "PARSE_ERROR"
96
+ | "DUPLICATE_NAME"
97
+ | "INVALID_MAP";
91
98
 
92
99
  export type ValidationWarningCode = "EXTRA_FIELD" | "DEPRECATED_FIELD";
93
100