From 8c11b28580d44ca32187070cede9ac414dff77cc Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:48:54 -0500 Subject: [PATCH] refactor: [M3-8945] - Update `yup` from `0.32.9` to `1.4.0` (#11324) * initial update * fix cloudpulse schemas * fix incorrect `stackscript_data` schema * add changesets --------- Co-authored-by: Banks Nussman --- .../pr-11324-tech-stories-1732641088654.md | 5 + packages/api-v4/package.json | 3 +- .../pr-11324-tech-stories-1732641114172.md | 5 + .../support/plugins/generate-weights.ts | 10 +- packages/manager/package.json | 3 +- .../CloudPulse/Alerts/CreateAlert/schemas.ts | 7 +- .../pr-11324-tech-stories-1732641099442.md | 5 + packages/validation/package.json | 3 +- packages/validation/src/account.schema.ts | 19 +- packages/validation/src/buckets.schema.ts | 4 +- packages/validation/src/cloudpulse.schema.ts | 2 +- packages/validation/src/databases.schema.ts | 8 +- packages/validation/src/domains.schema.ts | 16 +- packages/validation/src/firewalls.schema.ts | 23 +- packages/validation/src/kubernetes.schema.ts | 48 ++-- packages/validation/src/linodes.schema.ts | 260 ++++++++++-------- packages/validation/src/managed.schema.ts | 6 +- packages/validation/src/networking.schema.ts | 2 +- .../validation/src/nodebalancers.schema.ts | 21 +- packages/validation/src/records.schema.ts | 4 +- packages/validation/src/volumes.schema.ts | 2 +- packages/validation/src/vpcs.schema.ts | 60 ++-- yarn.lock | 71 ++--- 23 files changed, 294 insertions(+), 293 deletions(-) create mode 100644 packages/api-v4/.changeset/pr-11324-tech-stories-1732641088654.md create mode 100644 packages/manager/.changeset/pr-11324-tech-stories-1732641114172.md create mode 100644 packages/validation/.changeset/pr-11324-tech-stories-1732641099442.md diff --git a/packages/api-v4/.changeset/pr-11324-tech-stories-1732641088654.md b/packages/api-v4/.changeset/pr-11324-tech-stories-1732641088654.md new file mode 100644 index 00000000000..54ab7a2bd16 --- /dev/null +++ b/packages/api-v4/.changeset/pr-11324-tech-stories-1732641088654.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Tech Stories +--- + +Update yup from `0.32.9` to `1.4.0` ([#11324](https://github.com/linode/manager/pull/11324)) diff --git a/packages/api-v4/package.json b/packages/api-v4/package.json index 2c151782dbc..c048a286f11 100644 --- a/packages/api-v4/package.json +++ b/packages/api-v4/package.json @@ -43,7 +43,7 @@ "@linode/validation": "*", "axios": "~1.7.4", "ipaddr.js": "^2.0.0", - "yup": "^0.32.9" + "yup": "^1.4.0" }, "scripts": { "start": "concurrently --raw \"tsc -w --preserveWatchOutput\" \"tsup --watch\"", @@ -57,7 +57,6 @@ "lib" ], "devDependencies": { - "@types/yup": "^0.29.13", "axios-mock-adapter": "^1.22.0", "concurrently": "^9.0.1", "eslint": "^6.8.0", diff --git a/packages/manager/.changeset/pr-11324-tech-stories-1732641114172.md b/packages/manager/.changeset/pr-11324-tech-stories-1732641114172.md new file mode 100644 index 00000000000..d45d08a966d --- /dev/null +++ b/packages/manager/.changeset/pr-11324-tech-stories-1732641114172.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Update yup from `0.32.9` to `1.4.0` ([#11324](https://github.com/linode/manager/pull/11324)) diff --git a/packages/manager/cypress/support/plugins/generate-weights.ts b/packages/manager/cypress/support/plugins/generate-weights.ts index bfb13cc0b32..becd14638f6 100644 --- a/packages/manager/cypress/support/plugins/generate-weights.ts +++ b/packages/manager/cypress/support/plugins/generate-weights.ts @@ -1,8 +1,10 @@ -import type { CypressPlugin } from './plugin'; -import { DateTime } from 'luxon'; import { writeFileSync } from 'fs'; +import { DateTime } from 'luxon'; import { resolve } from 'path'; -import { object, string, array, number, SchemaOf } from 'yup'; +import { array, number, object, string } from 'yup'; + +import type { CypressPlugin } from './plugin'; +import type { ObjectSchema } from 'yup'; // The name of the environment variable to read to check if generation is enabled. // The value should be a path to the weights file. @@ -50,7 +52,7 @@ export interface SpecWeight extends SpecResult { /** * Spec weights schema for JSON parsing, etc. */ -export const specWeightsSchema: SchemaOf = object({ +export const specWeightsSchema: ObjectSchema = object({ meta: object({ datetime: string().required(), totalWeight: number().required(), diff --git a/packages/manager/package.json b/packages/manager/package.json index 32c9c19bc59..85c4c8ef960 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -85,7 +85,7 @@ "tss-react": "^4.8.2", "typescript-fsa": "^3.0.0", "typescript-fsa-reducers": "^1.2.0", - "yup": "^0.32.9", + "yup": "^1.4.0", "zxcvbn": "^4.4.2" }, "scripts": { @@ -166,7 +166,6 @@ "@types/redux-mock-store": "^1.0.1", "@types/throttle-debounce": "^1.0.0", "@types/uuid": "^3.4.3", - "@types/yup": "^0.29.13", "@types/zxcvbn": "^4.4.0", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/schemas.ts b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/schemas.ts index 5bbe4289000..8b9301c3ebf 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/schemas.ts +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/schemas.ts @@ -3,13 +3,14 @@ import { object, string } from 'yup'; const engineOptionValidation = string().when('service_type', { is: 'dbaas', - otherwise: string().notRequired().nullable(), - then: string().required('Engine type is required.').nullable(), + otherwise: (schema) => schema.notRequired().nullable(), + then: (schema) => schema.required('Engine type is required.').nullable(), }); + export const CreateAlertDefinitionFormSchema = createAlertDefinitionSchema.concat( object({ engineType: engineOptionValidation, region: string().required('Region is required.'), - serviceType: string().required('Service is required.').nullable(), + serviceType: string().required('Service is required.'), }) ); diff --git a/packages/validation/.changeset/pr-11324-tech-stories-1732641099442.md b/packages/validation/.changeset/pr-11324-tech-stories-1732641099442.md new file mode 100644 index 00000000000..3e19735e4b9 --- /dev/null +++ b/packages/validation/.changeset/pr-11324-tech-stories-1732641099442.md @@ -0,0 +1,5 @@ +--- +"@linode/validation": Tech Stories +--- + +Update yup from `0.32.9` to `1.4.0` ([#11324](https://github.com/linode/manager/pull/11324)) diff --git a/packages/validation/package.json b/packages/validation/package.json index 37f62119cb8..26f563e1be7 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -37,10 +37,9 @@ "url": "https://github.com/linode/manager/tree/develop/packages/validation" }, "dependencies": { - "@types/yup": "^0.29.13", "ipaddr.js": "^2.0.0", "libphonenumber-js": "^1.10.6", - "yup": "^0.32.9" + "yup": "^1.4.0" }, "devDependencies": { "concurrently": "^9.0.1", diff --git a/packages/validation/src/account.schema.ts b/packages/validation/src/account.schema.ts index ff600b9ce78..7813135f39a 100644 --- a/packages/validation/src/account.schema.ts +++ b/packages/validation/src/account.schema.ts @@ -67,10 +67,11 @@ export const PaymentMethodSchema = object({ ), data: object().when('type', { is: 'credit_card', - then: CreditCardSchema, - otherwise: object({ - nonce: string().required('Payment nonce is required.'), - }), + then: () => CreditCardSchema, + otherwise: () => + object({ + nonce: string().required('Payment nonce is required.'), + }), }), is_default: boolean().required( 'You must indicate if this should be your default method of payment.' @@ -100,10 +101,12 @@ export const UpdateUserSchema = object({ const GrantSchema = object({ id: number().required('ID is required.'), - permissions: mixed().oneOf( - [null, 'read_only', 'read_write'], - 'Permissions must be null, read_only, or read_write.' - ), + permissions: string() + .oneOf( + ['read_only', 'read_write'], + 'Permissions must be null, read_only, or read_write.' + ) + .nullable('Permissions must be null, read_only, or read_write.'), }); export const UpdateGrantSchema = object({ diff --git a/packages/validation/src/buckets.schema.ts b/packages/validation/src/buckets.schema.ts index e3d0d486608..bb14be983a4 100644 --- a/packages/validation/src/buckets.schema.ts +++ b/packages/validation/src/buckets.schema.ts @@ -39,11 +39,11 @@ export const CreateBucketSchema = object() ), cluster: string().when('region', { is: (region: string) => !region || region.length === 0, - then: string().required('Cluster is required.'), + then: (schema) => schema.required('Cluster is required.'), }), region: string().when('cluster', { is: (cluster: string) => !cluster || cluster.length === 0, - then: string().required('Region is required.'), + then: (schema) => schema.required('Region is required.'), }), endpoint_type: string() .oneOf([...ENDPOINT_TYPES]) diff --git a/packages/validation/src/cloudpulse.schema.ts b/packages/validation/src/cloudpulse.schema.ts index 18c5b59886b..c639deb9d7d 100644 --- a/packages/validation/src/cloudpulse.schema.ts +++ b/packages/validation/src/cloudpulse.schema.ts @@ -30,8 +30,8 @@ const triggerCondition = object({ export const createAlertDefinitionSchema = object({ label: string().required('Name is required.'), description: string().optional(), + severity: string().required('Severity is required.'), entity_ids: array().of(string()).min(1, 'At least one resource is needed.'), - severity: string().required('Severity is required.').nullable(), criteria: array() .of(metricCriteria) .min(1, 'At least one metric criteria is needed.'), diff --git a/packages/validation/src/databases.schema.ts b/packages/validation/src/databases.schema.ts index 9cc4edd9b7f..60ad4ed0a2b 100644 --- a/packages/validation/src/databases.schema.ts +++ b/packages/validation/src/databases.schema.ts @@ -14,8 +14,8 @@ export const createDatabaseSchema = object({ cluster_size: number() .oneOf([1, 2, 3], 'Nodes are required') .required('Nodes are required'), - replication_type: string().notRequired().nullable(true), // TODO (UIE-8214) remove POST GA - replication_commit_type: string().notRequired().nullable(true), // TODO (UIE-8214) remove POST GA + replication_type: string().notRequired().nullable(), // TODO (UIE-8214) remove POST GA + replication_commit_type: string().notRequired().nullable(), // TODO (UIE-8214) remove POST GA }); export const updateDatabaseSchema = object({ @@ -28,8 +28,8 @@ export const updateDatabaseSchema = object({ duration: number(), hour_of_day: number(), day_of_week: number(), - week_of_month: number().nullable(true), + week_of_month: number().nullable(), }) - .nullable(true), + .nullable(), type: string().notRequired(), }); diff --git a/packages/validation/src/domains.schema.ts b/packages/validation/src/domains.schema.ts index d453be54ed9..4e0e7d1f29b 100644 --- a/packages/validation/src/domains.schema.ts +++ b/packages/validation/src/domains.schema.ts @@ -37,8 +37,7 @@ export const createDomainSchema = domainSchemaBase.shape({ soa_email: string() .when('type', { is: 'master', - then: string().required('SOA Email is required.'), - otherwise: string(), + then: (schema) => schema.required('SOA Email is required.'), }) .email('SOA Email is not valid.') .trim(), @@ -46,13 +45,12 @@ export const createDomainSchema = domainSchemaBase.shape({ .of(string()) .when('type', { is: 'slave', - then: array() - .of(string()) - .compact() - .ensure() - .required('At least one primary IP address is required.') - .min(1, 'At least one primary IP address is required.'), - otherwise: array().of(string()), + then: (schema) => + schema + .compact() + .ensure() + .required('At least one primary IP address is required.') + .min(1, 'At least one primary IP address is required.'), }), }); diff --git a/packages/validation/src/firewalls.schema.ts b/packages/validation/src/firewalls.schema.ts index d00e1eb6451..bdf5c3e1b80 100644 --- a/packages/validation/src/firewalls.schema.ts +++ b/packages/validation/src/firewalls.schema.ts @@ -147,26 +147,27 @@ export const FirewallRuleTypeSchema = object().shape({ .required('Protocol is required.'), ports: string().when('protocol', { is: (val: any) => val !== 'ICMP' && val !== 'IPENCAP', - then: validateFirewallPorts, + then: () => validateFirewallPorts, // Workaround to get the test to fail if ports is defined when protocol === ICMP or IPENCAP - otherwise: string().test({ - name: 'protocol', - message: 'Ports are not allowed for ICMP and IPENCAP protocols.', - test: (value) => typeof value === 'undefined', - }), + otherwise: (schema) => + schema.test({ + name: 'protocol', + message: 'Ports are not allowed for ICMP and IPENCAP protocols.', + test: (value) => typeof value === 'undefined', + }), }), addresses: object() .shape({ - ipv4: array().of(ipAddress).nullable(true), - ipv6: array().of(ipAddress).nullable(true), + ipv4: array().of(ipAddress).nullable(), + ipv6: array().of(ipAddress).nullable(), }) .strict(true) - .nullable(true), + .nullable(), }); export const FirewallRuleSchema = object().shape({ - inbound: array(FirewallRuleTypeSchema).nullable(true), - outbound: array(FirewallRuleTypeSchema).nullable(true), + inbound: array(FirewallRuleTypeSchema).nullable(), + outbound: array(FirewallRuleTypeSchema).nullable(), inbound_policy: mixed() .oneOf(['ACCEPT', 'DROP']) .required('Inbound policy is required.'), diff --git a/packages/validation/src/kubernetes.schema.ts b/packages/validation/src/kubernetes.schema.ts index 8910936d005..b16ecd24493 100644 --- a/packages/validation/src/kubernetes.schema.ts +++ b/packages/validation/src/kubernetes.schema.ts @@ -10,31 +10,33 @@ export const AutoscaleNodePoolSchema = object({ enabled: boolean(), min: number().when('enabled', { is: true, - then: number() - .required() - .test( - 'min', - 'Minimum must be between 1 and 99 nodes and cannot be greater than Maximum.', - function (min) { - if (!min) { - return false; + then: (schema) => + schema + .required() + .test( + 'min', + 'Minimum must be between 1 and 99 nodes and cannot be greater than Maximum.', + function (min) { + if (!min) { + return false; + } + if (min < 1 || min > 99) { + return false; + } + if (min > this.parent['max']) { + return false; + } + return true; } - if (min < 1 || min > 99) { - return false; - } - if (min > this.parent['max']) { - return false; - } - return true; - } - ), + ), }), max: number().when('enabled', { is: true, - then: number() - .required() - .min(1, 'Maximum must be between 1 and 100 nodes.') - .max(100, 'Maximum must be between 1 and 100 nodes.'), + then: (schema) => + schema + .required() + .min(1, 'Maximum must be between 1 and 100 nodes.') + .max(100, 'Maximum must be between 1 and 100 nodes.'), }), }); @@ -76,8 +78,8 @@ const controlPlaneACLOptionsSchema = object().shape({ enabled: boolean(), 'revision-id': string(), addresses: object().shape({ - ipv4: array().of(ipv4Address).nullable(true), - ipv6: array().of(ipv6Address).nullable(true), + ipv4: array().of(ipv4Address).nullable(), + ipv6: array().of(ipv6Address).nullable(), }), }); diff --git a/packages/validation/src/linodes.schema.ts b/packages/validation/src/linodes.schema.ts index 712b534098a..de1f1d16d31 100644 --- a/packages/validation/src/linodes.schema.ts +++ b/packages/validation/src/linodes.schema.ts @@ -47,7 +47,7 @@ const LINODE_LABEL_CHAR_REQUIREMENT = 'Label must contain between 3 and 64 characters.'; // Schemas -const stackscript_data = array().of(object()).nullable(true); +const stackscript_data = object().nullable(); const IPv4 = string() .notRequired() @@ -70,55 +70,57 @@ const IPv6 = string() const ipv4ConfigInterface = object().when('purpose', { is: 'vpc', - then: object({ - vpc: IPv4, - nat_1_1: lazy((value) => - value === 'any' ? string().notRequired().nullable() : IPv4 - ), - }), - otherwise: object() - .nullable() - .test({ - name: testnameDisallowedBasedOnPurpose('VPC'), - message: testmessageDisallowedBasedOnPurpose('vpc', 'ipv4.vpc'), - /* + then: (schema) => + schema.shape({ + vpc: IPv4, + nat_1_1: lazy((value) => + value === 'any' ? string().notRequired().nullable() : IPv4 + ), + }), + otherwise: (schema) => + schema + .nullable() + .test({ + name: testnameDisallowedBasedOnPurpose('VPC'), + message: testmessageDisallowedBasedOnPurpose('vpc', 'ipv4.vpc'), + /* Workaround to get test to fail if field is populated when it should not be based on purpose (inspired by similar approach in firewalls.schema.ts for ports field). Similarly-structured logic (return typeof xyz === 'undefined') throughout this file serves the same purpose. */ - test: (value) => { - if (value?.vpc) { - return typeof value.vpc === 'undefined'; - } - - return true; - }, - }) - .test({ - name: testnameDisallowedBasedOnPurpose('VPC'), - message: testmessageDisallowedBasedOnPurpose('vpc', 'ipv4.nat_1_1'), - test: (value) => { - if (value?.nat_1_1) { - return typeof value.nat_1_1 === 'undefined'; - } - - return true; - }, - }), + test: (value: any) => { + if (value?.vpc) { + return typeof value.vpc === 'undefined'; + } + + return true; + }, + }) + .test({ + name: testnameDisallowedBasedOnPurpose('VPC'), + message: testmessageDisallowedBasedOnPurpose('vpc', 'ipv4.nat_1_1'), + test: (value: any) => { + if (value?.nat_1_1) { + return typeof value.nat_1_1 === 'undefined'; + } + + return true; + }, + }), }); const ipv6ConfigInterface = object().when('purpose', { is: 'vpc', - then: object({ - vpc: IPv6, - }), - otherwise: object() - .nullable() - .test({ + then: (schema) => + schema.shape({ + vpc: IPv6, + }), + otherwise: (schema) => + schema.nullable().test({ name: testnameDisallowedBasedOnPurpose('VPC'), message: testmessageDisallowedBasedOnPurpose('vpc', 'ipv6.vpc'), - test: (value) => { + test: (value: any) => { if (value?.vpc) { return typeof value.vpc === 'undefined'; } @@ -135,67 +137,79 @@ export const LinodeInterfaceSchema = object().shape({ ), label: string().when('purpose', { is: 'vlan', - then: string() - .required('VLAN label is required.') - .min(1, 'VLAN label must be between 1 and 64 characters.') - .max(64, 'VLAN label must be between 1 and 64 characters.') - .matches( - /[a-zA-Z0-9-]+/, - 'Must include only ASCII letters, numbers, and dashes' - ), - otherwise: string().when('label', { - is: null, - then: string().nullable(), - otherwise: string().test({ - name: testnameDisallowedBasedOnPurpose('VLAN'), - message: testmessageDisallowedBasedOnPurpose('vlan', 'label'), - test: (value) => typeof value === 'undefined' || value === '', + then: (schema) => + schema + .required('VLAN label is required.') + .min(1, 'VLAN label must be between 1 and 64 characters.') + .max(64, 'VLAN label must be between 1 and 64 characters.') + .matches( + /[a-zA-Z0-9-]+/, + 'Must include only ASCII letters, numbers, and dashes' + ), + otherwise: (schema) => + schema.when('label', { + is: null, + then: (s) => s.nullable(), + otherwise: (s) => + s.test({ + name: testnameDisallowedBasedOnPurpose('VLAN'), + message: testmessageDisallowedBasedOnPurpose('vlan', 'label'), + test: (value) => typeof value === 'undefined' || value === '', + }), }), - }), }), ipam_address: string().when('purpose', { is: 'vlan', - then: string().notRequired().nullable().test({ - name: 'validateIPAM', - message: 'Must be a valid IPv4 range, e.g. 192.0.2.0/24.', - test: validateIP, - }), - otherwise: string().when('ipam_address', { - is: null, - then: string().nullable(), - otherwise: string().test({ - name: testnameDisallowedBasedOnPurpose('VLAN'), - message: testmessageDisallowedBasedOnPurpose('vlan', 'ipam_address'), - test: (value) => typeof value === 'undefined' || value === '', + then: (schema) => + schema.notRequired().nullable().test({ + name: 'validateIPAM', + message: 'Must be a valid IPv4 range, e.g. 192.0.2.0/24.', + test: validateIP, + }), + otherwise: (schema) => + schema.when('ipam_address', { + is: null, + then: (s) => s.nullable(), + otherwise: (s) => + s.test({ + name: testnameDisallowedBasedOnPurpose('VLAN'), + message: testmessageDisallowedBasedOnPurpose( + 'vlan', + 'ipam_address' + ), + test: (value) => typeof value === 'undefined' || value === '', + }), }), - }), }), primary: boolean().notRequired(), subnet_id: number().when('purpose', { is: 'vpc', - then: number() - .transform((value) => (isNaN(value) ? undefined : value)) - .required('Subnet is required.'), - otherwise: number() - .notRequired() - .nullable() - .test({ - name: testnameDisallowedBasedOnPurpose('VPC'), - message: testmessageDisallowedBasedOnPurpose('vpc', 'subnet_id'), - test: (value) => typeof value === 'undefined' || value === null, - }), + then: (schema) => + schema + .transform((value) => (isNaN(value) ? undefined : value)) + .required('Subnet is required.'), + otherwise: (schema) => + schema + .notRequired() + .nullable() + .test({ + name: testnameDisallowedBasedOnPurpose('VPC'), + message: testmessageDisallowedBasedOnPurpose('vpc', 'subnet_id'), + test: (value) => typeof value === 'undefined' || value === null, + }), }), vpc_id: number().when('purpose', { is: 'vpc', - then: number().required('VPC is required.'), - otherwise: number() - .notRequired() - .nullable() - .test({ - name: testnameDisallowedBasedOnPurpose('VPC'), - message: testmessageDisallowedBasedOnPurpose('vpc', 'vpc_id'), - test: (value) => typeof value === 'undefined' || value === null, - }), + then: (schema) => schema.required('VPC is required.'), + otherwise: (schema) => + schema + .notRequired() + .nullable() + .test({ + name: testnameDisallowedBasedOnPurpose('VPC'), + message: testmessageDisallowedBasedOnPurpose('vpc', 'vpc_id'), + test: (value) => typeof value === 'undefined' || value === null, + }), }), ipv4: ipv4ConfigInterface, ipv6: ipv6ConfigInterface, @@ -205,24 +219,26 @@ export const LinodeInterfaceSchema = object().shape({ .nullable() .when('purpose', { is: 'vpc', - then: array() - .of( - string().test( - 'valid-ip-range', - 'Must be a valid IPv4 range, e.g. 192.0.2.0/24.', - validateIP + then: (schema) => + schema + .of( + string().test( + 'valid-ip-range', + 'Must be a valid IPv4 range, e.g. 192.0.2.0/24.', + validateIP + ) ) - ) - .notRequired() - .nullable(), - otherwise: array() - .test({ - name: testnameDisallowedBasedOnPurpose('VPC'), - message: testmessageDisallowedBasedOnPurpose('vpc', 'ip_ranges'), - test: (value) => typeof value === 'undefined' || value === null, - }) - .notRequired() - .nullable(), + .notRequired() + .nullable(), + otherwise: (schema) => + schema + .test({ + name: testnameDisallowedBasedOnPurpose('VPC'), + message: testmessageDisallowedBasedOnPurpose('vpc', 'ip_ranges'), + test: (value) => typeof value === 'undefined' || value === null, + }) + .notRequired() + .nullable(), }), }); @@ -279,17 +295,17 @@ export const UpdateLinodePasswordSchema = object({ }); const MetadataSchema = object({ - user_data: string().notRequired().nullable(true), + user_data: string().notRequired().nullable(), }); const PlacementGroupPayloadSchema = object({ - id: number().notRequired().nullable(true), + id: number().notRequired().nullable(), }); const DiskEncryptionSchema = string() .oneOf(['enabled', 'disabled']) .notRequired() - .nullable(true); + .nullable(); export const CreateLinodeSchema = object({ type: string().ensure().required('Plan is required.'), @@ -299,8 +315,8 @@ export const CreateLinodeSchema = object({ swap_size: number().notRequired(), image: string().when('stackscript_id', { is: (value?: number) => value !== undefined, - then: string().ensure().required('Image is required.'), - otherwise: string().nullable().notRequired(), + then: (schema) => schema.ensure().required('Image is required.'), + otherwise: (schema) => schema.nullable().notRequired(), }), authorized_keys: array().of(string()).notRequired(), backups_enabled: boolean().notRequired(), @@ -316,11 +332,12 @@ export const CreateLinodeSchema = object({ authorized_users: array().of(string()).notRequired(), root_pass: string().when('image', { is: (value: any) => Boolean(value), - then: string().required( - 'You must provide a root password when deploying from an image.' - ), + then: (schema) => + schema.required( + 'You must provide a root password when deploying from an image.' + ), // .concat(rootPasswordValidation), - otherwise: string().notRequired(), + otherwise: (schema) => schema.notRequired(), }), interfaces: LinodeInterfacesSchema, metadata: MetadataSchema, @@ -429,9 +446,9 @@ export const CreateSnapshotSchema = object({ }); const device = object({ - disk_id: number().nullable(true), - volume_id: number().nullable(true), -}).nullable(true); + disk_id: number().nullable(), + volume_id: number().nullable(), +}).nullable(); const devices = object({ sda: device, @@ -496,11 +513,12 @@ export const CreateLinodeDiskSchema = object({ authorized_users: array().of(string()), root_pass: string().when('image', { is: (value: any) => Boolean(value), - then: string().required( - 'You must provide a root password when deploying from an image.' - ), + then: (schema) => + schema.required( + 'You must provide a root password when deploying from an image.' + ), // .concat(rootPasswordValidation), - otherwise: string().notRequired(), + otherwise: (schema) => schema.notRequired(), }), stackscript_id: number(), stackscript_data, diff --git a/packages/validation/src/managed.schema.ts b/packages/validation/src/managed.schema.ts index bba3040b170..fca538dd03b 100644 --- a/packages/validation/src/managed.schema.ts +++ b/packages/validation/src/managed.schema.ts @@ -68,13 +68,13 @@ export const createContactSchema = object().shape({ .email('Invalid e-mail address'), phone: object() .shape({ - primary: string().nullable(true).notRequired(), - secondary: string().nullable(true).notRequired(), + primary: string().nullable().notRequired(), + secondary: string().nullable().notRequired(), }) .notRequired(), group: string() .notRequired() - .nullable(true) + .nullable() .min(2, 'Group must be between 2 and 50 characters.') .max(50, 'Group must be between 2 and 50 characters.'), }); diff --git a/packages/validation/src/networking.schema.ts b/packages/validation/src/networking.schema.ts index 26870d0c58a..9628c74f75a 100644 --- a/packages/validation/src/networking.schema.ts +++ b/packages/validation/src/networking.schema.ts @@ -1,7 +1,7 @@ import { array, boolean, number, object, string } from 'yup'; export const updateIPSchema = object().shape({ - rdns: string().notRequired().nullable(true), + rdns: string().notRequired().nullable(), }); export const allocateIPSchema = object().shape({ diff --git a/packages/validation/src/nodebalancers.schema.ts b/packages/validation/src/nodebalancers.schema.ts index d83942c46fd..6cefa2592c4 100644 --- a/packages/validation/src/nodebalancers.schema.ts +++ b/packages/validation/src/nodebalancers.schema.ts @@ -63,7 +63,7 @@ export const createNodeBalancerConfigSchema = object({ .integer(), check_body: string().when('check', { is: 'http_body', - then: string().required('An HTTP body regex is required.'), + then: (schema) => schema.required('An HTTP body regex is required.'), }), check_interval: number() .min( @@ -81,11 +81,11 @@ export const createNodeBalancerConfigSchema = object({ .matches(/\/.*/) .when('check', { is: 'http', - then: string().required('An HTTP path is required.'), + then: (schema) => schema.required('An HTTP path is required.'), }) .when('check', { is: 'http_body', - then: string().required('An HTTP path is required.'), + then: (schema) => schema.required('An HTTP path is required.'), }), proxy_protocol: string().oneOf(['none', 'v1', 'v2']), check_timeout: number() @@ -109,11 +109,12 @@ export const createNodeBalancerConfigSchema = object({ protocol: mixed().oneOf(['http', 'https', 'tcp']), ssl_key: string().when('protocol', { is: 'https', - then: string().required('SSL key is required when using HTTPS.'), + then: (schema) => schema.required('SSL key is required when using HTTPS.'), }), ssl_cert: string().when('protocol', { is: 'https', - then: string().required('SSL certificate is required when using HTTPS.'), + then: (schema) => + schema.required('SSL certificate is required when using HTTPS.'), }), stickiness: mixed().oneOf(['none', 'table', 'http_cookie']), nodes: array() @@ -136,7 +137,7 @@ export const UpdateNodeBalancerConfigSchema = object({ .integer(), check_body: string().when('check', { is: 'http_body', - then: string().required('An HTTP body regex is required.'), + then: (schema) => schema.required('An HTTP body regex is required.'), }), check_interval: number() .min( @@ -154,11 +155,11 @@ export const UpdateNodeBalancerConfigSchema = object({ .matches(/\/.*/) .when('check', { is: 'http', - then: string().required('An HTTP path is required.'), + then: (schema) => schema.required('An HTTP path is required.'), }) .when('check', { is: 'http_body', - then: string().required('An HTTP path is required.'), + then: (schema) => schema.required('An HTTP path is required.'), }), proxy_protocol: string().oneOf(['none', 'v1', 'v2']), check_timeout: number() @@ -182,11 +183,11 @@ export const UpdateNodeBalancerConfigSchema = object({ protocol: mixed().oneOf(['http', 'https', 'tcp']), ssl_key: string().when('protocol', { is: 'https', - then: string().required(), + then: (schema) => schema.required(), }), ssl_cert: string().when('protocol', { is: 'https', - then: string().required(), + then: (schema) => schema.required(), }), stickiness: mixed().oneOf(['none', 'table', 'http_cookie']), nodes: array().of(nodeBalancerConfigNodeSchema), diff --git a/packages/validation/src/records.schema.ts b/packages/validation/src/records.schema.ts index f1760653a98..177b214c62c 100644 --- a/packages/validation/src/records.schema.ts +++ b/packages/validation/src/records.schema.ts @@ -8,8 +8,8 @@ const recordBaseSchema = object().shape({ .max(255, 'Priority must be between 0 and 255.'), weight: number(), port: number(), - service: string().nullable(true), - protocol: string().nullable(true), + service: string().nullable(), + protocol: string().nullable(), ttl_sec: number(), tag: string(), }); diff --git a/packages/validation/src/volumes.schema.ts b/packages/validation/src/volumes.schema.ts index b98db4c01c9..19e5960c9d6 100644 --- a/packages/validation/src/volumes.schema.ts +++ b/packages/validation/src/volumes.schema.ts @@ -20,7 +20,7 @@ const createSizeValidation = (minSize: number = 10) => export const CreateVolumeSchema = object({ region: string().when('linode_id', { is: (id: any) => id === undefined || id === '', - then: string().required('Must provide a region or a Linode ID.'), + then: (schema) => schema.required('Must provide a region or a Linode ID.'), }), linode_id: number().nullable(), size: createSizeValidation(10), diff --git a/packages/validation/src/vpcs.schema.ts b/packages/validation/src/vpcs.schema.ts index 3b3ecba58dc..4484e08047f 100644 --- a/packages/validation/src/vpcs.schema.ts +++ b/packages/validation/src/vpcs.schema.ts @@ -127,9 +127,8 @@ export const createSubnetSchema = object().shape( ipv4: string().when('ipv6', { is: (value: unknown) => value === '' || value === null || value === undefined, - then: string() - .required(IP_EITHER_BOTH_NOT_NEITHER) - .test({ + then: (schema) => + schema.required(IP_EITHER_BOTH_NOT_NEITHER).test({ name: 'IPv4 CIDR format', message: 'The IPv4 range must be in CIDR format', test: (value) => @@ -139,15 +138,14 @@ export const createSubnetSchema = object().shape( mustBeIPMask: false, }), }), - otherwise: lazy((value: string | undefined) => { - switch (typeof value) { - case 'undefined': - return string().notRequired().nullable(); - - case 'string': - return string() - .notRequired() - .test({ + otherwise: (schema) => + lazy((value: string | undefined) => { + switch (typeof value) { + case 'undefined': + return schema.notRequired().nullable(); + + case 'string': + return schema.notRequired().test({ name: 'IPv4 CIDR format', message: 'The IPv4 range must be in CIDR format', test: (value) => @@ -158,17 +156,16 @@ export const createSubnetSchema = object().shape( }), }); - default: - return string().notRequired().nullable(); - } - }), + default: + return schema.notRequired().nullable(); + } + }), }), ipv6: string().when('ipv4', { is: (value: unknown) => value === '' || value === null || value === undefined, - then: string() - .required(IP_EITHER_BOTH_NOT_NEITHER) - .test({ + then: (schema) => + schema.required(IP_EITHER_BOTH_NOT_NEITHER).test({ name: 'IPv6 prefix length', message: 'Must be the prefix length (64-125) of the IP, e.g. /64', test: (value) => @@ -178,15 +175,14 @@ export const createSubnetSchema = object().shape( mustBeIPMask: true, }), }), - otherwise: lazy((value: string | undefined) => { - switch (typeof value) { - case 'undefined': - return string().notRequired().nullable(); - - case 'string': - return string() - .notRequired() - .test({ + otherwise: (schema) => + lazy((value: string | undefined) => { + switch (typeof value) { + case 'undefined': + return schema.notRequired().nullable(); + + case 'string': + return schema.notRequired().test({ name: 'IPv6 prefix length', message: 'Must be the prefix length (64-125) of the IP, e.g. /64', @@ -198,10 +194,10 @@ export const createSubnetSchema = object().shape( }), }); - default: - return string().notRequired().nullable(); - } - }), + default: + return schema.notRequired().nullable(); + } + }), }), }, [ diff --git a/yarn.lock b/yarn.lock index d27dbbb2c2c..6ac23dd6a46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -264,7 +264,7 @@ dependencies: "@babel/types" "^7.25.6" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.25.6", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.25.6", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.25.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2" integrity sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ== @@ -2323,7 +2323,7 @@ resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== -"@types/lodash@^4.14.167", "@types/lodash@^4.14.175": +"@types/lodash@^4.14.167": version "4.17.9" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.9.tgz#0dc4902c229f6b8e2ac5456522104d7b1a230290" integrity sha512-w9iWudx1XWOHW5lQRS9iKpK/XuRhnN+0T7HvdCCd802FYkT1AMTnxndJHGrNJwRoRHkslGr4S29tjm1cT7x/7w== @@ -2639,11 +2639,6 @@ dependencies: "@types/node" "*" -"@types/yup@^0.29.13": - version "0.29.14" - resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.14.tgz#754f1dccedcc66fc2bbe290c27f5323b407ceb00" - integrity sha512-Ynb/CjHhE/Xp/4bhHmQC4U1Ox+I2OpfRYF3dnNgQqn1cHa6LK3H1wJMNPT02tSVZA6FYuXE2ITORfbnb6zBCSA== - "@types/zxcvbn@^4.4.0": version "4.4.5" resolved "https://registry.yarnpkg.com/@types/zxcvbn/-/zxcvbn-4.4.5.tgz#8ce8623ed7a36e3a76d1c0b539708dfb2e859bc0" @@ -6901,7 +6896,7 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash-es@^4.17.14, lodash-es@^4.17.21: +lodash-es@^4.17.14: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== @@ -7698,11 +7693,6 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nanoclone@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4" - integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== - nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -8229,7 +8219,7 @@ prop-types@^15.0.0, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, object-assign "^4.1.1" react-is "^16.13.1" -property-expr@^2.0.4: +property-expr@^2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8" integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA== @@ -9273,7 +9263,7 @@ string-argv@~0.3.2: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9291,15 +9281,6 @@ string-width@^3.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -9380,7 +9361,7 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -9394,13 +9375,6 @@ strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -9584,6 +9558,11 @@ through@^2.3.6, through@^2.3.8, through@~2.3.4: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== +tiny-case@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" + integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== + tiny-invariant@^1.0.2, tiny-invariant@^1.3.1, tiny-invariant@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" @@ -10346,7 +10325,7 @@ word-wrap@^1.2.5, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -10364,15 +10343,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -10487,18 +10457,15 @@ yoctocolors-cjs@^2.1.2: resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz#f4b905a840a37506813a7acaa28febe97767a242" integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA== -yup@^0.32.9: - version "0.32.11" - resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5" - integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg== +yup@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/yup/-/yup-1.4.0.tgz#898dcd660f9fb97c41f181839d3d65c3ee15a43e" + integrity sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg== dependencies: - "@babel/runtime" "^7.15.4" - "@types/lodash" "^4.14.175" - lodash "^4.17.21" - lodash-es "^4.17.21" - nanoclone "^0.2.1" - property-expr "^2.0.4" + property-expr "^2.0.5" + tiny-case "^1.0.3" toposort "^2.0.2" + type-fest "^2.19.0" zwitch@^2.0.0: version "2.0.4"